From 9c8db0d57771c1bbc82183af515275753cf3f691 Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 16 Jun 2026 22:28:37 +0200 Subject: [PATCH] feat(D.5.1): UiItemList widget + factory branch for class 0x10000031 Ports retail UIElement_ItemList (class 0x10000031) as a behavioral-leaf container that owns its UiItemSlot children procedurally. Single-cell default covers every toolbar slot; N-cell grid is deferred to the inventory phase. OnDraw syncs the cell rect to the list's Width/Height each frame so the cell is sized and hit-testable from the first rendered frame, even though the factory sets rect AFTER construction. Factory: adds `0x10000031u => new UiItemList(resolve)` arm before the fallback, so all 18 toolbar itemlist slots route to UiItemList instead of UiDatElement. Tests: 4 new (IsLeafWidget, StartsWithOneCell, Cell_returnsFirstSlot, Create_buildsUiItemList_forItemListClassId). All 4 pass; full suite green (415 pass / 2 skip in App.Tests; 0 fail total). Co-Authored-By: Claude Opus 4.8 (1M context) --- src/AcDream.App/UI/Layout/DatWidgetFactory.cs | 1 + src/AcDream.App/UI/UiItemList.cs | 61 +++++++++++++++++++ .../UI/Layout/DatWidgetFactoryTests.cs | 10 +++ tests/AcDream.App.Tests/UI/UiItemListTests.cs | 25 ++++++++ 4 files changed, 97 insertions(+) create mode 100644 src/AcDream.App/UI/UiItemList.cs create mode 100644 tests/AcDream.App.Tests/UI/UiItemListTests.cs diff --git a/src/AcDream.App/UI/Layout/DatWidgetFactory.cs b/src/AcDream.App/UI/Layout/DatWidgetFactory.cs index 4bb9ef62..0955aed4 100644 --- a/src/AcDream.App/UI/Layout/DatWidgetFactory.cs +++ b/src/AcDream.App/UI/Layout/DatWidgetFactory.cs @@ -67,6 +67,7 @@ public static class DatWidgetFactory 7 => BuildMeter(info, resolve, datFont), // UIElement_Meter 11 => new UiScrollbar(), // UIElement_Scrollbar (reg :124137) 12 => BuildText(info, resolve), // UIElement_Text (reg :115655) + 0x10000031u => new UiItemList(resolve), // UIElement_ItemList — toolbar/inventory/paperdoll slots _ => new UiDatElement(info, resolve), // generic fallback (incl. Type 3 chrome/containers) }; diff --git a/src/AcDream.App/UI/UiItemList.cs b/src/AcDream.App/UI/UiItemList.cs new file mode 100644 index 00000000..df77f47e --- /dev/null +++ b/src/AcDream.App/UI/UiItemList.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; + +namespace AcDream.App.UI; + +/// +/// 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. +/// +public sealed class UiItemList : UiElement +{ + private readonly List _cells = new(); + + public UiItemList(Func? 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? SpriteResolve { get; set; } + + /// Convenience for single-cell slots (the toolbar): the first cell. + public UiItemSlot Cell => _cells[0]; + + 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; + cell.Left = 0; cell.Top = 0; cell.Width = Width; cell.Height = Height; + _cells.Add(cell); + AddChild(cell); + } + + public void Flush() + { + foreach (var c in _cells) RemoveChild(c); + _cells.Clear(); + } + + protected override void OnDraw(UiRenderContext ctx) + { + // The factory sets THIS list's Width/Height AFTER construction, so the cell + // (added in the ctor) starts 0x0. For the single-cell toolbar slot, keep the + // cell sized to the list each frame; the cell paints itself in the children + // pass that follows. (N-cell grid layout is the inventory phase.) + if (_cells.Count > 0) + { + var cell = _cells[0]; + cell.Left = 0; cell.Top = 0; cell.Width = Width; cell.Height = Height; + } + } +} diff --git a/tests/AcDream.App.Tests/UI/Layout/DatWidgetFactoryTests.cs b/tests/AcDream.App.Tests/UI/Layout/DatWidgetFactoryTests.cs index ce7e63f9..d5079b62 100644 --- a/tests/AcDream.App.Tests/UI/Layout/DatWidgetFactoryTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/DatWidgetFactoryTests.cs @@ -126,6 +126,16 @@ public class DatWidgetFactoryTests Assert.IsType(e); } + // ── Test 7: Type 0x10000031 → UiItemList ──────────────────────────────── + + [Fact] + public void Create_buildsUiItemList_forItemListClassId() + { + var info = new AcDream.App.UI.Layout.ElementInfo { Id = 0x100001A7u, Type = 0x10000031u, Width = 32, Height = 32 }; + var w = AcDream.App.UI.Layout.DatWidgetFactory.Create(info, _ => (0u, 0, 0), null); + Assert.IsType(w); + } + // ── Test 6: Meter slice extraction (the important one) ─────────────────── /// diff --git a/tests/AcDream.App.Tests/UI/UiItemListTests.cs b/tests/AcDream.App.Tests/UI/UiItemListTests.cs new file mode 100644 index 00000000..832a8507 --- /dev/null +++ b/tests/AcDream.App.Tests/UI/UiItemListTests.cs @@ -0,0 +1,25 @@ +using AcDream.App.UI; +using Xunit; + +namespace AcDream.App.Tests.UI; + +public class UiItemListTests +{ + [Fact] + public void IsLeafWidget() => Assert.True(new UiItemList().ConsumesDatChildren); + + [Fact] + public void StartsWithOneCell_forSingleCellSlot() + { + var list = new UiItemList(); + Assert.Equal(1, list.GetNumUIItems()); + Assert.NotNull(list.GetItem(0)); + } + + [Fact] + public void Cell_returnsTheFirstSlot() + { + var list = new UiItemList(); + Assert.Same(list.GetItem(0), list.Cell); + } +}