using System; using System.Numerics; using AcDream.App.Rendering; namespace AcDream.App.UI; /// /// Icon-list cell for non-weenie catalog entries such as spells and components. /// It reuses the retail UIItemList geometry without pretending a spell id is an /// object GUID; catalog drags stay distinct from physical item drag/drop. /// public sealed class UiCatalogSlot : UiItemSlot { public uint EntryId { get; set; } public uint CatalogIconTexture { get; set; } /// Optional second 32x32 layer (retail spell endowment item icon). public uint CatalogOverlayTexture { get; set; } public string Label { get; set; } = string.Empty; public string? Detail { get; set; } public bool ShowLabel { get; init; } public uint BackgroundSprite { get; init; } public UiDatFont? LabelFont { get; init; } public Vector4 LabelColor { get; init; } = new(0.92f, 0.88f, 0.70f, 1f); /// Retail LayoutDesc property 0x21 (two-pass glyph outline, /// UIElement_Text::SetOutline @0x0046a81c). Catalog slots are /// controller-built (no authored element), so this is settable-only, for /// parity with the other text-bearing widgets (round-5 review S2 — /// per-STATE switching is AP-192). public bool Outline { get; init; } /// Retail LayoutDesc property 0x22 (m_curOutlineColor, /// ctor default black). Only meaningful when is true. public Vector4 OutlineColor { get; init; } = UiRenderContext.DefaultOutlineColor; public float IconLeft { get; init; } public float IconTop { get; init; } public float IconWidth { get; init; } public float IconHeight { get; init; } public float LabelLeft { get; init; } public float LabelWidth { get; init; } public bool SelectionBehindContent { get; init; } public new Action? Clicked { get; set; } public new Action? DoubleClicked { get; set; } public object? CatalogDragPayload { get; init; } public Action? DragBegan { get; init; } public Action? DragEnded { get; init; } public Action? Dropped { get; init; } /// /// Per-panel drag-over acceptance — the catalog-cell port of retail's /// per-list drag handler deciding the rollover state /// (UIElement_ItemList::ItemList_DragOver @ 0x004E3400 → /// m_dragHandler->OnItemListDragOver; the spell bar's is /// SpellCastSubMenu::OnItemListDragOver @ 0x004C5990). While a drag /// hovers this cell, the returned acceptance drives the authored /// DragAccept frame (accept 0x060011F9 / reject 0x060011F8). Null (the /// default for every list that never set one) keeps the cell neutral — /// no overlay, exactly the pre-existing presentation. /// public Func? DragOverAcceptance { get; init; } protected override bool IsShortcutOccupied => EntryId != 0u; public override bool IsEmptySlot => EntryId == 0u; public override string? GetTooltipText() => string.IsNullOrWhiteSpace(Label) ? null : Label; public override bool IsDragSource => CatalogDragPayload is not null; public override object? GetDragPayload() => CatalogDragPayload; public override (uint tex, int w, int h)? GetDragGhost() => CatalogDragPayload is not null && CatalogIconTexture != 0u ? (CatalogIconTexture, 32, 32) : null; internal override void SetDragSourceActive(bool active, object? payload) { if (!active && payload is not null) DragEnded?.Invoke(payload); } public override bool OnEvent(in UiEvent e) { switch (e.Type) { case UiEventType.MouseDown: if (EntryId != 0u && FindList() is { PrimaryCatalogEntryPressed: { } pressed }) pressed(EntryId); return true; case UiEventType.Click: Clicked?.Invoke(); return true; case UiEventType.DoubleClick: DoubleClicked?.Invoke(); return true; case UiEventType.RightClick: if (EntryId != 0u && FindList() is { ExamineCatalogEntryRequested: { } examine }) examine(EntryId); return true; case UiEventType.DragBegin: if (e.Payload is not null) DragBegan?.Invoke(e.Payload); return true; case UiEventType.DragEnter: // Pointer entered mid-drag: ask the panel's acceptance seam and show // the authored frame (retail ItemList_DragOver → the list handler's // SetDragAcceptState on the hovered UIItem's 0x1000045A child). SetDragAcceptVisual(e.Payload is { } enterPayload ? DragOverAcceptance?.Invoke(enterPayload) switch { ItemDragAcceptance.Accept => DragAcceptState.Accept, ItemDragAcceptance.Reject => DragAcceptState.Reject, _ => DragAcceptState.None, } : DragAcceptState.None); return true; case UiEventType.DragOver: // UiRoot fires DragOver on LEAVE — retail's dwParam1 == 0 reset to // ItemSlot_DragOver_Normal (0x1000003F) @ 0x004E1438-0x004E1456. SetDragAcceptVisual(DragAcceptState.None); return true; case UiEventType.DropReleased: SetDragAcceptVisual(DragAcceptState.None); if (e.Payload is not null) Dropped?.Invoke(e.Payload); return true; default: return false; } } protected override void OnDraw(UiRenderContext ctx) { if (BackgroundSprite != 0u && SpriteResolve is not null) { var (texture, _, _) = SpriteResolve(BackgroundSprite); if (texture != 0u) ctx.DrawSprite(texture, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One); } if (SelectionBehindContent) DrawSelection(ctx); float iconWidth = IconWidth > 0f ? IconWidth : ShowLabel ? MathF.Min(32f, Height) : Width; float iconHeight = IconHeight > 0f ? IconHeight : ShowLabel ? MathF.Min(32f, Height) : Height; if (CatalogIconTexture != 0) ctx.DrawSprite(CatalogIconTexture, IconLeft, IconTop, iconWidth, iconHeight, 0f, 0f, 1f, 1f, Vector4.One); else if (SpriteResolve is not null && EmptySprite != 0) { var (texture, _, _) = SpriteResolve(EmptySprite); if (texture != 0) ctx.DrawSprite(texture, IconLeft, IconTop, iconWidth, iconHeight, 0f, 0f, 1f, 1f, Vector4.One); } if (CatalogOverlayTexture != 0) ctx.DrawSprite(CatalogOverlayTexture, IconLeft, IconTop, iconWidth, iconHeight, 0f, 0f, 1f, 1f, Vector4.One); // Spell favorites are UIElement_UIItems too. Retail's // SpellCastSubMenu::UpdateShortcutOverlays @ 0x004C5BE0 calls the same // SetShortcutNum path used by physical toolbar shortcuts. DrawShortcutOverlay(ctx); if (!SelectionBehindContent) DrawSelection(ctx); if (ShowLabel) { float labelLeft = LabelLeft > 0f ? LabelLeft : IconLeft + iconWidth + 4f; float labelWidth = LabelWidth > 0f ? LabelWidth : MathF.Max(0f, Width - labelLeft); if (LabelFont is not null) { string text = FitText(Label, labelWidth, LabelFont.MeasureWidth); float y = MathF.Max(0f, (Height - LabelFont.LineHeight) * 0.5f); ctx.DrawStringDat(LabelFont, text, labelLeft, y, LabelColor, Outline, OutlineColor); } else { BitmapFont? font = ctx.DefaultFont; string text = font is null ? Label : FitText(Label, labelWidth, font.MeasureWidth); float y = font is null ? 3f : MathF.Max(0f, (Height - font.LineHeight) * 0.5f); ctx.DrawString(text, labelLeft, y, LabelColor, font); } if (!string.IsNullOrWhiteSpace(Detail)) ctx.DrawString(Detail!, MathF.Max(labelLeft, Width - 48f), 3f, Vector4.One); } // Drag-rollover frame last — same layering as the physical UIItem draw // (accept/reject above selection; catalog cells have no cooldown layer). DrawDragAcceptOverlay(ctx); } private void DrawSelection(UiRenderContext ctx) { if (!Selected || SpriteResolve is null || SelectedSprite == 0u) return; var (texture, _, _) = SpriteResolve(SelectedSprite); if (texture != 0u) ctx.DrawSprite(texture, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One); } private static string FitText(string text, float width, Func measure) { if (width <= 0f || string.IsNullOrEmpty(text) || measure(text) <= width) return text; const string ellipsis = "..."; float ellipsisWidth = measure(ellipsis); if (ellipsisWidth >= width) return string.Empty; int low = 0, high = text.Length; while (low < high) { int mid = (low + high + 1) / 2; if (measure(text[..mid]) + ellipsisWidth <= width) low = mid; else high = mid - 1; } return text[..low] + ellipsis; } }