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

@ -320,6 +320,13 @@ public abstract class UiElement
/// </summary>
protected virtual void OnDrawOverlay(UiRenderContext ctx) { }
/// <summary>
/// When true, descendant drawing and hit-testing are clipped to this element's
/// local bounds. Scrollable listboxes use this so edge rows can remain visible
/// at arbitrary pixel offsets without painting or receiving input outside the viewport.
/// </summary>
protected virtual bool ClipsChildren => false;
/// <summary>Per-frame tick (animations, timers, caret blink).</summary>
protected virtual void OnTick(double deltaSeconds) { }
@ -378,11 +385,22 @@ public abstract class UiElement
// Children painted back-to-front (lowest ZOrder first).
if (_children.Count > 0)
{
// Avoid LINQ allocation by copying to a temp array and sorting.
var ordered = _children.ToArray();
Array.Sort(ordered, static (a, b) => a.ZOrder.CompareTo(b.ZOrder));
for (int i = 0; i < ordered.Length; i++)
ordered[i].DrawSelfAndChildren(ctx);
bool clipsChildren = ClipsChildren;
if (clipsChildren)
ctx.PushClip(0f, 0f, Width, Height);
try
{
// Avoid LINQ allocation by copying to a temp array and sorting.
var ordered = _children.ToArray();
Array.Sort(ordered, static (a, b) => a.ZOrder.CompareTo(b.ZOrder));
for (int i = 0; i < ordered.Length; i++)
ordered[i].DrawSelfAndChildren(ctx);
}
finally
{
if (clipsChildren)
ctx.PopClip();
}
}
// Foreground pass for this element (e.g. a window frame's border drawn
@ -411,10 +429,21 @@ public abstract class UiElement
OnDrawOverlay(ctx);
if (_children.Count > 0)
{
var ordered = _children.ToArray();
Array.Sort(ordered, static (a, b) => a.ZOrder.CompareTo(b.ZOrder));
for (int i = 0; i < ordered.Length; i++)
ordered[i].DrawOverlays(ctx);
bool clipsChildren = ClipsChildren;
if (clipsChildren)
ctx.PushClip(0f, 0f, Width, Height);
try
{
var ordered = _children.ToArray();
Array.Sort(ordered, static (a, b) => a.ZOrder.CompareTo(b.ZOrder));
for (int i = 0; i < ordered.Length; i++)
ordered[i].DrawOverlays(ctx);
}
finally
{
if (clipsChildren)
ctx.PopClip();
}
}
}
finally
@ -440,6 +469,9 @@ public abstract class UiElement
internal UiElement? HitTest(float localX, float localY)
{
if (!Visible || !Enabled) return null;
if (ClipsChildren
&& (localX < 0f || localX >= Width || localY < 0f || localY >= Height))
return null;
// Children first, in reverse Z-order (topmost first). ClickThrough means
// THIS element is transparent to the pointer — but its children are NOT.