feat(ui): port favorite spell bar overflow
Import the retail arrow-only spell bar scrollbar from LayoutDesc, preserve authored end-button extents and HideDisabled behavior in the shared retained widget, and bind each favorite list to its sole horizontal pixel-scroll model. Match retail selection exposure for off-screen spells and pin the behavior with real-fixture conformance tests. Document the six-slice world-interaction completion program as the active pre-M4 work order. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
20e2998adb
commit
02c29e67c8
12 changed files with 582 additions and 45 deletions
|
|
@ -16,9 +16,8 @@ namespace AcDream.App.UI;
|
|||
/// Down button element 0x10000072 — Y=32, 16×16, Normal sprite 0x06004C6C.
|
||||
/// Track body sprite: 0x06004C5F (48px tall in the base template; stretched to H=68 in chat).
|
||||
/// Thumb is a 3-slice: top cap 0x06004C60, middle 0x06004C63, bottom cap 0x06004C66.
|
||||
/// For Task H wiring: up/down regions occupy the top and bottom ButtonH (16px) of the
|
||||
/// rendered scrollbar's height; the widget responds to those regions directly via hit
|
||||
/// comparison in OnEvent without requiring separate child elements.
|
||||
/// The widget reproduces referenced button children procedurally and uses their
|
||||
/// authored extent for drawing and hit comparison (16px in this base layout).
|
||||
/// </remarks>
|
||||
public sealed class UiScrollbar : UiElement
|
||||
{
|
||||
|
|
@ -84,16 +83,28 @@ public sealed class UiScrollbar : UiElement
|
|||
/// <summary>Down-arrow button sprite id (0x06004C6C Normal state, element 0x10000072).</summary>
|
||||
public uint DownSprite { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Authored extent of the decrement button along the scrollbar axis. Retail
|
||||
/// positions and subtracts the referenced button's real size; chat uses
|
||||
/// 16 pixels while the favorite-spell bar uses 23.
|
||||
/// </summary>
|
||||
public float DecrementButtonExtent { get; set; } = 16f;
|
||||
|
||||
/// <summary>Authored extent of the increment button along the scrollbar axis.</summary>
|
||||
public float IncrementButtonExtent { get; set; } = 16f;
|
||||
|
||||
/// <summary>
|
||||
/// Retail scrollbar property 0x79. When the linked scrollable is disabled
|
||||
/// because its content fits, UpdateLayout also hides the scrollbar.
|
||||
/// </summary>
|
||||
public bool HideWhenDisabled { get; set; }
|
||||
|
||||
/// <summary>Retail attribute 0x89 floor: minimum thumb height in pixels.</summary>
|
||||
private const float MinThumb = 8f;
|
||||
|
||||
/// <summary>Thumb cap height (native sprite height from base layout 0x2100003E).</summary>
|
||||
private const float CapH = 3f;
|
||||
|
||||
/// <summary>Up/down button height in pixels. Matches element height 16px from
|
||||
/// the up/down button children in base layout 0x2100003E.</summary>
|
||||
private const float ButtonH = 16f;
|
||||
|
||||
private bool _draggingThumb;
|
||||
private float _dragOffsetY;
|
||||
private float _dragOffsetX;
|
||||
|
|
@ -104,6 +115,18 @@ public sealed class UiScrollbar : UiElement
|
|||
/// children are reproduced procedurally, so the importer must not build them.</summary>
|
||||
public override bool ConsumesDatChildren => true;
|
||||
|
||||
/// <summary>
|
||||
/// The model owns disabled state just as retail UIElement_Scrollable does:
|
||||
/// no overflow means the linked scrollbar is disabled.
|
||||
/// </summary>
|
||||
internal bool IsModelDisabled
|
||||
=> ScalarChanged is null && Model is { HasOverflow: false };
|
||||
|
||||
internal bool IsPresentationVisible => !HideWhenDisabled || !IsModelDisabled;
|
||||
|
||||
protected override bool OnHitTest(float localX, float localY)
|
||||
=> !IsModelDisabled && base.OnHitTest(localX, localY);
|
||||
|
||||
/// <summary>
|
||||
/// Computes the thumb rectangle (local y origin and height) within the track area
|
||||
/// between the two end buttons. Ports retail <c>UIElement_Scrollbar::UpdateLayout
|
||||
|
|
@ -138,6 +161,7 @@ public sealed class UiScrollbar : UiElement
|
|||
|
||||
protected override void OnDraw(UiRenderContext ctx)
|
||||
{
|
||||
if (!IsPresentationVisible) return;
|
||||
if (SpriteResolve is not { } resolve) return;
|
||||
if (Horizontal)
|
||||
{
|
||||
|
|
@ -173,11 +197,13 @@ public sealed class UiScrollbar : UiElement
|
|||
// sprite (~16×32) repeats to fill the element height instead of stretch-distorting.
|
||||
DrawTiled(ctx, resolve, TrackSprite, 0f, 0f, Width, Height);
|
||||
|
||||
// Decrement/up button — top ButtonH rows (inherited element 0x10000071).
|
||||
DrawSprite(ctx, resolve, UpSprite, 0f, 0f, Width, ButtonH);
|
||||
float decrementExtent = AxisExtent(DecrementButtonExtent, Height);
|
||||
float incrementExtent = AxisExtent(IncrementButtonExtent, Height);
|
||||
|
||||
// Increment/down button — bottom ButtonH rows (inherited element 0x10000072).
|
||||
DrawSprite(ctx, resolve, DownSprite, 0f, Height - ButtonH, Width, ButtonH);
|
||||
// Decrement/up and increment/down use their authored button heights.
|
||||
DrawSprite(ctx, resolve, UpSprite, 0f, 0f, Width, decrementExtent);
|
||||
DrawSprite(ctx, resolve, DownSprite,
|
||||
0f, Height - incrementExtent, Width, incrementExtent);
|
||||
|
||||
// Thumb — only when content overflows the view. Retail 3-slice: top cap +
|
||||
// tiled middle + bottom cap (base layout 0x2100003E thumb sub-elements
|
||||
|
|
@ -185,8 +211,8 @@ public sealed class UiScrollbar : UiElement
|
|||
// or the thumb is too short to hold both caps.
|
||||
if (m.HasOverflow)
|
||||
{
|
||||
float trackTop = ButtonH;
|
||||
float trackLen = Height - 2f * ButtonH;
|
||||
float trackTop = decrementExtent;
|
||||
float trackLen = MathF.Max(0f, Height - decrementExtent - incrementExtent);
|
||||
var (ty, th) = ThumbRect(m, trackTop, trackLen);
|
||||
if (ThumbTopSprite != 0 && ThumbBotSprite != 0 && th >= 2f * CapH)
|
||||
{
|
||||
|
|
@ -206,13 +232,16 @@ public sealed class UiScrollbar : UiElement
|
|||
Func<uint, (uint tex, int w, int h)> resolve,
|
||||
UiScrollable model)
|
||||
{
|
||||
float decrementExtent = AxisExtent(DecrementButtonExtent, Width);
|
||||
float incrementExtent = AxisExtent(IncrementButtonExtent, Width);
|
||||
DrawTiled(ctx, resolve, TrackSprite, 0f, 0f, Width, Height);
|
||||
DrawSprite(ctx, resolve, UpSprite, 0f, 0f, ButtonH, Height);
|
||||
DrawSprite(ctx, resolve, DownSprite, Width - ButtonH, 0f, ButtonH, Height);
|
||||
DrawSprite(ctx, resolve, UpSprite, 0f, 0f, decrementExtent, Height);
|
||||
DrawSprite(ctx, resolve, DownSprite,
|
||||
Width - incrementExtent, 0f, incrementExtent, Height);
|
||||
if (!model.HasOverflow) return;
|
||||
|
||||
float trackLeft = ButtonH;
|
||||
float trackLength = MathF.Max(0f, Width - 2f * ButtonH);
|
||||
float trackLeft = decrementExtent;
|
||||
float trackLength = MathF.Max(0f, Width - decrementExtent - incrementExtent);
|
||||
var (tx, tw) = ThumbRect(model, trackLeft, trackLength);
|
||||
if (ThumbTopSprite != 0 && ThumbBotSprite != 0 && tw >= 2f * CapH)
|
||||
{
|
||||
|
|
@ -290,6 +319,12 @@ public sealed class UiScrollbar : UiElement
|
|||
|
||||
public override bool OnEvent(in UiEvent e)
|
||||
{
|
||||
if (IsModelDisabled)
|
||||
{
|
||||
_draggingThumb = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Horizontal && ScalarChanged is not null)
|
||||
return OnScalarEvent(e);
|
||||
|
||||
|
|
@ -304,16 +339,18 @@ public sealed class UiScrollbar : UiElement
|
|||
{
|
||||
// e.Data1 = local X, e.Data2 = local Y (int pixel coords, see UiRoot hit dispatch).
|
||||
float ly = e.Data2;
|
||||
float decrementExtent = AxisExtent(DecrementButtonExtent, Height);
|
||||
float incrementExtent = AxisExtent(IncrementButtonExtent, Height);
|
||||
|
||||
// Up-button region: top ButtonH rows.
|
||||
if (ly <= ButtonH) { m.ScrollByLines(-1); return true; }
|
||||
// Up-button region: authored top rows.
|
||||
if (ly < decrementExtent) { m.ScrollByLines(-1); return true; }
|
||||
|
||||
// Down-button region: bottom ButtonH rows.
|
||||
if (ly >= Height - ButtonH) { m.ScrollByLines(1); return true; }
|
||||
// Down-button region: authored bottom rows.
|
||||
if (ly >= Height - incrementExtent) { m.ScrollByLines(1); return true; }
|
||||
|
||||
// Track interior: start a thumb drag or page-scroll.
|
||||
float trackTop = ButtonH;
|
||||
float trackLen = Height - 2f * ButtonH;
|
||||
float trackTop = decrementExtent;
|
||||
float trackLen = MathF.Max(0f, Height - decrementExtent - incrementExtent);
|
||||
var (ty, th) = ThumbRect(m, trackTop, trackLen);
|
||||
|
||||
if (ly >= ty && ly <= ty + th)
|
||||
|
|
@ -334,8 +371,12 @@ public sealed class UiScrollbar : UiElement
|
|||
{
|
||||
// Map current local Y (minus drag offset from thumb top) back to a
|
||||
// position ratio across the available travel distance.
|
||||
float trackTop = ButtonH;
|
||||
float trackLen = Height - 2f * ButtonH;
|
||||
float trackTop = AxisExtent(DecrementButtonExtent, Height);
|
||||
float trackLen = MathF.Max(
|
||||
0f,
|
||||
Height
|
||||
- AxisExtent(DecrementButtonExtent, Height)
|
||||
- AxisExtent(IncrementButtonExtent, Height));
|
||||
float thumbH = MathF.Max(MinThumb, trackLen * m.ThumbRatio);
|
||||
float travel = MathF.Max(1f, trackLen - thumbH);
|
||||
float newRatio = ((float)e.Data2 - _dragOffsetY - trackTop) / travel;
|
||||
|
|
@ -359,11 +400,13 @@ public sealed class UiScrollbar : UiElement
|
|||
case UiEventType.MouseDown:
|
||||
{
|
||||
float x = e.Data1;
|
||||
if (x <= ButtonH) { m.ScrollByLines(-1); return true; }
|
||||
if (x >= Width - ButtonH) { m.ScrollByLines(1); return true; }
|
||||
float decrementExtent = AxisExtent(DecrementButtonExtent, Width);
|
||||
float incrementExtent = AxisExtent(IncrementButtonExtent, Width);
|
||||
if (x < decrementExtent) { m.ScrollByLines(-1); return true; }
|
||||
if (x >= Width - incrementExtent) { m.ScrollByLines(1); return true; }
|
||||
|
||||
float trackLeft = ButtonH;
|
||||
float trackLength = MathF.Max(0f, Width - 2f * ButtonH);
|
||||
float trackLeft = decrementExtent;
|
||||
float trackLength = MathF.Max(0f, Width - decrementExtent - incrementExtent);
|
||||
var (tx, tw) = ThumbRect(m, trackLeft, trackLength);
|
||||
if (x >= tx && x <= tx + tw)
|
||||
{
|
||||
|
|
@ -379,8 +422,12 @@ public sealed class UiScrollbar : UiElement
|
|||
|
||||
case UiEventType.MouseMove when _draggingThumb:
|
||||
{
|
||||
float trackLeft = ButtonH;
|
||||
float trackLength = MathF.Max(0f, Width - 2f * ButtonH);
|
||||
float trackLeft = AxisExtent(DecrementButtonExtent, Width);
|
||||
float trackLength = MathF.Max(
|
||||
0f,
|
||||
Width
|
||||
- AxisExtent(DecrementButtonExtent, Width)
|
||||
- AxisExtent(IncrementButtonExtent, Width));
|
||||
float thumbWidth = MathF.Max(MinThumb, trackLength * m.ThumbRatio);
|
||||
float travel = MathF.Max(1f, trackLength - thumbWidth);
|
||||
float ratio = ((float)e.Data1 - _dragOffsetX - trackLeft) / travel;
|
||||
|
|
@ -449,4 +496,7 @@ public sealed class UiScrollbar : UiElement
|
|||
SetScalarPosition(position);
|
||||
ScalarChanged?.Invoke(ScalarPosition);
|
||||
}
|
||||
|
||||
private static float AxisExtent(float authoredExtent, float axisLength)
|
||||
=> Math.Clamp(authoredExtent, 0f, MathF.Max(0f, axisLength));
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue