feat(D.2b): UiChatScrollbar — track/thumb/buttons driving UiScrollable
Implements the right-side chat scrollbar widget. Ports retail UIElement_Scrollbar::UpdateLayout @0x4710d0 (thumb sizing + placement) and HandleButtonClick @0x470e90 (step ±1 line, page on track click). Dat element ids sourced from chat LayoutDesc 0x21000006 (base layout 0x2100003E): up-button sprite 0x06004C69, down-button 0x06004C6C, track 0x06004C5F, thumb middle 0x06004C63. Up/down buttons occupy the top and bottom ButtonH (16px) regions of the widget height, matching element positions Y=0 and Y=32 in the base scrollbar template. Adds 6 pure ThumbRect tests (no GL): sizing, clamping to MinThumb, position at start/mid/end, no-overflow full-fill. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0eaef67b9d
commit
2940b4e3b2
2 changed files with 245 additions and 0 deletions
164
src/AcDream.App/UI/UiChatScrollbar.cs
Normal file
164
src/AcDream.App/UI/UiChatScrollbar.cs
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
using System;
|
||||
using System.Numerics;
|
||||
|
||||
namespace AcDream.App.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Right-side chat scrollbar: a track sprite, a draggable thumb sized to the
|
||||
/// content/view ratio, and up/down step buttons. Drives a linked
|
||||
/// <see cref="UiScrollable"/>. Ports retail <c>UIElement_Scrollbar::UpdateLayout
|
||||
/// @0x4710d0</c> (thumb size = trackLen * ThumbRatio, min 8px; thumb pos from
|
||||
/// PositionRatio) and <c>HandleButtonClick @0x470e90</c> (step ±1 line).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Dat element ids (chat LayoutDesc 0x21000006): track 0x10000012 (X=474 Y=6 W=16 H=68),
|
||||
/// thumb 0x1000048C. The track is instanced from base layout 0x2100003E which contains
|
||||
/// the full scrollbar widget with distinct up/down button children:
|
||||
/// Up button element 0x10000071 — Y=0, 16×16, Normal sprite 0x06004C69.
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
public sealed class UiChatScrollbar : UiElement
|
||||
{
|
||||
/// <summary>The scroll model this bar reflects + drives (shared with the transcript).</summary>
|
||||
public UiScrollable? Model { get; set; }
|
||||
|
||||
/// <summary>RenderSurface id → (GL tex, w, h). 0 id = skip.</summary>
|
||||
public Func<uint, (uint tex, int w, int h)>? SpriteResolve { get; set; }
|
||||
|
||||
/// <summary>Track background sprite id (0x06004C5F from layout 0x2100003E element 0x10000455).</summary>
|
||||
public uint TrackSprite { get; set; }
|
||||
|
||||
/// <summary>Thumb sprite id (3-slice middle tile: 0x06004C63; the widget draws
|
||||
/// a single stretched sprite for simplicity — Task H can upgrade to 3-slice).</summary>
|
||||
public uint ThumbSprite { get; set; }
|
||||
|
||||
/// <summary>Up-arrow button sprite id (0x06004C69 Normal state, element 0x10000071).</summary>
|
||||
public uint UpSprite { get; set; }
|
||||
|
||||
/// <summary>Down-arrow button sprite id (0x06004C6C Normal state, element 0x10000072).</summary>
|
||||
public uint DownSprite { get; set; }
|
||||
|
||||
/// <summary>Retail attribute 0x89 floor: minimum thumb height in pixels.</summary>
|
||||
private const float MinThumb = 8f;
|
||||
|
||||
/// <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;
|
||||
|
||||
public UiChatScrollbar() { CapturesPointerDrag = true; }
|
||||
|
||||
/// <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
|
||||
/// @0x4710d0</c>: thumb height = max(MinThumb, trackLen * ThumbRatio); thumb top
|
||||
/// offset = trackTop + (trackLen - thumbH) * PositionRatio.
|
||||
/// </summary>
|
||||
/// <param name="m">The scroll model.</param>
|
||||
/// <param name="trackTop">Y of the top of the usable track area (below up-button).</param>
|
||||
/// <param name="trackLen">Pixel length of the usable track area (between up and down buttons).</param>
|
||||
/// <returns>Local Y of the thumb's top edge, and its pixel height.</returns>
|
||||
public static (float y, float h) ThumbRect(UiScrollable m, float trackTop, float trackLen)
|
||||
{
|
||||
float h = MathF.Max(MinThumb, trackLen * m.ThumbRatio);
|
||||
float travel = trackLen - h;
|
||||
float y = trackTop + travel * m.PositionRatio;
|
||||
return (y, h);
|
||||
}
|
||||
|
||||
protected override void OnDraw(UiRenderContext ctx)
|
||||
{
|
||||
if (Model is not { } m || SpriteResolve is not { } resolve) return;
|
||||
|
||||
// Track background, full element bounds.
|
||||
DrawSprite(ctx, resolve, TrackSprite, 0f, 0f, Width, Height);
|
||||
|
||||
// Up button — top ButtonH rows.
|
||||
DrawSprite(ctx, resolve, UpSprite, 0f, 0f, Width, ButtonH);
|
||||
|
||||
// Down button — bottom ButtonH rows.
|
||||
DrawSprite(ctx, resolve, DownSprite, 0f, Height - ButtonH, Width, ButtonH);
|
||||
|
||||
// Thumb — only when content overflows the view.
|
||||
if (m.HasOverflow)
|
||||
{
|
||||
float trackTop = ButtonH;
|
||||
float trackLen = Height - 2f * ButtonH;
|
||||
var (ty, th) = ThumbRect(m, trackTop, trackLen);
|
||||
DrawSprite(ctx, resolve, ThumbSprite, 0f, ty, Width, th);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawSprite(UiRenderContext ctx, Func<uint, (uint tex, int w, int h)> resolve,
|
||||
uint id, float x, float y, float w, float h)
|
||||
{
|
||||
if (id == 0) return;
|
||||
var (tex, _, _) = resolve(id);
|
||||
if (tex == 0) return;
|
||||
ctx.DrawSprite(tex, x, y, w, h, 0f, 0f, 1f, 1f, Vector4.One);
|
||||
}
|
||||
|
||||
public override bool OnEvent(in UiEvent e)
|
||||
{
|
||||
if (Model is not { } m) return false;
|
||||
|
||||
switch (e.Type)
|
||||
{
|
||||
case UiEventType.MouseDown:
|
||||
{
|
||||
// e.Data1 = local X, e.Data2 = local Y (int pixel coords, see UiRoot hit dispatch).
|
||||
float ly = e.Data2;
|
||||
|
||||
// Up-button region: top ButtonH rows.
|
||||
if (ly <= ButtonH) { m.ScrollByLines(-1); return true; }
|
||||
|
||||
// Down-button region: bottom ButtonH rows.
|
||||
if (ly >= Height - ButtonH) { m.ScrollByLines(1); return true; }
|
||||
|
||||
// Track interior: start a thumb drag or page-scroll.
|
||||
float trackTop = ButtonH;
|
||||
float trackLen = Height - 2f * ButtonH;
|
||||
var (ty, th) = ThumbRect(m, trackTop, trackLen);
|
||||
|
||||
if (ly >= ty && ly <= ty + th)
|
||||
{
|
||||
// Clicked inside the thumb — begin drag with offset from thumb top.
|
||||
_draggingThumb = true;
|
||||
_dragOffsetY = ly - ty;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Clicked above or below thumb — page scroll (HandleButtonClick page case).
|
||||
m.ScrollByPage(ly < ty ? -1 : 1);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
case UiEventType.MouseMove when _draggingThumb:
|
||||
{
|
||||
// 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 thumbH = MathF.Max(MinThumb, trackLen * m.ThumbRatio);
|
||||
float travel = MathF.Max(1f, trackLen - thumbH);
|
||||
float newRatio = ((float)e.Data2 - _dragOffsetY - trackTop) / travel;
|
||||
m.SetPositionRatio(newRatio);
|
||||
return true;
|
||||
}
|
||||
|
||||
case UiEventType.MouseUp:
|
||||
_draggingThumb = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue