acdream/src/AcDream.App/UI/RetailScrollbarChrome.cs
Erik 8fd3d1a9f0
All checks were successful
CI / linux-portable (push) Successful in 3m32s
CI / windows-gate (push) Successful in 6m15s
CI / release (push) Successful in 2m16s
fix(ui): retail scrollbar parity — button seating, full-track thumb, hover/pressed states
Owner report (2026-08-24): our scrollbar arrows pointed the wrong way,
the thumb vanished when there was nothing to scroll, and neither the
thumb nor the arrow buttons reacted to hover/press.

All three are one retail mechanism we had not ported:

1. Seating: UIElement_Scrollbar::UpdateScrollingArea @0x00470AA0 moves
   the INCREMENT designee (attribute 0x77) to the top/left corner and
   the DECREMENT designee (0x78) to the bottom/right, ignoring authored
   positions. The vertical base skin (0x10000455 in layout 0x2100003E)
   authors the DOWN-arrow decrement at Y=0 and the UP-arrow increment
   at Y=32 (live-DAT probed; sprite art visually verified from decoded
   PNGs), so our authored-Y ordering drew both arrows upside down.
   DatWidgetFactory now seats by designation; the hand-wired sites
   (CharacterStatController, ExternalContainerController, the
   Config/Vendor menu chrome) share the new RetailScrollbarChrome
   catalog instead of local constants.

2. Full-track thumb: UpdateLayout @0x004710d0 sizes the thumb from
   proportion attribute 0x88, which DEFAULTS to 1.0 — a content-fits
   bar shows a thumb filling the whole track; disabled only removes
   input and the page regions. Our draw skipped the thumb entirely on
   !HasOverflow.

3. States: every arrow button and thumb slice authors Normal (red gem /
   dark navy), Normal_rollover (amber gem / bright blue) and
   Normal_pressed (gold highlight / dark) media. The widget now tracks
   thumb hover and selects rollover media on hover and pressed media
   while dragging; the factory extracts the thumb-state media for both
   the 3-slice and single-sprite thumb shapes.

ScrollbarSkinLiveDatTests pins the designations and state media against
the installed DAT so a revision or importer regression fails loudly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 19:20:43 +02:00

108 lines
5.1 KiB
C#

namespace AcDream.App.UI;
/// <summary>
/// The base scrollbar skin from LayoutDesc <c>0x2100003E</c>, seated the way
/// retail seats it at runtime. <c>UIElement_Scrollbar::UpdateScrollingArea
/// @0x00470AA0</c> ignores the buttons' authored positions: it moves the
/// INCREMENT button (attribute <c>0x77</c>) to the top/left corner
/// (<c>MoveTo(0,0)</c>) and the DECREMENT button (attribute <c>0x78</c>) to
/// the bottom/right corner. The vertical skin (element <c>0x10000455</c>)
/// designates <c>0x77=0x10000072</c> (the UP-arrow art, authored at Y=32)
/// and <c>0x78=0x10000071</c> (the DOWN-arrow art, authored at Y=0) — so
/// seating by authored Y renders both arrows upside down (the 2026-08-24
/// owner report). Each button and thumb slice authors three states:
/// Normal (red center gem / dark navy thumb), Normal_rollover (amber gem /
/// bright blue thumb), Normal_pressed (gold highlight gem / dark thumb) —
/// all live-DAT probed 2026-08-24.
/// </summary>
internal static class RetailScrollbarChrome
{
// ── Vertical skin (element 0x10000455, 16 px) ────────────────────────
internal const uint Track = 0x06004C5Fu;
/// <summary>Top button = the INCREMENT designee 0x10000072 (up arrow).</summary>
internal const uint UpNormal = 0x06004C6Cu;
internal const uint UpRollover = 0x06004C6Du;
internal const uint UpPressed = 0x06004C6Eu;
/// <summary>Bottom button = the DECREMENT designee 0x10000071 (down arrow).</summary>
internal const uint DownNormal = 0x06004C69u;
internal const uint DownRollover = 0x06004C6Au;
internal const uint DownPressed = 0x06004C6Bu;
internal const uint ThumbTopNormal = 0x06004C60u;
internal const uint ThumbTopRollover = 0x06004C61u;
internal const uint ThumbTopPressed = 0x06004C62u;
internal const uint ThumbMidNormal = 0x06004C63u;
internal const uint ThumbMidRollover = 0x06004C64u;
internal const uint ThumbMidPressed = 0x06004C65u;
internal const uint ThumbBotNormal = 0x06004C66u;
internal const uint ThumbBotRollover = 0x06004C67u;
internal const uint ThumbBotPressed = 0x06004C68u;
// ── Horizontal skin (element 0x1000036D, 16 px) ──────────────────────
internal const uint HTrack = 0x06004C7Fu;
/// <summary>Left button = the INCREMENT designee 0x1000036C.</summary>
internal const uint LeftNormal = 0x06004C8Cu;
internal const uint LeftRollover = 0x06004C8Du;
internal const uint LeftPressed = 0x06004C8Eu;
/// <summary>Right button = the DECREMENT designee 0x1000036B.</summary>
internal const uint RightNormal = 0x06004C89u;
internal const uint RightRollover = 0x06004C8Au;
internal const uint RightPressed = 0x06004C8Bu;
internal const uint HThumbTopNormal = 0x06004C80u;
internal const uint HThumbTopRollover = 0x06004C81u;
internal const uint HThumbTopPressed = 0x06004C82u;
internal const uint HThumbMidNormal = 0x06004C83u;
internal const uint HThumbMidRollover = 0x06004C84u;
internal const uint HThumbMidPressed = 0x06004C85u;
internal const uint HThumbBotNormal = 0x06004C86u;
internal const uint HThumbBotRollover = 0x06004C87u;
internal const uint HThumbBotPressed = 0x06004C88u;
/// <summary>Wires the full retail vertical skin onto <paramref name="bar"/>.</summary>
internal static void ApplyVertical(UiScrollbar bar)
{
bar.TrackSprite = Track;
bar.UpSprite = UpNormal;
bar.UpRolloverSprite = UpRollover;
bar.UpPressedSprite = UpPressed;
bar.DownSprite = DownNormal;
bar.DownRolloverSprite = DownRollover;
bar.DownPressedSprite = DownPressed;
bar.ThumbTopSprite = ThumbTopNormal;
bar.ThumbTopRolloverSprite = ThumbTopRollover;
bar.ThumbTopPressedSprite = ThumbTopPressed;
bar.ThumbSprite = ThumbMidNormal;
bar.ThumbRolloverSprite = ThumbMidRollover;
bar.ThumbPressedSprite = ThumbMidPressed;
bar.ThumbBotSprite = ThumbBotNormal;
bar.ThumbBotRolloverSprite = ThumbBotRollover;
bar.ThumbBotPressedSprite = ThumbBotPressed;
}
/// <summary>Wires the full retail horizontal skin onto <paramref name="bar"/>.
/// The leading (<see cref="UiScrollbar.UpSprite"/>) slot is the LEFT edge.</summary>
internal static void ApplyHorizontal(UiScrollbar bar)
{
bar.TrackSprite = HTrack;
bar.UpSprite = LeftNormal;
bar.UpRolloverSprite = LeftRollover;
bar.UpPressedSprite = LeftPressed;
bar.DownSprite = RightNormal;
bar.DownRolloverSprite = RightRollover;
bar.DownPressedSprite = RightPressed;
bar.ThumbTopSprite = HThumbTopNormal;
bar.ThumbTopRolloverSprite = HThumbTopRollover;
bar.ThumbTopPressedSprite = HThumbTopPressed;
bar.ThumbSprite = HThumbMidNormal;
bar.ThumbRolloverSprite = HThumbMidRollover;
bar.ThumbPressedSprite = HThumbMidPressed;
bar.ThumbBotSprite = HThumbBotNormal;
bar.ThumbBotRolloverSprite = HThumbBotRollover;
bar.ThumbBotPressedSprite = HThumbBotPressed;
}
}