fix(ui): retain compound spellbook button children
Render Type-1 state-propagating child media as the button face so spellbook school and level filters show their authored green Highlight indicator and keep labels at the retail left inset. Lift captions, fonts, and colors from authored text children for framed buttons such as Delete, preventing centered filter labels from being covered by the overlapping delete frame. Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
parent
ac2ca8f965
commit
c918ccea06
3 changed files with 113 additions and 9 deletions
|
|
@ -269,12 +269,23 @@ row background 0x06001396
|
|||
selected overlay child 0x10000342 -> 0x06001397
|
||||
label child 0x10000344 (x=42, width=230)
|
||||
icon child 0x1000033B (x=0, 32 x 32)
|
||||
school/level filters Type-1, 90 x 14; label property on parent
|
||||
filter indicator child 0x10000328 (x=0, y=1, 13 x 13)
|
||||
filter Normal/Highlight art 0x06004D15 / 0x06004D17
|
||||
delete button 0x100002A5
|
||||
delete label child 0x100002A6 (100 x 32, centered, font 0x40000001)
|
||||
```
|
||||
|
||||
The row's authored background contains the narrow horizontal separator visible
|
||||
between entries. It is not an invented procedural line. The selection surface
|
||||
is a separate overlay, so selecting a spell does not replace its icon or label.
|
||||
The filter controls are ordinary `UIElement_Button` objects whose states have
|
||||
`PassToChildren`; the green selected face is therefore the `Highlight` media on
|
||||
their 13-pixel child, while the parent owns the label and the 90-pixel hit box.
|
||||
The label begins four pixels after that child. Conversely, the Delete button
|
||||
owns its frame media but its caption/font live in a full-size text child. A
|
||||
retained leaf must lift those authored child roles; discarding all button
|
||||
children loses both the filter indicator and the Delete caption.
|
||||
|
||||
```text
|
||||
AddSpell(spellID):
|
||||
|
|
|
|||
|
|
@ -76,9 +76,9 @@ public static class DatWidgetFactory
|
|||
UiElement e = info.Type switch
|
||||
{
|
||||
UiRadar.RetailClassId => new UiRadar(), // gmRadarUI (Register 0x004D8B80)
|
||||
1 => BuildButton(info, resolve, elementFont, stringResolve), // UIElement_Button
|
||||
1 => BuildButton(info, resolve, elementFont, fontResolve, stringResolve), // UIElement_Button
|
||||
EffectsIndicatorController.RetailClassId => BuildButton(
|
||||
info, resolve, elementFont, stringResolve), // gmUIElement_EffectsIndicator
|
||||
info, resolve, elementFont, fontResolve, stringResolve), // gmUIElement_EffectsIndicator
|
||||
6 => new UiMenu(), // UIElement_Menu (reg :120163)
|
||||
7 => BuildMeter(info, resolve, elementFont), // UIElement_Meter
|
||||
0xD => new UiViewport(), // UIElement_Viewport — 3-D mini-scene blit leaf
|
||||
|
|
@ -86,7 +86,8 @@ public static class DatWidgetFactory
|
|||
12 => BuildText(info, resolve, elementFont, stringResolve), // UIElement_Text
|
||||
0x13 => new UiDialogRoot(), // ConfirmationDialog
|
||||
0x10000031u => new UiItemList(resolve), // UIElement_ItemList — toolbar/inventory/paperdoll slots
|
||||
0x10000035u => BuildCheckbox(info, resolve, elementFont, stringResolve), // UIOption_Checkbox
|
||||
0x10000035u => BuildCheckbox(
|
||||
info, resolve, elementFont, fontResolve, stringResolve), // UIOption_Checkbox
|
||||
_ => new UiDatElement(info, resolve), // generic fallback (incl. Type 3 chrome/containers)
|
||||
};
|
||||
|
||||
|
|
@ -544,14 +545,63 @@ public static class DatWidgetFactory
|
|||
ElementInfo info,
|
||||
Func<uint, (uint, int, int)> resolve,
|
||||
UiDatFont? elementFont,
|
||||
Func<uint, UiDatFont?>? fontResolve,
|
||||
Func<UiStringInfoValue, string?>? stringResolve)
|
||||
=> new(info, resolve)
|
||||
{
|
||||
// UIElement_Button propagates its retail state into authored children.
|
||||
// Spellbook school/level filters are Type-1 buttons with no parent media:
|
||||
// their 13x13 child carries Normal/Highlight art. Keep that child as the
|
||||
// face of this retained leaf instead of consuming and losing it.
|
||||
ElementInfo? face = info.StateMedia.Count == 0
|
||||
? FindStatefulFaceChild(info)
|
||||
: null;
|
||||
|
||||
string? label = ResolveAuthoredString(info, stringResolve);
|
||||
ElementInfo labelInfo = info;
|
||||
if (label is null)
|
||||
{
|
||||
Label = ResolveAuthoredString(info, stringResolve),
|
||||
LabelFont = elementFont,
|
||||
LabelColor = info.FontColor ?? System.Numerics.Vector4.One,
|
||||
// Normal retail buttons such as the spellbook Delete control keep
|
||||
// their caption in a full-size UIElement_Text child. UiButton is a
|
||||
// retained leaf, so lift that authored text/font/color onto the leaf.
|
||||
foreach (ElementInfo child in info.Children.Where(child => child.Type == 12u))
|
||||
{
|
||||
label = ResolveAuthoredString(child, stringResolve);
|
||||
if (label is null) continue;
|
||||
labelInfo = child;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
UiDatFont? labelFont = elementFont;
|
||||
if (labelInfo.FontDid != 0u && fontResolve is not null)
|
||||
labelFont = fontResolve(labelInfo.FontDid) ?? elementFont;
|
||||
|
||||
var button = new UiButton(info, resolve, face)
|
||||
{
|
||||
Label = label,
|
||||
LabelFont = labelFont,
|
||||
LabelColor = labelInfo.FontColor ?? info.FontColor
|
||||
?? System.Numerics.Vector4.One,
|
||||
};
|
||||
|
||||
if (face is not null)
|
||||
{
|
||||
button.FaceLeft = face.X;
|
||||
button.FaceTop = face.Y;
|
||||
button.FaceWidth = face.Width;
|
||||
button.FaceHeight = face.Height;
|
||||
button.LabelAlign = UiButton.LabelAlignment.Left;
|
||||
button.LabelOffsetX = face.X + face.Width + 4f;
|
||||
}
|
||||
else if (!ReferenceEquals(labelInfo, info) && labelInfo.HJustify == HJustify.Left)
|
||||
{
|
||||
button.LabelAlign = UiButton.LabelAlignment.Left;
|
||||
button.LabelOffsetX = labelInfo.X;
|
||||
}
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail UIOption_Checkbox is a UIElement_Button whose visible face is its
|
||||
/// authored indicator child. Its label lives on the option object rather than
|
||||
|
|
@ -561,13 +611,16 @@ public static class DatWidgetFactory
|
|||
ElementInfo info,
|
||||
Func<uint, (uint, int, int)> resolve,
|
||||
UiDatFont? elementFont,
|
||||
Func<uint, UiDatFont?>? fontResolve,
|
||||
Func<UiStringInfoValue, string?>? stringResolve)
|
||||
{
|
||||
ElementInfo? indicator = info.Children.FirstOrDefault(child => DefaultImage(child) != 0u);
|
||||
ElementInfo? indicator = FindStatefulFaceChild(info);
|
||||
var button = new UiButton(info, resolve, indicator)
|
||||
{
|
||||
Label = ResolveAuthoredString(info, stringResolve),
|
||||
LabelFont = elementFont,
|
||||
LabelFont = info.FontDid != 0u && fontResolve is not null
|
||||
? fontResolve(info.FontDid) ?? elementFont
|
||||
: elementFont,
|
||||
LabelColor = info.FontColor ?? System.Numerics.Vector4.One,
|
||||
LabelAlign = UiButton.LabelAlignment.Left,
|
||||
};
|
||||
|
|
@ -584,6 +637,13 @@ public static class DatWidgetFactory
|
|||
return button;
|
||||
}
|
||||
|
||||
private static ElementInfo? FindStatefulFaceChild(ElementInfo info)
|
||||
=> info.Children.FirstOrDefault(child =>
|
||||
child.StateMedia.Count != 0
|
||||
&& child.StateMedia.Keys.Any(childState =>
|
||||
info.States.Values.Any(parentState =>
|
||||
string.Equals(parentState.Name, childState, StringComparison.Ordinal))));
|
||||
|
||||
private static string? ResolveAuthoredString(
|
||||
ElementInfo info,
|
||||
Func<UiStringInfoValue, string?>? stringResolve)
|
||||
|
|
|
|||
|
|
@ -57,6 +57,39 @@ public sealed class SpellbookWindowControllerTests
|
|||
Assert.Empty(closed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RetailFixture_BindsFilterIndicatorChildren_AndDeleteCaptionChild()
|
||||
{
|
||||
ImportedLayout layout = LayoutImporter.Build(
|
||||
FixtureLoader.LoadSpellbookInfos(),
|
||||
_ => (0u, 0, 0),
|
||||
datFont: null,
|
||||
stringResolve: value => value.StringId == 164868556u ? "Delete" : "Filter");
|
||||
|
||||
UiButton creature = Assert.IsType<UiButton>(layout.FindElement(0x10000298u));
|
||||
Assert.Equal((0f, 1f, 13f, 13f),
|
||||
(creature.FaceLeft, creature.FaceTop, creature.FaceWidth, creature.FaceHeight));
|
||||
Assert.Equal(UiButton.LabelAlignment.Left, creature.LabelAlign);
|
||||
Assert.Equal(17f, creature.LabelOffsetX);
|
||||
Assert.Equal("Filter", creature.Label);
|
||||
Assert.True(creature.ToggleBehavior);
|
||||
|
||||
creature.Selected = true;
|
||||
Assert.Equal("Highlight", creature.ActiveState);
|
||||
Assert.Equal(UiButtonStateMachine.Highlight, creature.ActiveRetailStateId);
|
||||
|
||||
UiButton levelSix = Assert.IsType<UiButton>(layout.FindElement(0x100002A1u));
|
||||
Assert.Equal(UiButton.LabelAlignment.Left, levelSix.LabelAlign);
|
||||
Assert.Equal(17f, levelSix.LabelOffsetX);
|
||||
|
||||
UiButton delete = Assert.IsType<UiButton>(
|
||||
layout.FindElement(SpellbookWindowController.DeleteButtonId));
|
||||
Assert.Equal("Delete", delete.Label);
|
||||
Assert.Equal(UiButton.LabelAlignment.Center, delete.LabelAlign);
|
||||
Assert.Equal((187f, 61f, 100f, 32f),
|
||||
(delete.Left, delete.Top, delete.FaceWidth, delete.FaceHeight));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LearnedSpells_AreAuthoredRows_InDisplayOrder_WithSelectionAndDragPayload()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue