173 lines
7.4 KiB
C#
173 lines
7.4 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
|
|
namespace AcDream.App.UI;
|
|
|
|
/// <summary>
|
|
/// A container of item cells (port of retail UIElement_ItemList, class 0x10000031).
|
|
/// Behavioral LEAF: it creates/owns its UiItemSlot children procedurally, so the
|
|
/// LayoutImporter must NOT build dat children. The toolbar uses single-cell
|
|
/// instances (one slot); the inventory phase will grow this to an N-cell grid.
|
|
/// </summary>
|
|
public sealed class UiItemList : UiElement
|
|
{
|
|
private readonly List<UiItemSlot> _cells = new();
|
|
|
|
/// <summary>Vertical scroll model for grid mode (clip+scroll). Bound to the gutter
|
|
/// UiScrollbar by the panel controller. Inert in fill mode (single toolbar cell).</summary>
|
|
public UiScrollable Scroll { get; } = new();
|
|
|
|
public UiItemList(Func<uint, (uint tex, int w, int h)>? spriteResolve = null)
|
|
{
|
|
SpriteResolve = spriteResolve;
|
|
// Single-cell default: every toolbar slot always shows one cell (empty or filled).
|
|
AddItem(new UiItemSlot { SpriteResolve = spriteResolve });
|
|
}
|
|
|
|
public override bool ConsumesDatChildren => true;
|
|
|
|
public Func<uint, (uint tex, int w, int h)>? SpriteResolve { get; set; }
|
|
|
|
private uint _cellEmptySprite;
|
|
/// <summary>Empty-slot sprite for THIS list's cells, resolved from the dat cell template
|
|
/// (retail attribute 0x1000000e -> catalog 0x21000037 prototype's ItemSlot_Empty; see
|
|
/// <see cref="Layout.ItemListCellTemplate.ResolveEmptySprite"/>). 0 = leave the
|
|
/// <see cref="UiItemSlot.EmptySprite"/> default (0x060074CF). Applied to every existing
|
|
/// AND future cell.</summary>
|
|
public uint CellEmptySprite
|
|
{
|
|
get => _cellEmptySprite;
|
|
set
|
|
{
|
|
_cellEmptySprite = value;
|
|
if (value != 0)
|
|
foreach (var c in _cells) c.EmptySprite = value;
|
|
}
|
|
}
|
|
|
|
/// <summary>The drag handler this list routes drops to (a panel controller).
|
|
/// Null = the list accepts no drops. Retail: UIElement_ItemList::m_dragHandler.</summary>
|
|
public IItemListDragHandler? DragHandler { get; private set; }
|
|
|
|
/// <summary>Register the panel's drag handler on this list. Retail:
|
|
/// UIElement_ItemList::RegisterItemListDragHandler (decomp 230461).</summary>
|
|
public void RegisterDragHandler(IItemListDragHandler handler) => DragHandler = handler;
|
|
|
|
/// <summary>Convenience for single-cell slots (the toolbar): the first cell.
|
|
/// Valid only while the list has at least one cell; after <see cref="Flush"/>
|
|
/// (the inventory-phase rebuild path) the list is empty until <see cref="AddItem"/>
|
|
/// runs, so use <see cref="GetItem"/> there instead.</summary>
|
|
/// <exception cref="InvalidOperationException">the list has no cells (e.g. after Flush).</exception>
|
|
public UiItemSlot Cell => _cells.Count > 0
|
|
? _cells[0]
|
|
: throw new InvalidOperationException("UiItemList has no cells; call AddItem first or use GetItem(index).");
|
|
|
|
public int GetNumUIItems() => _cells.Count;
|
|
|
|
public UiItemSlot? GetItem(int index)
|
|
=> index >= 0 && index < _cells.Count ? _cells[index] : null;
|
|
|
|
public void AddItem(UiItemSlot cell)
|
|
{
|
|
cell.SpriteResolve ??= SpriteResolve;
|
|
if (_cellEmptySprite != 0) cell.EmptySprite = _cellEmptySprite;
|
|
// The list lays cells out procedurally (grid offset + scroll clip), so cells must be
|
|
// EXEMPT from the parent anchor pass — UiElement.DrawSelfAndChildren runs ApplyAnchor
|
|
// on every child after OnDraw, which would otherwise reset the scroll offset to each
|
|
// cell's captured base position (the escaping-grid bug). Anchors=None makes LayoutCells
|
|
// the sole authority over cell rects.
|
|
cell.Anchors = AnchorEdges.None;
|
|
_cells.Add(cell);
|
|
AddChild(cell);
|
|
LayoutCells();
|
|
}
|
|
|
|
/// <summary>Grid columns (row-major). 1 = single column. Ignored in fill mode.</summary>
|
|
public int Columns { get; set; } = 1;
|
|
|
|
/// <summary>Fixed cell width in grid mode. 0 = "fill the list" — the single cell sizes
|
|
/// to the whole list (the toolbar single-slot legacy). Set >0 (with CellHeight) for a grid.</summary>
|
|
public float CellWidth { get; set; }
|
|
/// <summary>Fixed cell height in grid mode (pairs with CellWidth).</summary>
|
|
public float CellHeight { get; set; }
|
|
|
|
/// <summary>Whole rows needed for <paramref name="cellCount"/> cells in a grid of
|
|
/// <paramref name="columns"/> columns.</summary>
|
|
public static int RowCount(int cellCount, int columns)
|
|
{
|
|
int cols = columns < 1 ? 1 : columns;
|
|
return (cellCount + cols - 1) / cols;
|
|
}
|
|
|
|
/// <summary>Row-major pixel offset of cell <paramref name="index"/> in a grid of
|
|
/// <paramref name="columns"/> columns at the given cell pitch.</summary>
|
|
internal static (float x, float y) CellOffset(int index, int columns, float cellW, float cellH)
|
|
{
|
|
int col = index % columns, row = index / columns;
|
|
return (col * cellW, row * cellH);
|
|
}
|
|
|
|
/// <summary>Position every cell per the current mode: fill (CellWidth<=0) sizes the single
|
|
/// cell to the list; grid (CellWidth>0) tiles cells row-major, offset by the scroll position
|
|
/// with whole-row vertical clipping (cells fully outside the view are hidden).</summary>
|
|
internal void LayoutCells()
|
|
{
|
|
if (CellWidth <= 0f)
|
|
{
|
|
// Fill mode (the toolbar single cell): size the one cell to the list.
|
|
if (_cells.Count > 0)
|
|
{
|
|
var c = _cells[0];
|
|
c.Left = 0; c.Top = 0; c.Width = Width; c.Height = Height; c.Visible = true;
|
|
}
|
|
return;
|
|
}
|
|
|
|
int cols = Columns < 1 ? 1 : Columns;
|
|
int cellH = (int)MathF.Round(CellHeight);
|
|
|
|
// Drive the shared scroll model from the current geometry, then re-clamp the offset to
|
|
// the (possibly changed) max. ContentHeight/ViewHeight must be set BEFORE reading ScrollY
|
|
// so the clamp uses the right max.
|
|
Scroll.LineHeight = cellH > 0 ? cellH : 1;
|
|
Scroll.ContentHeight = RowCount(_cells.Count, cols) * cellH;
|
|
Scroll.ViewHeight = (int)MathF.Floor(Height);
|
|
Scroll.SetScrollY(Scroll.ScrollY); // re-clamp to the new max
|
|
float scrollY = Scroll.ScrollY;
|
|
|
|
for (int i = 0; i < _cells.Count; i++)
|
|
{
|
|
var (x, baseY) = CellOffset(i, cols, CellWidth, CellHeight);
|
|
float top = baseY - scrollY;
|
|
var cell = _cells[i];
|
|
cell.Left = x; cell.Top = top; cell.Width = CellWidth; cell.Height = CellHeight;
|
|
// Whole-row vertical clip (no scissor — mirrors UiText.cs:198). A row fully inside
|
|
// [0, Height] draws; a partially-scrolled row is hidden.
|
|
cell.Visible = top >= -0.5f && top + CellHeight <= Height + 0.5f;
|
|
}
|
|
}
|
|
|
|
public void Flush()
|
|
{
|
|
foreach (var c in _cells) RemoveChild(c);
|
|
_cells.Clear();
|
|
}
|
|
|
|
public override bool OnEvent(in UiEvent e)
|
|
{
|
|
if (e.Type == UiEventType.Scroll && CellWidth > 0f)
|
|
{
|
|
// Mirror UiText: Silk +Y wheel = up/older = decrease ScrollY; negate Data0.
|
|
Scroll.ScrollByLines(-e.Data0);
|
|
return true;
|
|
}
|
|
return base.OnEvent(e);
|
|
}
|
|
|
|
protected override void OnDraw(UiRenderContext ctx)
|
|
{
|
|
// The factory sets Width/Height AFTER construction, so re-layout each frame:
|
|
// fill mode keeps the single toolbar cell sized to the list; grid mode tiles cells.
|
|
LayoutCells();
|
|
}
|
|
}
|