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);
+ }
+}