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:
parent
7aba6d235c
commit
366af0c34f
2 changed files with 89 additions and 5 deletions
|
|
@ -13,6 +13,10 @@ public sealed class UiItemList : UiElement
|
||||||
{
|
{
|
||||||
private readonly List<UiItemSlot> _cells = new();
|
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)
|
public UiItemList(Func<uint, (uint tex, int w, int h)>? spriteResolve = null)
|
||||||
{
|
{
|
||||||
SpriteResolve = spriteResolve;
|
SpriteResolve = spriteResolve;
|
||||||
|
|
@ -63,6 +67,14 @@ public sealed class UiItemList : UiElement
|
||||||
/// <summary>Fixed cell height in grid mode (pairs with CellWidth).</summary>
|
/// <summary>Fixed cell height in grid mode (pairs with CellWidth).</summary>
|
||||||
public float CellHeight { get; set; }
|
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
|
/// <summary>Row-major pixel offset of cell <paramref name="index"/> in a grid of
|
||||||
/// <paramref name="columns"/> columns at the given cell pitch.</summary>
|
/// <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)
|
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<=0) sizes the single
|
/// <summary>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.</summary>
|
/// cell to the list; grid (CellWidth>0) tiles cells row-major, offset by the scroll position
|
||||||
private void LayoutCells()
|
/// with whole-row vertical clipping (cells fully outside the view are hidden).</summary>
|
||||||
|
internal void LayoutCells()
|
||||||
{
|
{
|
||||||
if (CellWidth <= 0f)
|
if (CellWidth <= 0f)
|
||||||
{
|
{
|
||||||
|
// Fill mode (the toolbar single cell): size the one cell to the list.
|
||||||
if (_cells.Count > 0)
|
if (_cells.Count > 0)
|
||||||
{
|
{
|
||||||
var c = _cells[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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int cols = Columns < 1 ? 1 : Columns;
|
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++)
|
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];
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
54
tests/AcDream.App.Tests/UI/UiItemListScrollTests.cs
Normal file
54
tests/AcDream.App.Tests/UI/UiItemListScrollTests.cs
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
using AcDream.App.UI;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace AcDream.App.Tests.UI;
|
||||||
|
|
||||||
|
public sealed class UiItemListScrollTests
|
||||||
|
{
|
||||||
|
private static UiItemList Grid(int items)
|
||||||
|
{
|
||||||
|
var list = new UiItemList { Columns = 6, CellWidth = 32, CellHeight = 32, Width = 192, Height = 96 };
|
||||||
|
list.Flush(); // drop the default single cell
|
||||||
|
for (int i = 0; i < items; i++) list.AddItem(new UiItemSlot());
|
||||||
|
list.LayoutCells(); // drive the scroll model + position cells
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RowCount_ceils_to_whole_rows()
|
||||||
|
{
|
||||||
|
Assert.Equal(0, UiItemList.RowCount(0, 6));
|
||||||
|
Assert.Equal(1, UiItemList.RowCount(1, 6));
|
||||||
|
Assert.Equal(7, UiItemList.RowCount(41, 6));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Grid_drives_scroll_model_from_content()
|
||||||
|
{
|
||||||
|
var list = Grid(30); // 5 rows × 32 = 160 content, 96 view
|
||||||
|
Assert.Equal(160, list.Scroll.ContentHeight);
|
||||||
|
Assert.Equal(96, list.Scroll.ViewHeight);
|
||||||
|
Assert.Equal(64, list.Scroll.MaxScroll);
|
||||||
|
Assert.True(list.Scroll.HasOverflow);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void At_top_first_three_rows_visible_rest_clipped()
|
||||||
|
{
|
||||||
|
var list = Grid(30);
|
||||||
|
Assert.True(list.GetItem(0)!.Visible); // row 0, top 0
|
||||||
|
Assert.True(list.GetItem(12)!.Visible); // row 2, top 64 (+32 == 96)
|
||||||
|
Assert.False(list.GetItem(18)!.Visible); // row 3, top 96 (off the bottom)
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Scrolled_one_row_shifts_window_and_clips_top_row()
|
||||||
|
{
|
||||||
|
var list = Grid(30);
|
||||||
|
list.Scroll.SetScrollY(32);
|
||||||
|
list.LayoutCells();
|
||||||
|
Assert.False(list.GetItem(0)!.Visible); // row 0 scrolled above
|
||||||
|
Assert.Equal(0f, list.GetItem(6)!.Top); // row 1 now at the view top
|
||||||
|
Assert.True(list.GetItem(18)!.Visible); // row 3 now visible
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue