fix #430: tooltip wrap and draw honor the text child's authored margins
All checks were successful
CI / linux-portable (push) Successful in 3m29s
CI / windows-gate (push) Successful in 5m44s
CI / release (push) Successful in 2m19s

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:
Erik 2026-08-24 18:44:28 +02:00
parent d087b50aa3
commit 35abbe1d0d
3 changed files with 92 additions and 11 deletions

View file

@ -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 20080=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);

View file

@ -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}");
}