feat(ui): D.2b inventory finish — UiItemList clip+scroll via UiScrollable

Grid mode now drives a shared UiScrollable from its content + clips whole rows
to the panel (cells offset by -ScrollY, Visible toggled by whole-row clip,
mirroring UiText). Fill mode (toolbar single cell) unchanged. LayoutCells made
internal for tests; RowCount helper added.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-21 20:25:11 +02:00
parent 7aba6d235c
commit 366af0c34f
2 changed files with 89 additions and 5 deletions

View file

@ -13,6 +13,10 @@ 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;
@ -63,6 +67,14 @@ public sealed class UiItemList : UiElement
/// <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)
@ -72,24 +84,42 @@ public sealed class UiItemList : UiElement
}
/// <summary>Position every cell per the current mode: fill (CellWidth&lt;=0) sizes the single
/// cell to the list; grid (CellWidth&gt;0) tiles cells row-major at the cell pitch.</summary>
private void LayoutCells()
/// cell to the list; grid (CellWidth&gt;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.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, y) = CellOffset(i, cols, CellWidth, CellHeight);
var (x, baseY) = CellOffset(i, cols, CellWidth, CellHeight);
float top = baseY - scrollY;
var cell = _cells[i];
cell.Left = x; cell.Top = y; cell.Width = CellWidth; cell.Height = CellHeight;
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;
}
}