fix(ui): complete retail inventory scroll polish

Preserve pixel scroll offsets across inventory rebuilds, crop partially visible rows with nested geometry/UV clips, and replace the obsolete 560px resize ceiling with available screen height. Keep retail's row-sized wheel step while allowing continuous scrollbar thumb positions.

Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-11 10:56:33 +02:00
parent ea72c395c9
commit 15aa3b9aff
15 changed files with 379 additions and 34 deletions

View file

@ -22,6 +22,7 @@ public enum UiItemListFlow
public sealed class UiItemList : UiElement
{
private readonly List<UiItemSlot> _cells = new();
private int _layoutDeferralDepth;
/// <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>
@ -89,7 +90,19 @@ public sealed class UiItemList : UiElement
cell.Anchors = AnchorEdges.None;
_cells.Add(cell);
AddChild(cell);
LayoutCells();
if (_layoutDeferralDepth == 0)
LayoutCells();
}
/// <summary>
/// Batch a procedural list rebuild without repeatedly shrinking the scroll
/// extents as cells are re-added. The retained pixel offset is clamped once
/// against the completed list when the outermost scope ends.
/// </summary>
public IDisposable DeferLayout()
{
_layoutDeferralDepth++;
return new LayoutDeferral(this);
}
/// <summary>Grid columns. 1 = single column. Ignored in fill mode.</summary>
@ -143,7 +156,7 @@ 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 by <see cref="Flow"/>, offset by the scroll position
/// with whole-row vertical clipping (cells fully outside the view are hidden).</summary>
/// with pixel viewport clipping (partially visible edge rows are cropped).</summary>
internal void LayoutCells()
{
if (CellWidth <= 0f)
@ -175,12 +188,14 @@ public sealed class UiItemList : UiElement
float top = baseY - scrollY;
var cell = _cells[i];
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;
// Retail listboxes retain intersecting edge rows at arbitrary pixel offsets.
// The UiElement child-clip traversal crops their visible portion.
cell.Visible = top < Height && top + CellHeight > 0f;
}
}
protected override bool ClipsChildren => CellWidth > 0f;
public void Flush()
{
foreach (var c in _cells) RemoveChild(c);
@ -204,4 +219,27 @@ public sealed class UiItemList : UiElement
// fill mode keeps the single toolbar cell sized to the list; grid mode tiles cells.
LayoutCells();
}
private void EndLayoutDeferral()
{
if (_layoutDeferralDepth <= 0)
return;
_layoutDeferralDepth--;
if (_layoutDeferralDepth == 0)
LayoutCells();
}
private sealed class LayoutDeferral : IDisposable
{
private UiItemList? _owner;
public LayoutDeferral(UiItemList owner) => _owner = owner;
public void Dispose()
{
UiItemList? owner = _owner;
_owner = null;
owner?.EndLayoutDeferral();
}
}
}