fix(ui): match spellbook frame and tab alignment
This commit is contained in:
parent
16eb4844c1
commit
1249ad03de
5 changed files with 116 additions and 7 deletions
|
|
@ -221,6 +221,16 @@ UIElement_Text::OnSetAttribute @ 0x0046A640
|
||||||
SetOneLine(value)
|
SetOneLine(value)
|
||||||
// absent 0x20 therefore remains multi-line; do not force it true
|
// absent 0x20 therefore remains multi-line; do not force it true
|
||||||
|
|
||||||
|
UIElement_Text::CalcJustification @ 0x00467260
|
||||||
|
available = max(scrollableExtent, surfaceExtent) - leadingMargin - trailingMargin
|
||||||
|
if justification is Center:
|
||||||
|
offset = available / 2 - contentExtent / 2
|
||||||
|
else if justification is Right or Bottom:
|
||||||
|
offset = available - contentExtent
|
||||||
|
else:
|
||||||
|
offset = 0
|
||||||
|
return leadingMargin + offset
|
||||||
|
|
||||||
UIElement::SetState @ 0x00464E70
|
UIElement::SetState @ 0x00464E70
|
||||||
next = desc.AccessStateDesc(stateId)
|
next = desc.AccessStateDesc(stateId)
|
||||||
if next != current:
|
if next != current:
|
||||||
|
|
@ -245,6 +255,12 @@ before its retained children lets the center chrome overpaint the caption.
|
||||||
This rule is derived structurally from `PassToChildren`, not from the two
|
This rule is derived structurally from `PassToChildren`, not from the two
|
||||||
spellbook element ids.
|
spellbook element ids.
|
||||||
|
|
||||||
|
Layout `0x21000034` supplies the 300 by 600 content surface, including its tab
|
||||||
|
bar and close control, but not the enclosing movable-window border. Production
|
||||||
|
mounts it through the same shared retained nine-slice frame used by the
|
||||||
|
Attributes/Skills window (`0x2100002E`). Treating the root as already framed
|
||||||
|
removes the gold outer border.
|
||||||
|
|
||||||
The Magic-combat favorite tab captions are authored text (`I` through `VIII`)
|
The Magic-combat favorite tab captions are authored text (`I` through `VIII`)
|
||||||
and intentionally omit property `0x20`. Their small rectangles still render
|
and intentionally omit property `0x20`. Their small rectangles still render
|
||||||
because retail begins with zero text margins. A generic four-pixel inset on
|
because retail begins with zero text margins. A generic four-pixel inset on
|
||||||
|
|
|
||||||
|
|
@ -764,7 +764,10 @@ public sealed class RetailUiRuntime : IDisposable
|
||||||
new RetailWindowFrame.Options
|
new RetailWindowFrame.Options
|
||||||
{
|
{
|
||||||
WindowName = WindowNames.Spellbook,
|
WindowName = WindowNames.Spellbook,
|
||||||
Chrome = RetailWindowChrome.Imported,
|
// LayoutDesc 0x21000034 is the 300x600 content surface. Like the
|
||||||
|
// Attributes/Skills window, retail supplies the outer gold frame
|
||||||
|
// through the shared window chrome rather than this root element.
|
||||||
|
Chrome = RetailWindowChrome.NineSlice,
|
||||||
Left = 18f,
|
Left = 18f,
|
||||||
Top = 18f,
|
Top = 18f,
|
||||||
Visible = false,
|
Visible = false,
|
||||||
|
|
|
||||||
|
|
@ -166,6 +166,7 @@ public sealed class UiText : UiElement, IUiDatStateful
|
||||||
private uint _activeRetailStateId = UiStateInfo.DirectStateId;
|
private uint _activeRetailStateId = UiStateInfo.DirectStateId;
|
||||||
private string _activeDatStateName = "";
|
private string _activeDatStateName = "";
|
||||||
private bool _drawTextAfterChildren;
|
private bool _drawTextAfterChildren;
|
||||||
|
private bool _honorDatVerticalJustification;
|
||||||
|
|
||||||
// ── Selection state ──────────────────────────────────────────────────
|
// ── Selection state ──────────────────────────────────────────────────
|
||||||
private Pos? _selAnchor; // where the drag started
|
private Pos? _selAnchor; // where the drag started
|
||||||
|
|
@ -206,6 +207,7 @@ public sealed class UiText : UiElement, IUiDatStateful
|
||||||
internal void ConfigureDatState(ElementInfo info)
|
internal void ConfigureDatState(ElementInfo info)
|
||||||
{
|
{
|
||||||
_datInfo = info;
|
_datInfo = info;
|
||||||
|
_honorDatVerticalJustification = true;
|
||||||
// Retail spellbook tabs are UIElement_Text parents whose Open/Closed
|
// Retail spellbook tabs are UIElement_Text parents whose Open/Closed
|
||||||
// PassToChildren states drive three authored chrome pieces. In retail's
|
// PassToChildren states drive three authored chrome pieces. In retail's
|
||||||
// software surface those pieces form the tab background while the text
|
// software surface those pieces form the tab background while the text
|
||||||
|
|
@ -429,12 +431,18 @@ public sealed class UiText : UiElement, IUiDatStateful
|
||||||
(int)MathF.Floor(innerH),
|
(int)MathF.Floor(innerH),
|
||||||
preserveEnd: true);
|
preserveEnd: true);
|
||||||
|
|
||||||
// UiScrollable: ScrollY=0 is TOP/oldest, ScrollY=MaxScroll is BOTTOM/newest.
|
// Overflow keeps the UiScrollable convention: ScrollY=0 is TOP/oldest and
|
||||||
// Visual layout: newest at bottom → baseY = bottom - contentH (ScrollY at max).
|
// ScrollY=MaxScroll is BOTTOM/newest. Fitting DAT-authored content instead
|
||||||
// Invert: baseY = bottom - contentH + (MaxScroll - ScrollY).
|
// uses retail CalcJustification (tabs center their one glyph line); synthesized
|
||||||
// With _pinBottom: ScrollY=MaxScroll → baseY=bottom-contentH → last line ends at bottom. ✓
|
// transcript widgets retain the established bottom pin.
|
||||||
// Scrolled to top: ScrollY=0 → baseY=bottom-contentH+MaxScroll=bottom-innerH=top. ✓
|
float baseY = ContentBaseY(
|
||||||
float baseY = bottom - contentH + (Scroll.MaxScroll - Scroll.ScrollY);
|
top,
|
||||||
|
bottom,
|
||||||
|
contentH,
|
||||||
|
Scroll.MaxScroll,
|
||||||
|
Scroll.ScrollY,
|
||||||
|
VerticalJustify,
|
||||||
|
_honorDatVerticalJustification);
|
||||||
_lastBaseY = baseY;
|
_lastBaseY = baseY;
|
||||||
|
|
||||||
// Normalised selection span (start <= end), if any.
|
// Normalised selection span (start <= end), if any.
|
||||||
|
|
@ -624,6 +632,35 @@ public sealed class UiText : UiElement, IUiDatStateful
|
||||||
_ => (height - lineHeight) * 0.5f, // Center (default)
|
_ => (height - lineHeight) * 0.5f, // Center (default)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resolve the first line's Y coordinate for the normal multi-line path.
|
||||||
|
/// Retail <c>UIElement_Text::CalcJustification @ 0x00467260</c> applies the
|
||||||
|
/// authored vertical justification when the text content fits the surface;
|
||||||
|
/// overflow continues to use the scroll offset. Synthesized transcript widgets
|
||||||
|
/// retain the historical bottom-pinned behavior by passing
|
||||||
|
/// <paramref name="honorJustification"/> as <see langword="false"/>.
|
||||||
|
/// </summary>
|
||||||
|
public static float ContentBaseY(
|
||||||
|
float top,
|
||||||
|
float bottom,
|
||||||
|
float contentHeight,
|
||||||
|
float maxScroll,
|
||||||
|
float scrollY,
|
||||||
|
VJustify justification,
|
||||||
|
bool honorJustification)
|
||||||
|
{
|
||||||
|
float viewHeight = Math.Max(0f, bottom - top);
|
||||||
|
if (!honorJustification || contentHeight > viewHeight)
|
||||||
|
return bottom - contentHeight + (maxScroll - scrollY);
|
||||||
|
|
||||||
|
return justification switch
|
||||||
|
{
|
||||||
|
VJustify.Top => top,
|
||||||
|
VJustify.Bottom => bottom - contentHeight,
|
||||||
|
_ => top + (viewHeight - contentHeight) * 0.5f,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Order two caret positions so the first is <= the second (by line,
|
/// <summary>Order two caret positions so the first is <= the second (by line,
|
||||||
/// then column).</summary>
|
/// then column).</summary>
|
||||||
public static (Pos start, Pos end) Order(Pos a, Pos b)
|
public static (Pos start, Pos end) Order(Pos a, Pos b)
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,9 @@ public sealed class SpellbookWindowControllerTests
|
||||||
|
|
||||||
Assert.Equal("Spellbook", Assert.Single(spellTab.LinesProvider()).Text);
|
Assert.Equal("Spellbook", Assert.Single(spellTab.LinesProvider()).Text);
|
||||||
Assert.Equal("Components", Assert.Single(componentTab.LinesProvider()).Text);
|
Assert.Equal("Components", Assert.Single(componentTab.LinesProvider()).Text);
|
||||||
|
Assert.False(spellTab.OneLine);
|
||||||
|
Assert.True(spellTab.Centered);
|
||||||
|
Assert.Equal(VJustify.Center, spellTab.VerticalJustify);
|
||||||
Assert.True(spellTab.DrawTextAfterChildren);
|
Assert.True(spellTab.DrawTextAfterChildren);
|
||||||
Assert.True(componentTab.DrawTextAfterChildren);
|
Assert.True(componentTab.DrawTextAfterChildren);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -227,4 +227,54 @@ public class UiTextTests
|
||||||
Assert.Equal(yNoPad, yWithPad); // Center is padding-independent
|
Assert.Equal(yNoPad, yWithPad); // Center is padding-independent
|
||||||
Assert.Equal((40f - 12f) * 0.5f, yNoPad);
|
Assert.Equal((40f - 12f) * 0.5f, yNoPad);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(VJustify.Top, 0f)]
|
||||||
|
[InlineData(VJustify.Center, 4.5f)]
|
||||||
|
[InlineData(VJustify.Bottom, 9f)]
|
||||||
|
public void ContentBaseY_FittingAuthoredText_UsesRetailVerticalJustification(
|
||||||
|
VJustify justification,
|
||||||
|
float expected)
|
||||||
|
{
|
||||||
|
float y = UiText.ContentBaseY(
|
||||||
|
top: 0f,
|
||||||
|
bottom: 25f,
|
||||||
|
contentHeight: 16f,
|
||||||
|
maxScroll: 0f,
|
||||||
|
scrollY: 0f,
|
||||||
|
justification,
|
||||||
|
honorJustification: true);
|
||||||
|
|
||||||
|
Assert.Equal(expected, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ContentBaseY_OverflowingAuthoredText_PreservesScrollPosition()
|
||||||
|
{
|
||||||
|
float y = UiText.ContentBaseY(
|
||||||
|
top: 0f,
|
||||||
|
bottom: 25f,
|
||||||
|
contentHeight: 64f,
|
||||||
|
maxScroll: 39f,
|
||||||
|
scrollY: 12f,
|
||||||
|
VJustify.Center,
|
||||||
|
honorJustification: true);
|
||||||
|
|
||||||
|
Assert.Equal(-12f, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ContentBaseY_SynthesizedText_RetainsBottomPinWhenContentFits()
|
||||||
|
{
|
||||||
|
float y = UiText.ContentBaseY(
|
||||||
|
top: 0f,
|
||||||
|
bottom: 25f,
|
||||||
|
contentHeight: 16f,
|
||||||
|
maxScroll: 0f,
|
||||||
|
scrollY: 0f,
|
||||||
|
VJustify.Center,
|
||||||
|
honorJustification: false);
|
||||||
|
|
||||||
|
Assert.Equal(9f, y);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue