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:
parent
ea72c395c9
commit
15aa3b9aff
15 changed files with 379 additions and 34 deletions
|
|
@ -3,6 +3,51 @@ using AcDream.App.Rendering;
|
|||
|
||||
namespace AcDream.App.UI;
|
||||
|
||||
internal readonly record struct UiClipRect(float Left, float Top, float Right, float Bottom)
|
||||
{
|
||||
public bool IsEmpty => Right <= Left || Bottom <= Top;
|
||||
|
||||
public static UiClipRect Intersect(UiClipRect a, UiClipRect b)
|
||||
=> new(
|
||||
MathF.Max(a.Left, b.Left),
|
||||
MathF.Max(a.Top, b.Top),
|
||||
MathF.Min(a.Right, b.Right),
|
||||
MathF.Min(a.Bottom, b.Bottom));
|
||||
|
||||
public static bool TryClipSprite(
|
||||
UiClipRect clip,
|
||||
ref float x, ref float y, ref float w, ref float h,
|
||||
ref float u0, ref float v0, ref float u1, ref float v1)
|
||||
{
|
||||
if (clip.IsEmpty || w <= 0f || h <= 0f)
|
||||
return false;
|
||||
|
||||
float left = MathF.Max(x, clip.Left);
|
||||
float top = MathF.Max(y, clip.Top);
|
||||
float right = MathF.Min(x + w, clip.Right);
|
||||
float bottom = MathF.Min(y + h, clip.Bottom);
|
||||
if (right <= left || bottom <= top)
|
||||
return false;
|
||||
|
||||
float oldX = x, oldY = y, oldW = w, oldH = h;
|
||||
float oldU0 = u0, oldV0 = v0, oldU1 = u1, oldV1 = v1;
|
||||
float tx0 = (left - oldX) / oldW;
|
||||
float tx1 = (right - oldX) / oldW;
|
||||
float ty0 = (top - oldY) / oldH;
|
||||
float ty1 = (bottom - oldY) / oldH;
|
||||
|
||||
x = left;
|
||||
y = top;
|
||||
w = right - left;
|
||||
h = bottom - top;
|
||||
u0 = oldU0 + (oldU1 - oldU0) * tx0;
|
||||
u1 = oldU0 + (oldU1 - oldU0) * tx1;
|
||||
v0 = oldV0 + (oldV1 - oldV0) * ty0;
|
||||
v1 = oldV0 + (oldV1 - oldV0) * ty1;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Per-frame drawing context passed through the <see cref="UiElement"/>
|
||||
/// tree. Wraps a <see cref="TextRenderer"/> (our 2D sprite batcher) and a
|
||||
|
|
@ -21,6 +66,8 @@ public sealed class UiRenderContext
|
|||
// Transform stack — simple 2D translate (no rotation/scale for UI).
|
||||
private readonly System.Collections.Generic.List<Vector2> _stack = new();
|
||||
private Vector2 _current;
|
||||
private readonly System.Collections.Generic.List<UiClipRect?> _clipStack = new();
|
||||
private UiClipRect? _clip;
|
||||
|
||||
// Alpha (opacity) stack — a window pushes its Opacity so its background/sprite
|
||||
// draws fade (retail's translucent-chat effect). Text draws bypass this (they go
|
||||
|
|
@ -68,6 +115,27 @@ public sealed class UiRenderContext
|
|||
|
||||
public Vector2 CurrentOrigin => _current;
|
||||
|
||||
/// <summary>Intersect descendant drawing with a local-space viewport.</summary>
|
||||
public void PushClip(float x, float y, float w, float h)
|
||||
{
|
||||
_clipStack.Add(_clip);
|
||||
var next = new UiClipRect(
|
||||
_current.X + x,
|
||||
_current.Y + y,
|
||||
_current.X + x + MathF.Max(0f, w),
|
||||
_current.Y + y + MathF.Max(0f, h));
|
||||
_clip = _clip is { } current
|
||||
? UiClipRect.Intersect(current, next)
|
||||
: next;
|
||||
}
|
||||
|
||||
public void PopClip()
|
||||
{
|
||||
if (_clipStack.Count == 0) return;
|
||||
_clip = _clipStack[^1];
|
||||
_clipStack.RemoveAt(_clipStack.Count - 1);
|
||||
}
|
||||
|
||||
/// <summary>Route subsequent draws to the overlay layer (flushed on top of the whole
|
||||
/// UI). Used by the root for the popup/overlay traversal. Pair with <see cref="EndOverlayLayer"/>.</summary>
|
||||
public void BeginOverlayLayer() => TextRenderer.OverlayMode = true;
|
||||
|
|
@ -76,22 +144,64 @@ public sealed class UiRenderContext
|
|||
// ── Pass-through draw helpers (add current translate) ──────────────
|
||||
|
||||
public void DrawRect(float x, float y, float w, float h, Vector4 color)
|
||||
=> TextRenderer.DrawRect(_current.X + x, _current.Y + y, w, h, ApplyAlpha(color));
|
||||
{
|
||||
x += _current.X;
|
||||
y += _current.Y;
|
||||
if (!ClipRect(ref x, ref y, ref w, ref h)) return;
|
||||
TextRenderer.DrawRect(x, y, w, h, ApplyAlpha(color));
|
||||
}
|
||||
|
||||
/// <summary>Solid-colour fill drawn in the SPRITE bucket (painter order with text), for
|
||||
/// a panel BACKGROUND that text draws on top of. <see cref="DrawRect"/> composites after
|
||||
/// all sprites and would cover the text — use this for backgrounds, that for foreground
|
||||
/// fills (carets, vital bars).</summary>
|
||||
public void DrawFill(float x, float y, float w, float h, Vector4 color)
|
||||
=> TextRenderer.DrawFill(_current.X + x, _current.Y + y, w, h, ApplyAlpha(color));
|
||||
{
|
||||
x += _current.X;
|
||||
y += _current.Y;
|
||||
if (!ClipRect(ref x, ref y, ref w, ref h)) return;
|
||||
TextRenderer.DrawFill(x, y, w, h, ApplyAlpha(color));
|
||||
}
|
||||
|
||||
public void DrawRectOutline(float x, float y, float w, float h, Vector4 color, float thickness = 1f)
|
||||
=> TextRenderer.DrawRectOutline(_current.X + x, _current.Y + y, w, h, ApplyAlpha(color), thickness);
|
||||
{
|
||||
if (thickness <= 0f || w <= 0f || h <= 0f) return;
|
||||
float t = MathF.Min(thickness, MathF.Min(w, h) * 0.5f);
|
||||
DrawRect(x, y, w, t, color);
|
||||
DrawRect(x, y + h - t, w, t, color);
|
||||
DrawRect(x, y + t, t, h - 2f * t, color);
|
||||
DrawRect(x + w - t, y + t, t, h - 2f * t, color);
|
||||
}
|
||||
|
||||
public void DrawSprite(uint texture, float x, float y, float w, float h,
|
||||
float u0, float v0, float u1, float v1, Vector4 tint)
|
||||
=> TextRenderer.DrawSprite(texture,
|
||||
_current.X + x, _current.Y + y, w, h, u0, v0, u1, v1, ApplyAlpha(tint));
|
||||
{
|
||||
x += _current.X;
|
||||
y += _current.Y;
|
||||
DrawSpriteAbsolute(texture, x, y, w, h, u0, v0, u1, v1, tint, applyAlpha: true);
|
||||
}
|
||||
|
||||
private void DrawSpriteAbsolute(
|
||||
uint texture, float x, float y, float w, float h,
|
||||
float u0, float v0, float u1, float v1, Vector4 tint, bool applyAlpha)
|
||||
{
|
||||
if (_clip is { } clip
|
||||
&& !UiClipRect.TryClipSprite(
|
||||
clip, ref x, ref y, ref w, ref h, ref u0, ref v0, ref u1, ref v1))
|
||||
return;
|
||||
TextRenderer.DrawSprite(
|
||||
texture, x, y, w, h, u0, v0, u1, v1,
|
||||
applyAlpha ? ApplyAlpha(tint) : tint);
|
||||
}
|
||||
|
||||
private bool ClipRect(ref float x, ref float y, ref float w, ref float h)
|
||||
{
|
||||
if (_clip is not { } clip)
|
||||
return w > 0f && h > 0f;
|
||||
float u0 = 0f, v0 = 0f, u1 = 1f, v1 = 1f;
|
||||
return UiClipRect.TryClipSprite(
|
||||
clip, ref x, ref y, ref w, ref h, ref u0, ref v0, ref u1, ref v1);
|
||||
}
|
||||
|
||||
/// <summary>Multiply the current window opacity into a draw color's alpha.</summary>
|
||||
private Vector4 ApplyAlpha(Vector4 c) => _alpha >= 1f ? c : new Vector4(c.X, c.Y, c.Z, c.W * _alpha);
|
||||
|
|
@ -172,14 +282,18 @@ public sealed class UiRenderContext
|
|||
var (bu0, bv0, bu1, bv1) = AtlasUv(
|
||||
g.OffsetX, g.OffsetY, g.Width, g.Height,
|
||||
font.BackgroundWidth, font.BackgroundHeight);
|
||||
TextRenderer.DrawSprite(font.BackgroundTexture, gx, gy, gw, gh, bu0, bv0, bu1, bv1, outlineTint);
|
||||
DrawSpriteAbsolute(
|
||||
font.BackgroundTexture, gx, gy, gw, gh,
|
||||
bu0, bv0, bu1, bv1, outlineTint, applyAlpha: false);
|
||||
}
|
||||
|
||||
// Foreground (fill) atlas pass, tinted with the requested color.
|
||||
var (fu0, fv0, fu1, fv1) = AtlasUv(
|
||||
g.OffsetX, g.OffsetY, g.Width, g.Height,
|
||||
font.ForegroundWidth, font.ForegroundHeight);
|
||||
TextRenderer.DrawSprite(font.ForegroundTexture, gx, gy, gw, gh, fu0, fv0, fu1, fv1, color);
|
||||
DrawSpriteAbsolute(
|
||||
font.ForegroundTexture, gx, gy, gw, gh,
|
||||
fu0, fv0, fu1, fv1, color, applyAlpha: false);
|
||||
}
|
||||
|
||||
pen += UiDatFont.GlyphAdvance(g);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue