using System;
using System.Numerics;
namespace AcDream.App.UI;
///
/// One item-in-a-slot cell (port of retail UIElement_UIItem, class 0x10000032).
/// A behavioral LEAF: it draws the empty-slot sprite when unbound, else a
/// pre-composited icon texture (set by the controller). Holds the bound weenie
/// guid (retail UIElement_UIItem::itemID, +0x5FC).
///
public sealed class UiItemSlot : UiElement
{
public UiItemSlot() { ClickThrough = false; }
public override bool ConsumesDatChildren => true;
/// Bound weenie guid (0 = empty). Retail UIElement_UIItem::itemID.
public uint ItemId { get; private set; }
/// Pre-composited icon GL texture for the bound item (0 = none).
public uint IconTexture { get; private set; }
/// This cell's own index within its panel (0..17 toolbar; container slot
/// for inventory). Distinct from (the 1–9 label, -1 on the
/// bottom row). Set by the controller; used as the drag payload's SourceSlot and to
/// identify the drop TARGET slot.
public int SlotIndex { get; set; } = -1;
/// What kind of slot this is, for the drag payload (retail InqDropIconInfo
/// flags). Controller overrides; default Inventory.
public ItemDragSource SourceKind { get; set; } = ItemDragSource.Inventory;
/// Drag-rollover accept frame (retail ItemSlot_DragOver_Accept 0x060011F9,
/// state id 0x10000041). Configurable; guard id != 0 before resolving.
public uint DragAcceptSprite { get; set; } = 0x060011F9u;
/// Drag-rollover reject frame (retail ItemSlot_DragOver_Reject 0x060011F8,
/// state id 0x10000040).
public uint DragRejectSprite { get; set; } = 0x060011F8u;
/// True when this cell is the OPEN container (its contents fill the grid). Draws the
/// open-container triangle. Port of UIElement_ItemList::UpdateOpenContainerIndicator
/// (0x004e3070) → SetOpenContainerState (0x004e1200): shown when item.itemID == openContainerId.
public bool IsOpenContainer { get; set; }
/// Open-container triangle sprite (element 0x10000450 on the container prototype
/// 0x1000033F). Configurable; guard id != 0 before resolving.
public uint OpenContainerSprite { get; set; } = 0x06005D9Cu;
/// True when this cell is the SELECTED item. Draws the green/yellow selection square.
/// Port of UIElement_ItemList::ItemList_SetSelectedItem (0x004e2fe0) → SetSelectedState
/// (0x004e1240): shown when item.itemID == selectedItemId. Uniform across item + container cells.
public bool Selected { get; set; }
/// Selected-item square sprite (element 0x10000342 on the 32×32 item prototype
/// 0x10000341; pixel-confirmed green/yellow frame). Drawn as a procedural overlay so it renders
/// on the 36×36 container cell too (whose prototype lacks the square child).
public uint SelectedSprite { get; set; } = 0x06004D21u;
/// Container fullness [0..1], or -1 = hidden (the cell is not a container, or its
/// itemsCapacity is unknown/0). Port of UIElement_UIItem::UpdateCapacityDisplay (0x004e16e0): a
/// per-cell vertical UIElement_Meter (element 0x10000347, 5×30 at x=26,y=1) shown only when
/// isContainer && itemsCapacity > 0, fill = numContainedItems / itemsCapacity (meter attr 0x69).
public float CapacityFill { get; set; } = -1f;
/// Capacity-bar track sprite (meter 0x10000347 DirectState).
public uint CapacityBackSprite { get; set; } = 0x06004D22u;
/// Capacity-bar fill sprite (meter 0x10000347 front child 0x00000002 DirectState).
public uint CapacityFrontSprite { get; set; } = 0x06004D23u;
/// Accept/reject overlay state while a drag hovers this cell.
public enum DragAcceptState { None, Accept, Reject }
private DragAcceptState _dragAccept = DragAcceptState.None;
/// Current overlay state — internal so unit tests can assert it (InternalsVisibleTo).
internal DragAcceptState DragAcceptVisual => _dragAccept;
/// Empty-slot sprite. Default = the generic toolbar empty-slot border
/// 0x060074CF (uiitem template 0x21000037, state ItemSlot_Empty). Configurable so
/// paperdoll equip slots can use their per-slot silhouettes later.
public uint EmptySprite { get; set; } = 0x060074CFu;
/// RenderSurface id -> (GL texture, w, h). Set by the factory/controller.
public Func? SpriteResolve { get; set; }
public void SetItem(uint itemId, uint iconTexture)
{
ItemId = itemId;
IconTexture = iconTexture;
}
public void Clear() { ItemId = 0; IconTexture = 0; }
///
public override object? GetDragPayload()
=> ItemId != 0 ? new ItemDragPayload(ItemId, SourceKind, SlotIndex, this) : null;
///
public override (uint tex, int w, int h)? GetDragGhost()
=> ItemId != 0 && IconTexture != 0 ? (IconTexture, (int)Width, (int)Height) : null;
/// An OCCUPIED slot is a drag source — a press-and-move picks up the item
/// rather than moving the toolbar window. An EMPTY slot is NOT a drag source, so a
/// press-and-move there falls through to the IA-12 whole-window-drag, keeping the bar
/// movable by its empty cells / chrome. Drives 's mousedown
/// window-vs-item disambiguation (retail moves the window via a dragbar, never cells;
/// our whole-window-drag approximation reconciles by gating on occupancy).
public override bool IsDragSource => ItemId != 0;
/// Walk up to the containing (the drop handler owner).
private UiItemList? FindList()
{
UiElement? e = Parent;
while (e is not null) { if (e is UiItemList l) return l; e = e.Parent; }
return null;
}
// ── Shortcut number (slot label) ─────────────────────────────────────────
// Port of UIElement_UIItem::SetShortcutNum (acclient_2013_pseudo_c.txt:229465).
// Retail draws the digit on the cell's ShortcutNum sub-element, picking the
// digit image from a DID-array property: 0x10000042 (peace) / 0x10000043 (war),
// indexed by slot position. Each digit is a 32×32 PFID_A8R8G8B8 RenderSurface
// with the digit baked into the top-left corner (rest alpha=0), drawn Alphablend.
/// Slot position in the shortcut bar (0-indexed). -1 = no number (retail
/// SetVisible(0) when edi < 0). Top row: 0..8 → digits 1..9. Bottom row: -1.
public int ShortcutNum { get; private set; } = -1;
/// True = draw peace digit set; false = war digit set.
public bool ShortcutPeace { get; private set; } = true;
/// Peace digit DID array. Index i → digit (i+1) sprite RenderSurface id.
/// Injected by the controller after reading LayoutDesc 0x21000037.
/// Retail ref: UIElement_UIItem::SetShortcutNum (decomp 229481) — occupied slot picks
/// property 0x10000042 (peace) or 0x10000043 (war) by stance.
public uint[]? PeaceDigits { get; set; }
/// War digit DID array. Same layout as PeaceDigits.
/// Retail ref: UIElement_UIItem::SetShortcutNum (decomp 229493) — war stance.
public uint[]? WarDigits { get; set; }
/// Empty-slot digit DID array (property 0x1000005e, stance-independent).
/// Used when the slot is EMPTY (ItemId == 0). Retail ref: UIElement_UIItem::SetShortcutNum
/// (decomp 229481) — else branch when m_elem_Icon->m_state == 0x1000001c (empty).
public uint[]? EmptyDigits { get; set; }
/// Set the slot's shortcut position and combat stance so the correct digit
/// is drawn. Call with index 0..8 for the top row; pass peace=true for NonCombat.
public void SetShortcutNum(int index, bool peace)
{
ShortcutNum = index;
ShortcutPeace = peace;
}
/// Clear the shortcut number label (hides the digit).
public void ClearShortcutNum() { ShortcutNum = -1; }
///
/// Returns the digit DID array that OnDraw will use, following the retail occupancy
/// branch in UIElement_UIItem::SetShortcutNum (decomp 229481):
/// occupied (ItemId != 0) → ShortcutPeace ? PeaceDigits : WarDigits (0x10000042/43)
/// empty (ItemId == 0) → EmptyDigits (0x1000005e, stance-independent)
/// Exposed as an internal method so unit tests can assert array selection without
/// needing a real render context.
///
internal uint[]? ActiveDigitArray()
{
bool occupied = ItemId != 0;
return occupied ? (ShortcutPeace ? PeaceDigits : WarDigits) : EmptyDigits;
}
// ── Events / draw ─────────────────────────────────────────────────────────
/// Invoked by when a left-button-down lands on
/// a bound slot. Wired by ToolbarController to the use-item callback.
public Action? Clicked { get; set; }
///
public override bool OnEvent(in UiEvent e)
{
switch (e.Type)
{
// Use fires on CLICK (mouse-up over the same cell), not MouseDown — so a
// drag (press + >3px move) does NOT also use the item. UiRoot suppresses the
// post-drag Click (UiRoot.cs FinishDrag returns before the Click emit).
case UiEventType.MouseDown:
return true; // consume the press; no use here
case UiEventType.Click:
Clicked?.Invoke();
return true;
case UiEventType.DragBegin:
// Notify the source list's handler so it can lift (remove + wire) — retail
// RecvNotice_ItemListBeginDrag → RemoveShortcut. UiRoot snapshotted the ghost first.
if (FindList() is { DragHandler: { } lh } liftList && e.Payload is ItemDragPayload lp)
lh.OnDragLift(liftList, this, lp);
return true;
case UiEventType.DragEnter: // pointer entered me mid-drag → ask the list's handler
_dragAccept = (FindList() is { DragHandler: { } h } list
&& e.Payload is ItemDragPayload p && h.OnDragOver(list, this, p))
? DragAcceptState.Accept : DragAcceptState.Reject;
return true;
case UiEventType.DragOver: // UiRoot fires this on LEAVE → neutral
_dragAccept = DragAcceptState.None;
return true;
case UiEventType.DropReleased:
_dragAccept = DragAcceptState.None;
if (FindList() is { DragHandler: { } dh } dl && e.Payload is ItemDragPayload dp)
dh.HandleDropRelease(dl, this, dp);
return true;
}
return false;
}
protected override void OnDraw(UiRenderContext ctx)
{
// Draw the icon (filled slot) or the empty-slot border. Both paths fall through
// to the digit draw below; the slot label always shows on top-row slots.
if (ItemId != 0 && IconTexture != 0)
{
ctx.DrawSprite(IconTexture, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One);
}
else if (SpriteResolve is not null && EmptySprite != 0)
{
var (tex, _, _) = SpriteResolve(EmptySprite);
if (tex != 0)
ctx.DrawSprite(tex, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One);
}
// Digit overlay: UIElement_UIItem::SetShortcutNum (acclient_2013_pseudo_c.txt:229465).
// Occupancy branch (decomp 229481):
// occupied (ItemId != 0) → peace/war digit set 0x10000042/43, split by stance
// empty (ItemId == 0) → background digit set 0x1000005e, stance-independent
// Each digit image is corner-baked (glyph in top-left, rest alpha=0); drawn
// full-cell Alphablend so the transparent region is invisible.
if (ShortcutNum >= 0 && SpriteResolve is not null)
{
var arr = ActiveDigitArray();
if (arr is not null && ShortcutNum < arr.Length)
{
uint did = arr[ShortcutNum];
if (did != 0)
{
var (tex, _, _) = SpriteResolve(did);
if (tex != 0)
ctx.DrawSprite(tex, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One);
}
}
}
// Container capacity bar — UIElement_UIItem::UpdateCapacityDisplay (0x004e16e0): a vertical
// UIElement_Meter (element 0x10000347, 5×30 at x=26,y=1) shown only for container cells
// (CapacityFill >= 0). Track drawn full; fill clipped to the fraction from the BOTTOM (the
// "how full" direction). Procedural — UiItemSlot is a behavioral leaf. Guard id != 0 first.
if (CapacityFill >= 0f && SpriteResolve is not null)
{
const float by = 1f, bw = 5f, bh = 30f; // element 0x10000347 size (dat 5×30 at y=1)
float bx = Width - bw; // flush to the cell's right edge (visual gate: the dat X=26 sat ~5px off the edge)
if (CapacityBackSprite != 0)
{
var (bt, _, _) = SpriteResolve(CapacityBackSprite);
if (bt != 0) ctx.DrawSprite(bt, bx, by, bw, bh, 0f, 0f, 1f, 1f, Vector4.One);
}
float f = Math.Clamp(CapacityFill, 0f, 1f);
if (f > 0f && CapacityFrontSprite != 0)
{
var (ft, _, _) = SpriteResolve(CapacityFrontSprite);
if (ft != 0)
{
// Bottom-up fill: draw the bottom f-fraction of the bar, sampling the matching
// bottom slice of the front sprite (UV v from 1-f to 1).
float fh = bh * f;
ctx.DrawSprite(ft, bx, by + (bh - fh), bw, fh, 0f, 1f - f, 1f, 1f, Vector4.One);
}
}
}
// Open-container triangle (retail SetOpenContainerState 0x004e1200) + selected-item square
// (retail SetSelectedState 0x004e1240). Drawn procedurally like the digit/drag overlays;
// guard id != 0 BEFORE resolving (feedback_ui_resolve_zero_magenta). Triangle under the
// square; both under the transient drag overlay below.
if (IsOpenContainer && SpriteResolve is not null && OpenContainerSprite != 0)
{
var (tex, _, _) = SpriteResolve(OpenContainerSprite);
if (tex != 0)
ctx.DrawSprite(tex, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One);
}
if (Selected && SpriteResolve is not null && SelectedSprite != 0)
{
var (tex, _, _) = SpriteResolve(SelectedSprite);
if (tex != 0)
ctx.DrawSprite(tex, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One);
}
// Drag-rollover accept/reject frame (retail SetDragAcceptState 0x10000041/40).
// Guard id != 0 BEFORE resolving — resolve(0) returns the 1×1 magenta placeholder
// with a non-zero GL handle (feedback_ui_resolve_zero_magenta).
if (_dragAccept != DragAcceptState.None && SpriteResolve is not null)
{
uint id = _dragAccept == DragAcceptState.Accept ? DragAcceptSprite : DragRejectSprite;
if (id != 0)
{
var (tex, _, _) = SpriteResolve(id);
if (tex != 0)
ctx.DrawSprite(tex, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One);
}
}
}
}