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>
This commit is contained in:
parent
35abbe1d0d
commit
8fd3d1a9f0
12 changed files with 541 additions and 64 deletions
|
|
@ -137,13 +137,10 @@ public static class CharacterStatController
|
|||
private const uint SkillHeaderUnusableSprite = 0x06000F89u;
|
||||
private const uint RowHighlightSprite = 0x06001397u;
|
||||
|
||||
// Scrollbar chrome from base layout 0x2100003E, shared with chat/inventory.
|
||||
private const uint ScrollTrackSprite = 0x06004C5Fu;
|
||||
private const uint ScrollThumbSprite = 0x06004C63u;
|
||||
private const uint ScrollThumbTop = 0x06004C60u;
|
||||
private const uint ScrollThumbBot = 0x06004C66u;
|
||||
private const uint ScrollUpSprite = 0x06004C69u;
|
||||
private const uint ScrollDownSprite = 0x06004C6Cu;
|
||||
// Scrollbar chrome from base layout 0x2100003E, shared with chat/
|
||||
// inventory — sprite set + retail button seating live in
|
||||
// RetailScrollbarChrome (2026-08-24: the previous local constants seated
|
||||
// the DOWN-arrow art on the top button).
|
||||
|
||||
private enum CharacterStatTab
|
||||
{
|
||||
|
|
@ -681,12 +678,7 @@ public static class CharacterStatController
|
|||
Func<uint, (uint handle, int w, int h)> spriteResolve)
|
||||
{
|
||||
bar.SpriteResolve = id => { var (h, w, ht) = spriteResolve(id); return (h, w, ht); };
|
||||
bar.TrackSprite = ScrollTrackSprite;
|
||||
bar.ThumbSprite = ScrollThumbSprite;
|
||||
bar.ThumbTopSprite = ScrollThumbTop;
|
||||
bar.ThumbBotSprite = ScrollThumbBot;
|
||||
bar.UpSprite = ScrollUpSprite;
|
||||
bar.DownSprite = ScrollDownSprite;
|
||||
RetailScrollbarChrome.ApplyVertical(bar);
|
||||
}
|
||||
|
||||
private static float SkillViewportWidth(UiElement statList, UiScrollbar? bar)
|
||||
|
|
|
|||
|
|
@ -272,8 +272,11 @@ public static class ConfigOptionsPageController
|
|||
public const uint ScrollThumbTop = 0x06004C60u;
|
||||
public const uint ScrollThumb = 0x06004C63u;
|
||||
public const uint ScrollThumbBottom = 0x06004C66u;
|
||||
public const uint ScrollUp = 0x06004C69u;
|
||||
public const uint ScrollDown = 0x06004C6Cu;
|
||||
// Retail seating (UpdateScrollingArea @0x00470AA0): the top slot
|
||||
// takes the INCREMENT designee's UP-arrow art, the bottom the
|
||||
// DECREMENT designee's DOWN-arrow (see RetailScrollbarChrome).
|
||||
public const uint ScrollUp = RetailScrollbarChrome.UpNormal;
|
||||
public const uint ScrollDown = RetailScrollbarChrome.DownNormal;
|
||||
}
|
||||
|
||||
/// <summary>Applies <see cref="MenuChromeSprites"/> + geometry to a
|
||||
|
|
|
|||
|
|
@ -232,16 +232,28 @@ public static class DatWidgetFactory
|
|||
uint decrementId = ReferencedElementId(info, 0x78u);
|
||||
ElementInfo? increment = info.Children.FirstOrDefault(child => child.Id == incrementId);
|
||||
ElementInfo? decrement = info.Children.FirstOrDefault(child => child.Id == decrementId);
|
||||
ElementInfo? leadingButton = new[] { increment, decrement }
|
||||
.Where(child => child is not null)
|
||||
.OrderBy(child => bar.Horizontal ? child!.X : child!.Y)
|
||||
.ThenBy(child => child!.ReadOrder)
|
||||
.FirstOrDefault();
|
||||
ElementInfo? trailingButton = new[] { increment, decrement }
|
||||
.Where(child => child is not null)
|
||||
.OrderByDescending(child => bar.Horizontal ? child!.X : child!.Y)
|
||||
.ThenByDescending(child => child!.ReadOrder)
|
||||
.FirstOrDefault();
|
||||
|
||||
// Seat by DESIGNATION, not authored position: retail's
|
||||
// UpdateScrollingArea @0x00470AA0 moves the increment designee to the
|
||||
// top/left corner (MoveTo(0,0)) and the decrement designee to the
|
||||
// bottom/right corner regardless of where the layout drew them. The
|
||||
// vertical base skin authors the DOWN-arrow (0x10000071) at Y=0 and
|
||||
// the UP-arrow (0x10000072) at Y=32, so the previous authored-Y
|
||||
// ordering rendered both arrows upside down (2026-08-24 owner
|
||||
// report). Fall back to authored order only when the designations
|
||||
// are missing.
|
||||
ElementInfo? leadingButton = increment;
|
||||
ElementInfo? trailingButton = decrement;
|
||||
if (leadingButton is null && trailingButton is null)
|
||||
{
|
||||
ElementInfo[] typeOneChildren = info.Children
|
||||
.Where(child => child.Type == 1u && child.Id != 1u)
|
||||
.OrderBy(child => bar.Horizontal ? child.X : child.Y)
|
||||
.ThenBy(child => child.ReadOrder)
|
||||
.ToArray();
|
||||
leadingButton = typeOneChildren.FirstOrDefault();
|
||||
trailingButton = typeOneChildren.Length > 1 ? typeOneChildren[^1] : null;
|
||||
}
|
||||
bar.UpSprite = ButtonStateImage(leadingButton, "Normal");
|
||||
bar.UpRolloverSprite = ButtonStateImage(leadingButton, "Normal_rollover");
|
||||
bar.UpPressedSprite = ButtonStateImage(leadingButton, "Normal_pressed");
|
||||
|
|
@ -323,9 +335,24 @@ public static class DatWidgetFactory
|
|||
.OrderBy(child => child.Y)
|
||||
.ThenBy(child => child.ReadOrder)
|
||||
.ToArray();
|
||||
if (slices.Length > 0) bar.ThumbTopSprite = DefaultImage(slices[0]);
|
||||
if (slices.Length > 1) bar.ThumbSprite = DefaultImage(slices[1]);
|
||||
if (slices.Length > 2) bar.ThumbBotSprite = DefaultImage(slices[^1]);
|
||||
if (slices.Length > 0)
|
||||
{
|
||||
bar.ThumbTopSprite = ButtonStateImage(slices[0], "Normal");
|
||||
bar.ThumbTopRolloverSprite = ButtonStateImage(slices[0], "Normal_rollover");
|
||||
bar.ThumbTopPressedSprite = ButtonStateImage(slices[0], "Normal_pressed");
|
||||
}
|
||||
if (slices.Length > 1)
|
||||
{
|
||||
bar.ThumbSprite = ButtonStateImage(slices[1], "Normal");
|
||||
bar.ThumbRolloverSprite = ButtonStateImage(slices[1], "Normal_rollover");
|
||||
bar.ThumbPressedSprite = ButtonStateImage(slices[1], "Normal_pressed");
|
||||
}
|
||||
if (slices.Length > 2)
|
||||
{
|
||||
bar.ThumbBotSprite = ButtonStateImage(slices[^1], "Normal");
|
||||
bar.ThumbBotRolloverSprite = ButtonStateImage(slices[^1], "Normal_rollover");
|
||||
bar.ThumbBotPressedSprite = ButtonStateImage(slices[^1], "Normal_pressed");
|
||||
}
|
||||
|
||||
// R3-4/R3-7 (Campaign CC gate round 1 re-test 2): retail authors
|
||||
// TWO distinct thumb shapes for UIElement_Scrollbar (Type 11) —
|
||||
|
|
@ -350,7 +377,11 @@ public static class DatWidgetFactory
|
|||
// slice children (chat) is unaffected since `slices.Length == 0`
|
||||
// is false for that shape.
|
||||
if (slices.Length == 0)
|
||||
bar.ThumbSprite = DefaultImage(thumb);
|
||||
{
|
||||
bar.ThumbSprite = ButtonStateImage(thumb, "Normal");
|
||||
bar.ThumbRolloverSprite = ButtonStateImage(thumb, "Normal_rollover");
|
||||
bar.ThumbPressedSprite = ButtonStateImage(thumb, "Normal_pressed");
|
||||
}
|
||||
}
|
||||
|
||||
return bar;
|
||||
|
|
|
|||
|
|
@ -98,13 +98,10 @@ public sealed class ExternalContainerController : IItemListDragHandler, IRetaine
|
|||
scrollbar.SpriteResolve ??= _contentsList.SpriteResolve;
|
||||
// Horizontal base LayoutDesc 0x2100003E media. The compatibility
|
||||
// factory treats all horizontal bars as scalar controls; this panel
|
||||
// binds the authored model sprites explicitly.
|
||||
scrollbar.TrackSprite = 0x06004C7Fu;
|
||||
scrollbar.ThumbTopSprite = 0x06004C80u;
|
||||
scrollbar.ThumbSprite = 0x06004C83u;
|
||||
scrollbar.ThumbBotSprite = 0x06004C86u;
|
||||
scrollbar.DownSprite = 0x06004C89u;
|
||||
scrollbar.UpSprite = 0x06004C8Cu;
|
||||
// binds the authored model sprites explicitly — full state set +
|
||||
// retail seating from RetailScrollbarChrome (increment/left =
|
||||
// 0x06004C8C, decrement/right = 0x06004C89).
|
||||
RetailScrollbarChrome.ApplyHorizontal(scrollbar);
|
||||
}
|
||||
|
||||
BindClose(layout, RequestClose);
|
||||
|
|
|
|||
|
|
@ -181,8 +181,8 @@ public sealed class VendorUiController : IRetainedPanelController, IItemListDrag
|
|||
/// list, with a real thumb/up/down-button subtree matching
|
||||
/// <see cref="UiScrollbar"/>'s own shape exactly: thumb caps
|
||||
/// <c>0x06004C60</c>/<c>63</c>/<c>66</c>, up button (element
|
||||
/// <c>0x10000071</c>) <c>0x06004C69</c>/<c>6A</c>/<c>6B</c>, down button
|
||||
/// (element <c>0x10000072</c>) <c>0x06004C6C</c>/<c>6D</c>/<c>6E</c>,
|
||||
/// <c>0x10000072</c>, retail-seated on top) <c>0x06004C6C</c>/<c>6D</c>/<c>6E</c>, down button
|
||||
/// (element <c>0x10000071</c>, retail-seated on the bottom) <c>0x06004C69</c>/<c>6A</c>/<c>6B</c>,
|
||||
/// track <c>0x06004C5F</c>). With 18 authored categories and only 6
|
||||
/// visible rows, retail's actual rendering is a single scrolling column
|
||||
/// (matching the user's reference screenshot: ~visible rows + scrollbar +
|
||||
|
|
@ -288,8 +288,10 @@ public sealed class VendorUiController : IRetainedPanelController, IItemListDrag
|
|||
private const uint TypeMenuScrollThumbTopSprite = 0x06004C60u;
|
||||
private const uint TypeMenuScrollThumbSprite = 0x06004C63u;
|
||||
private const uint TypeMenuScrollThumbBottomSprite = 0x06004C66u;
|
||||
private const uint TypeMenuScrollUpSprite = 0x06004C69u;
|
||||
private const uint TypeMenuScrollDownSprite = 0x06004C6Cu;
|
||||
// Retail seating (UpdateScrollingArea @0x00470AA0): top = the INCREMENT
|
||||
// designee's UP-arrow art, bottom = the DECREMENT designee's DOWN-arrow.
|
||||
private const uint TypeMenuScrollUpSprite = RetailScrollbarChrome.UpNormal;
|
||||
private const uint TypeMenuScrollDownSprite = RetailScrollbarChrome.DownNormal;
|
||||
|
||||
/// <summary>
|
||||
/// Retail's ordered category table, transcribed verbatim from
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue