fix #430: tooltip wrap and draw honor the text child's authored margins
Third owner-screenshot round: acdream fit one more word per line than retail and drew glyphs flush against the popup's right border. Retail's InqSizewMargins @0x00469660 wraps the glyph list at (bound - m_margL - m_margR) and adds the margins back into the measured width; the popup skins' shared text child 0x10000396 authors margins L=2/R=2 (U=2/D=2 on three of the four skins — live-DAT probed). The presenter now subtracts the horizontal margins from both wrap passes, re-adds them into the measured width used for root sizing, and counts the vertical margins in the measured/re-wrapped heights; the widget's own draw already insets by all four margins (UiText ContentOffsetX + the top/bottom inset), so the right-side spacing returns for free. TooltipSkinLiveDatTests pins the authored margins per skin alongside the P0x3D=256 wrap bound; a new presenter test proves margins shrink the wrap bound and survive onto the widget. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
d087b50aa3
commit
35abbe1d0d
3 changed files with 92 additions and 11 deletions
|
|
@ -660,12 +660,25 @@ public sealed class RetailTooltipPresenter : IDisposable
|
|||
text.Centered = false;
|
||||
text.RightAligned = false;
|
||||
|
||||
float measureWrapWidth = text.AuthoredResizeMaxWidth is { } authoredMaxTextWidth
|
||||
// (c) The text child's authored margins (P0x23-0x26 — L2/R2/U2/D2 on
|
||||
// the popup skins) participate exactly as InqSizewMargins does:
|
||||
// GlyphList::Recalculate wraps at (width − margL − margR) and
|
||||
// the measured result adds the margins back
|
||||
// (@0x00469762/@0x004697..: `Recalculate(..., w − margL − margR)`
|
||||
// then `*out += margR + margL`). Without this, line one fit one
|
||||
// more word than retail (256 vs retail's 252 wrap) and the text
|
||||
// drew flush against the parchment's right border.
|
||||
float marginsX = text.MarginLeft + text.MarginRight;
|
||||
float marginsY = text.MarginTop + text.MarginBottom;
|
||||
|
||||
float wrapBound = text.AuthoredResizeMaxWidth is { } authoredMaxTextWidth
|
||||
? MathF.Max(1f, authoredMaxTextWidth)
|
||||
: MathF.Max(1f, _host.EffectiveCanvasSize.X);
|
||||
float measureWrapWidth = MathF.Max(1f, wrapBound - marginsX);
|
||||
var measured = UiText.WrapWords(tooltipText, measure, measureWrapWidth);
|
||||
float measuredWidth = measured.Count == 0 ? 0f : measured.Max(measure);
|
||||
float measuredHeight = measured.Count * lineHeight;
|
||||
float measuredWidth =
|
||||
(measured.Count == 0 ? 0f : measured.Max(measure)) + marginsX;
|
||||
float measuredHeight = measured.Count * lineHeight + marginsY;
|
||||
|
||||
float requestedWidth = root.Width + (measuredWidth - authoredTextWidth);
|
||||
float requestedHeight = root.Height + (measuredHeight - authoredTextHeight);
|
||||
|
|
@ -697,13 +710,17 @@ public sealed class RetailTooltipPresenter : IDisposable
|
|||
float textFinalHeight =
|
||||
authoredTextHeight + (requestedHeight - authoredRootHeight);
|
||||
|
||||
// Pass 3: re-wrap at the final clamped width (RecalculateGlyphList).
|
||||
var wrapped = UiText.WrapWords(tooltipText, measure, textFinalWidth);
|
||||
// Pass 3: re-wrap at the final clamped width, inside the margins
|
||||
// (RecalculateGlyphList wraps glyphs at width − margL − margR).
|
||||
var wrapped = UiText.WrapWords(
|
||||
tooltipText, measure, MathF.Max(1f, textFinalWidth - marginsX));
|
||||
text.LinesProvider = () => wrapped
|
||||
.Select(line => new UiText.Line(line, text.DefaultColor))
|
||||
.ToArray();
|
||||
text.Width = wrapped.Count == 0 ? 0f : MathF.Min(textFinalWidth, wrapped.Max(measure));
|
||||
float rewrappedHeight = wrapped.Count * lineHeight;
|
||||
text.Width = wrapped.Count == 0
|
||||
? 0f
|
||||
: MathF.Min(textFinalWidth, wrapped.Max(measure) + marginsX);
|
||||
float rewrappedHeight = wrapped.Count * lineHeight + marginsY;
|
||||
|
||||
// Pass 4: grow the root's height by the re-wrap's overflow beyond
|
||||
// the text child's post-resize height (width unchanged), through
|
||||
|
|
|
|||
|
|
@ -510,7 +510,52 @@ public sealed class RetailTooltipPresenterTests
|
|||
$"popup height {popup.Height} does not fit {lines.Count} lines");
|
||||
}
|
||||
|
||||
private static void HoverAndDwellWithClampedPopup(UiRoot root, int maxWidth)
|
||||
[Fact]
|
||||
public void AuthoredMargins_ShrinkTheWrapBound_AndInsetTheMeasuredWidth()
|
||||
{
|
||||
// The real skins' text child (0x10000396) authors margins L=2/R=2
|
||||
// (U=2/D=2 on three of four skins). Retail's InqSizewMargins
|
||||
// @0x00469660 wraps at (bound − margL − margR) and adds the margins
|
||||
// back into the measured width; without this, line one fit one more
|
||||
// word than retail and the glyphs drew flush against the popup's
|
||||
// right border (CA5 owner screenshot pair, 2026-08-24).
|
||||
var (root, _, _) = CreateHarness();
|
||||
string longText = string.Join(
|
||||
" ", Enumerable.Repeat("description", 40));
|
||||
var target = new HoverTarget
|
||||
{
|
||||
Left = 100, Top = 100, Width = 40, Height = 20,
|
||||
AuthoredTooltipEnabled = true,
|
||||
AuthoredTooltipText = longText,
|
||||
AuthoredTooltipRootElementId = PopupRootId,
|
||||
AuthoredTooltipLayoutDid = PopupLayoutDid,
|
||||
};
|
||||
root.AddChild(target);
|
||||
|
||||
// Wide horizontal margins make the difference unmissable at the
|
||||
// 8px-per-char test measure: the wrap bound is 200−80=120.
|
||||
HoverAndDwellWithClampedPopup(root, maxWidth: 200, marginX: 40, marginY: 8);
|
||||
|
||||
var text = FindText(root.Children[^1]);
|
||||
Assert.NotNull(text);
|
||||
var lines = text!.LinesProvider();
|
||||
Assert.True(lines.Count >= 3, $"expected >=3 wrapped lines, got {lines.Count}");
|
||||
// Glyphs wrap INSIDE the margins...
|
||||
Assert.All(lines, l => Assert.True(
|
||||
l.Text.Length * 8f <= 200f - 80f + 8f,
|
||||
$"line ignored the margins: {l.Text}"));
|
||||
// ...and the widget keeps the margins so the draw insets by them
|
||||
// (the right-side spacing the owner's retail screenshot shows).
|
||||
Assert.Equal(40f, text.MarginLeft);
|
||||
Assert.Equal(40f, text.MarginRight);
|
||||
// Height accounts for the vertical margins on top of the lines.
|
||||
float lineHeight = 14f;
|
||||
Assert.True(root.Children[^1].Height >= lines.Count * lineHeight + 16f,
|
||||
$"popup height {root.Children[^1].Height} lost the vertical margins");
|
||||
}
|
||||
|
||||
private static void HoverAndDwellWithClampedPopup(
|
||||
UiRoot root, int maxWidth, int marginX = 0, int marginY = 0)
|
||||
{
|
||||
// The REAL skins bound the wrap on the TEXT CHILD: 0x10000396
|
||||
// authors P0x3D=256 on all four popup skins (live-DAT probed
|
||||
|
|
@ -520,7 +565,16 @@ public sealed class RetailTooltipPresenterTests
|
|||
{
|
||||
ImportedLayout layout = BuildPopup();
|
||||
if (layout.FindElement(TextChildId) is { } textChild)
|
||||
{
|
||||
textChild.AuthoredResizeMaxWidth = maxWidth;
|
||||
if (textChild is UiText t)
|
||||
{
|
||||
t.MarginLeft = marginX;
|
||||
t.MarginRight = marginX;
|
||||
t.MarginTop = marginY;
|
||||
t.MarginBottom = marginY;
|
||||
}
|
||||
}
|
||||
return layout;
|
||||
});
|
||||
HoverAndDwell(root);
|
||||
|
|
|
|||
|
|
@ -29,16 +29,26 @@ public sealed class TooltipSkinLiveDatTests
|
|||
Assert.NotNull(tree);
|
||||
|
||||
int textChildren = 0;
|
||||
void Walk(ElementInfo e)
|
||||
void Walk(ElementInfo e, uint parentId)
|
||||
{
|
||||
if (e.Id == 0x10000396u && e.Type == 12)
|
||||
{
|
||||
textChildren++;
|
||||
Assert.Equal(256, e.MaxWidth);
|
||||
// Retail wraps at (256 - margL - margR) and draws inset by
|
||||
// the margins — the CA5 owner-screenshot delta (one extra
|
||||
// word on line one + text flush against the right border).
|
||||
// All four skins author L=2/R=2; skin 0x10000398 alone
|
||||
// authors no vertical margins (live-DAT probe 2026-08-24).
|
||||
Assert.Equal(2, e.MarginLeft);
|
||||
Assert.Equal(2, e.MarginRight);
|
||||
int vertical = parentId == 0x10000398u ? 0 : 2;
|
||||
Assert.Equal(vertical, e.MarginTop);
|
||||
Assert.Equal(vertical, e.MarginBottom);
|
||||
}
|
||||
foreach (var c in e.Children) Walk(c);
|
||||
foreach (var c in e.Children) Walk(c, e.Id);
|
||||
}
|
||||
Walk(tree!);
|
||||
Walk(tree!, 0);
|
||||
Assert.True(textChildren >= 4,
|
||||
$"expected the text child under all four popup skins, found {textChildren}");
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue