feat(D.2b): UiScrollbar (Type 11) — promote the generic chat scrollbar (widget-generalization Task 2)

- git mv UiChatScrollbar.cs → UiScrollbar.cs; rename class + update doc summary to
  "Generic scrollbar. Ports retail UIElement_Scrollbar (RegisterElementClass(0xb) @
  acclient_2013_pseudo_c.txt:124137); thumb size = trackLen * ThumbRatio (min 8px); step ±1 line."
- git mv UiChatScrollbarTests.cs → UiScrollbarTests.cs; rename test class + replace
  every UiChatScrollbar reference with UiScrollbar (bodies unchanged).
- DatWidgetFactory: register Type 11 → new UiScrollbar() before the _ fallback case.
- ChatWindowController: change Scrollbar property type to UiScrollbar; replace the old
  "construct-remove-add" block with a "find factory-built UiScrollbar and bind in place"
  block (no RemoveChild/AddChild); keep `var track` assignment in scope so the Max/Min
  block's track.Left/track.Width reads still compile against UiElement?.
- AP-41 divergence register: update file:line to UiScrollbar.cs:35; narrow wording to
  "fallback only — single-tile drawn only when cap ids are unset; the chat controller
  passes all three cap ids so the 3-slice path is the active code path."
- Update inline UiChatScrollbar doc-comment references in UiScrollable.cs + UiChatView.cs.
- Full suite: 399 passed, 2 skipped (dat/tower fixture skips), 0 failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-16 17:02:49 +02:00
parent d1b13a7dbf
commit 3593d6623d
8 changed files with 49 additions and 50 deletions

View file

@ -20,9 +20,9 @@ namespace AcDream.App.UI.Layout;
/// <see cref="ElementInfo"/> tree (which contains everything) and adds the behavioral
/// widgets as children of their parent container widgets (transcript panel
/// <c>0x10000010</c> / input bar <c>0x10000013</c>) which ARE created as
/// <see cref="UiDatElement"/> nodes. The scrollbar track (<c>0x10000012</c>) and
/// channel menu (<c>0x10000014</c>) are created by the factory and are replaced
/// with their behavioral counterparts here.
/// <see cref="UiDatElement"/> nodes. The scrollbar track (<c>0x10000012</c>) is built
/// directly as a <see cref="UiScrollbar"/> by the factory (Type 11) and is bound in place
/// here. The channel menu (<c>0x10000014</c>) is still replaced with its behavioral counterpart.
/// </para>
/// </summary>
public sealed class ChatWindowController
@ -71,7 +71,7 @@ public sealed class ChatWindowController
public UiChatInput Input { get; private set; } = null!;
/// <summary>Scrollbar widget, driven by <see cref="Transcript"/>'s scroll model.</summary>
public UiChatScrollbar Scrollbar { get; private set; } = null!;
public UiScrollbar Scrollbar { get; private set; } = null!;
/// <summary>Channel-selector menu widget.</summary>
public UiChannelMenu Menu { get; private set; } = null!;
@ -110,7 +110,7 @@ public sealed class ChatWindowController
/// <param name="debugFont">Fallback debug bitmap font (used when
/// <paramref name="datFont"/> is null).</param>
/// <param name="resolve">Dat RenderSurface id → (GL tex handle, px width, px height).
/// Forwarded to <see cref="UiChatScrollbar"/> and <see cref="UiChannelMenu"/>.</param>
/// Forwarded to <see cref="UiScrollbar"/> and <see cref="UiChannelMenu"/>.</param>
public static ChatWindowController? Bind(
ElementInfo rootInfo,
ImportedLayout layout,
@ -196,33 +196,24 @@ public sealed class ChatWindowController
inputBar.AddChild(c.Input);
c.Input.OnSubmit = text => ChatCommandRouter.Submit(text, vm, busProvider(), c._activeChannel);
// ── Scrollbar — replace the imported track placeholder ────────────
// The factory created a UiDatElement for the track. Remove it and place a
// behavioral UiChatScrollbar at the same position, driving the transcript's scroll.
// ── Scrollbar — bind the factory-built Type-11 track element ────────
// The factory now builds the Type-11 track element (0x10000012) as a UiScrollbar
// directly. Find it, bind it in place — no remove/add needed.
var track = layout.FindElement(TrackId);
if (track?.Parent is { } trackParent)
if (track is UiScrollbar bar)
{
c.Scrollbar = new UiChatScrollbar
{
// Pull the bar up to the panel top so the top arrow meets the window
// border (and lines up with the max/min button at root y=0); the dat
// track sits 6px down, which left a gap after the resize-bar reclaim.
Left = track.Left,
Top = 0f,
Width = track.Width,
Height = track.Height + track.Top,
Anchors = track.Anchors,
Model = c.Transcript.Scroll,
SpriteResolve = resolve,
TrackSprite = TrackSprite,
ThumbSprite = ThumbSprite,
ThumbTopSprite = ThumbTopSprite,
ThumbBotSprite = ThumbBotSprite,
UpSprite = UpSprite,
DownSprite = DownSprite,
};
trackParent.RemoveChild(track);
trackParent.AddChild(c.Scrollbar);
float oldTop = bar.Top;
bar.Top = 0f; // pull up to the panel top (resize-bar reclaim)
bar.Height = bar.Height + oldTop;
bar.Model = c.Transcript.Scroll;
bar.SpriteResolve = resolve;
bar.TrackSprite = TrackSprite;
bar.ThumbSprite = ThumbSprite;
bar.ThumbTopSprite = ThumbTopSprite;
bar.ThumbBotSprite = ThumbBotSprite;
bar.UpSprite = UpSprite;
bar.DownSprite = DownSprite;
c.Scrollbar = bar;
}
// ── Channel menu — replace the imported menu placeholder ──────────

View file

@ -57,8 +57,9 @@ public static class DatWidgetFactory
UiElement e = info.Type switch
{
7 => BuildMeter(info, resolve, datFont), // UIElement_Meter
_ => new UiDatElement(info, resolve), // generic fallback for all other types
7 => BuildMeter(info, resolve, datFont), // UIElement_Meter
11 => new UiScrollbar(), // UIElement_Scrollbar (reg :124137)
_ => new UiDatElement(info, resolve), // generic fallback for all other types
};
// Propagate position + size (pixel-exact from the dat).

View file

@ -52,7 +52,7 @@ public sealed class UiChatView : UiElement
/// <summary>Inner text inset from the view edges, px.</summary>
public float Padding { get; set; } = 4f;
/// <summary>The scroll model — also read by the linked UiChatScrollbar.</summary>
/// <summary>The scroll model — also read by the linked UiScrollbar.</summary>
public UiScrollable Scroll { get; } = new();
/// <summary>True while the view is pinned to the newest line (auto-scrolls as content grows).</summary>

View file

@ -7,7 +7,7 @@ namespace AcDream.App.UI;
/// the scroll offset is an integer pixel value (<c>m_iScrollableY</c>) clamped to
/// [0, ContentHeight - ViewHeight]; the thumb ratio is view/content; the position
/// ratio is scroll/(content-view). Pure (no GL) so it is fully unit-tested and
/// shared by the transcript (UiChatView) and the scrollbar (UiChatScrollbar).
/// shared by the transcript (UiChatView) and the scrollbar (UiScrollbar).
/// Decomp anchors: SetScrollableXY @0x4740c0, UpdateScrollbarSize_ @0x4741a0,
/// UpdateScrollbarPosition_ @0x473f20, UIElement_Text::InqScrollDelta @0x4689b0.
/// </summary>

View file

@ -4,11 +4,9 @@ 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).
/// Generic scrollbar. Ports retail <c>UIElement_Scrollbar</c>
/// (RegisterElementClass(0xb) @ acclient_2013_pseudo_c.txt:124137);
/// thumb size = trackLen * ThumbRatio (min 8px); step ±1 line.
/// </summary>
/// <remarks>
/// Dat element ids (chat LayoutDesc 0x21000006): track 0x10000012 (X=474 Y=6 W=16 H=68),
@ -22,7 +20,7 @@ namespace AcDream.App.UI;
/// 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
public sealed class UiScrollbar : UiElement
{
/// <summary>The scroll model this bar reflects + drives (shared with the transcript).</summary>
public UiScrollable? Model { get; set; }
@ -61,7 +59,7 @@ public sealed class UiChatScrollbar : UiElement
private bool _draggingThumb;
private float _dragOffsetY;
public UiChatScrollbar() { CapturesPointerDrag = true; }
public UiScrollbar() { CapturesPointerDrag = true; }
/// <summary>
/// Computes the thumb rectangle (local y origin and height) within the track area