From 4fd4b09f3f5416d78e3ba62a1f40be2725d112ff Mon Sep 17 00:00:00 2001 From: Erik Date: Sat, 20 Jun 2026 22:34:30 +0200 Subject: [PATCH] =?UTF-8?q?feat(ui):=20D.2b-B=20=E2=80=94=20UiItemList=20N?= =?UTF-8?q?-cell=20grid=20mode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Columns + CellWidth/CellHeight + a row-major CellOffset/LayoutCells pass. CellWidth<=0 keeps the single-cell fill mode (toolbar slot sizes to the list — unchanged); CellWidth>0 tiles cells in a grid. Layout runs on AddItem and per-frame in OnDraw so a list resize reflows. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/AcDream.App/UI/UiItemList.cs | 53 +++++++++++++++---- .../UI/UiItemListGridTests.cs | 43 +++++++++++++++ 2 files changed, 86 insertions(+), 10 deletions(-) create mode 100644 tests/AcDream.App.Tests/UI/UiItemListGridTests.cs 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); + } +}