diff --git a/src/AcDream.App/UI/UiItemList.cs b/src/AcDream.App/UI/UiItemList.cs
index 6c4ce9bf..f550e53c 100644
--- a/src/AcDream.App/UI/UiItemList.cs
+++ b/src/AcDream.App/UI/UiItemList.cs
@@ -49,9 +49,48 @@ public sealed class UiItemList : UiElement
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);
+ LayoutCells();
+ }
+
+ /// Grid columns (row-major). 1 = single column. Ignored in fill mode.
+ public int Columns { get; set; } = 1;
+
+ /// 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.
+ public float CellWidth { get; set; }
+ /// Fixed cell height in grid mode (pairs with CellWidth).
+ public float CellHeight { get; set; }
+
+ /// Row-major pixel offset of cell in a grid of
+ /// columns at the given cell pitch.
+ 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);
+ }
+
+ /// Position every cell per the current mode: fill (CellWidth<=0) sizes the single
+ /// cell to the list; grid (CellWidth>0) tiles cells row-major at the cell pitch.
+ private void LayoutCells()
+ {
+ if (CellWidth <= 0f)
+ {
+ if (_cells.Count > 0)
+ {
+ var c = _cells[0];
+ c.Left = 0; c.Top = 0; c.Width = Width; c.Height = Height;
+ }
+ return;
+ }
+ int cols = Columns < 1 ? 1 : Columns;
+ for (int i = 0; i < _cells.Count; i++)
+ {
+ var (x, y) = CellOffset(i, cols, CellWidth, CellHeight);
+ var cell = _cells[i];
+ cell.Left = x; cell.Top = y; cell.Width = CellWidth; cell.Height = CellHeight;
+ }
}
public void Flush()
@@ -62,14 +101,8 @@ public sealed class UiItemList : UiElement
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;
- }
+ // 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();
}
}
diff --git a/tests/AcDream.App.Tests/UI/UiItemListGridTests.cs b/tests/AcDream.App.Tests/UI/UiItemListGridTests.cs
new file mode 100644
index 00000000..4053eac1
--- /dev/null
+++ b/tests/AcDream.App.Tests/UI/UiItemListGridTests.cs
@@ -0,0 +1,43 @@
+using AcDream.App.UI;
+
+namespace AcDream.App.Tests.UI;
+
+public class UiItemListGridTests
+{
+ [Fact]
+ public void CellOffset_RowMajor()
+ {
+ Assert.Equal((0f, 0f), UiItemList.CellOffset(0, 3, 36, 36));
+ Assert.Equal((72f, 0f), UiItemList.CellOffset(2, 3, 36, 36));
+ Assert.Equal((36f, 36f), UiItemList.CellOffset(4, 3, 36, 36)); // col 1, row 1
+ }
+
+ [Fact]
+ public void GridMode_PositionsCellsInColumns()
+ {
+ var list = new UiItemList { Columns = 3, CellWidth = 36, CellHeight = 36 };
+ list.Flush(); // drop the ctor's default cell
+ for (int i = 0; i < 7; i++) list.AddItem(new UiItemSlot());
+
+ var c4 = list.GetItem(4)!;
+ Assert.Equal(36f, c4.Left);
+ Assert.Equal(36f, c4.Top);
+ Assert.Equal(36f, c4.Width);
+ Assert.Equal(36f, c4.Height);
+ }
+
+ [Fact]
+ public void FillMode_SizesSingleCellToList()
+ {
+ // CellWidth defaults to 0 = "fill the list" (single-cell toolbar legacy).
+ var list = new UiItemList { Width = 36, Height = 36 };
+ list.Flush();
+ list.AddItem(new UiItemSlot());
+
+ var c = list.Cell;
+ Assert.Equal(0f, c.Left);
+ Assert.Equal(0f, c.Top);
+ Assert.Equal(36f, c.Width);
+ Assert.Equal(36f, c.Height);
+ }
+}