fix(ui): match retail spell bar controls

Place favorite-bar arrows by their authored sides, import rollover and pressed media through the shared scrollbar, and preserve manual offsets across passive refreshes. Carry the mixed-parent DAT anchor chain to a fixed 18-cell favorite viewport so overflow controls and the Cast button remain inside the retail-sized combat frame.

Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-23 07:01:01 +02:00
parent 02c29e67c8
commit 0134122c28
17 changed files with 424 additions and 59 deletions

View file

@ -126,8 +126,11 @@ public static class DatWidgetFactory
/// <summary>
/// Bind inherited scrollbar media structurally. Property 0x77 names the
/// increment button and 0x78 the decrement button; the remaining Type-1
/// child is the thumb with ordered top/middle/bottom image slices.
/// increment button and 0x78 the decrement button; retail
/// <c>UIElement_Scrollbar::UpdateScrollingArea @ 0x00470AA0</c> then places
/// those referenced child buttons by their authored leading/trailing
/// positions. The remaining Type-1 child is the thumb with ordered
/// top/middle/bottom image slices.
/// </summary>
private static UiScrollbar BuildScrollbar(
ElementInfo info,
@ -144,17 +147,31 @@ 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);
bar.UpSprite = decrement is null ? 0u : DefaultImage(decrement);
bar.DownSprite = increment is null ? 0u : DefaultImage(increment);
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();
bar.UpSprite = ButtonStateImage(leadingButton, "Normal");
bar.UpRolloverSprite = ButtonStateImage(leadingButton, "Normal_rollover");
bar.UpPressedSprite = ButtonStateImage(leadingButton, "Normal_pressed");
bar.DownSprite = ButtonStateImage(trailingButton, "Normal");
bar.DownRolloverSprite = ButtonStateImage(trailingButton, "Normal_rollover");
bar.DownPressedSprite = ButtonStateImage(trailingButton, "Normal_pressed");
if (info.TryGetEffectiveBool(0x79u, out bool hideDisabled))
bar.HideWhenDisabled = hideDisabled;
if (bar.Horizontal)
{
if (decrement is { Width: > 0f })
bar.DecrementButtonExtent = decrement.Width;
if (increment is { Width: > 0f })
bar.IncrementButtonExtent = increment.Width;
if (leadingButton is { Width: > 0f })
bar.DecrementButtonExtent = leadingButton.Width;
if (trailingButton is { Width: > 0f })
bar.IncrementButtonExtent = trailingButton.Width;
// Retail horizontal scrollbars use structural child ids: element 1 is
// the thumb and element 4 is the optional child-authored track.
@ -207,10 +224,10 @@ public static class DatWidgetFactory
return bar;
}
if (decrement is { Height: > 0f })
bar.DecrementButtonExtent = decrement.Height;
if (increment is { Height: > 0f })
bar.IncrementButtonExtent = increment.Height;
if (leadingButton is { Height: > 0f })
bar.DecrementButtonExtent = leadingButton.Height;
if (trailingButton is { Height: > 0f })
bar.IncrementButtonExtent = trailingButton.Height;
ElementInfo? thumb = info.Children.FirstOrDefault(child =>
child.Type == 1u && child.Id != incrementId && child.Id != decrementId);
@ -272,6 +289,19 @@ public static class DatWidgetFactory
return 0u;
}
private static uint ButtonStateImage(ElementInfo? info, string stateName)
{
if (info is null)
return 0u;
if (info.StateMedia.TryGetValue(stateName, out var media))
return media.File;
UiStateInfo? state = info.States.Values.FirstOrDefault(
candidate => string.Equals(candidate.Name, stateName, StringComparison.Ordinal));
if (state?.Image is { } image)
return image.File;
return stateName == "Normal" ? DefaultImage(info) : 0u;
}
// ── Meter ────────────────────────────────────────────────────────────────
/// <summary>