fix(ui): tooltip wrap bound comes from the popup text child's authored P0x3D, and tooltip text left-aligns
Owner screenshots vs retail at the CA5 re-check caught both: Wrap width: the popup skins' shared TEXT CHILD (0x10000396) authors P0x3D=256 on all four skins — live-DAT probed, now pinned by an installed-DAT test. Retail's InqSizewMargins UITS_MAX_WIDTH reads the text element's 0x3D BEFORE the display-width fallback, so retail wraps tooltip text at 256px; our measure pass used the display width because TS-85's 'zero elements author P0x3D' sweep had only covered hover TARGETS, never the popup skins. ApplyTooltipText now measures and re-wraps at the text child's authored bound, falling back to the display width only when none is authored. Alignment: tooltip text rendered centered where retail hugs the left edge. The skin authors no justification; retail's unauthored default is Left, our importer's ElementInfo default is Center — the same wrong-default class as #410's VJustify finding, now recorded there as the horizontal sibling. Point-fixed in the presenter exactly as the chat transcript already does; the client-wide default flip stays #410's scope. The two-pass sizing test now models the real skin (max width on the text child) and asserts left alignment. Full hermetic suite 15,332 passed / 0 failed; the new live-DAT pin passes against the installed DATs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
1e8596a440
commit
d087b50aa3
4 changed files with 88 additions and 6 deletions
|
|
@ -1986,6 +1986,15 @@ porting.
|
|||
|
||||
## #410 — Client-wide VJustify (vertical text justification) enum mapping + unauthored default are wrong (retail default is Top, not Center)
|
||||
|
||||
**HJustify sibling evidence (2026-08-24, CA5 tooltip re-check):** the SAME
|
||||
wrong-default class exists horizontally — `ElementInfo.HJustify` defaults
|
||||
to Center while retail's unauthored default is Left. Observed live: the
|
||||
tooltip popup text (whose authored skin sets no justification) rendered
|
||||
centered where retail left-aligns; point-fixed in
|
||||
`RetailTooltipPresenter.ApplyTooltipText` (the chat transcript carries the
|
||||
same point-fix). When this issue's client-wide default sweep runs, fix H
|
||||
and V together.
|
||||
|
||||
**Status:** OPEN
|
||||
**Severity:** MEDIUM (silently mispositions every DAT-imported `UiText` that
|
||||
relies on the unauthored default, or that authors a raw vertical-
|
||||
|
|
|
|||
|
|
@ -643,7 +643,26 @@ public sealed class RetailTooltipPresenter : IDisposable
|
|||
// than the clamped popup stayed one clipped line.
|
||||
float lineHeight = text.DatFont?.LineHeight ?? text.Font?.LineHeight ?? 14f;
|
||||
|
||||
float measureWrapWidth = MathF.Max(1f, _host.EffectiveCanvasSize.X);
|
||||
// CA5 re-check corrections (2026-08-24, owner screenshots vs retail):
|
||||
// (a) The popup skins' TEXT CHILD (0x10000396) authors P0x3D=256 —
|
||||
// live-DAT probed on all four skins. InqSizewMargins'
|
||||
// UITS_MAX_WIDTH branch reads GetAttribute_Int(0x3D) on the TEXT
|
||||
// element BEFORE the display-width fallback, so retail wraps
|
||||
// tooltip text at 256px, not the screen width. (TS-85's "zero
|
||||
// elements author P0x3D" sweep only covered hover TARGETS, never
|
||||
// the popup skins' text children.)
|
||||
// (b) Tooltip text is LEFT-aligned: the text child authors no
|
||||
// justification and retail's unauthored default is Left, while
|
||||
// our importer's ElementInfo default is Center — the same
|
||||
// wrong-default class as #410's VJustify finding. Point-fixed
|
||||
// here (the chat transcript does the same); the client-wide
|
||||
// default remains #410's scope.
|
||||
text.Centered = false;
|
||||
text.RightAligned = false;
|
||||
|
||||
float measureWrapWidth = text.AuthoredResizeMaxWidth is { } authoredMaxTextWidth
|
||||
? MathF.Max(1f, authoredMaxTextWidth)
|
||||
: MathF.Max(1f, _host.EffectiveCanvasSize.X);
|
||||
var measured = UiText.WrapWords(tooltipText, measure, measureWrapWidth);
|
||||
float measuredWidth = measured.Count == 0 ? 0f : measured.Max(measure);
|
||||
float measuredHeight = measured.Count * lineHeight;
|
||||
|
|
@ -673,6 +692,8 @@ public sealed class RetailTooltipPresenter : IDisposable
|
|||
// measured extents.
|
||||
float textFinalWidth = MathF.Max(
|
||||
1f, authoredTextWidth + (requestedWidth - authoredRootWidth));
|
||||
if (text.AuthoredResizeMaxWidth is { } textMaxWidth)
|
||||
textFinalWidth = MathF.Min(textFinalWidth, textMaxWidth);
|
||||
float textFinalHeight =
|
||||
authoredTextHeight + (requestedHeight - authoredRootHeight);
|
||||
|
||||
|
|
|
|||
|
|
@ -492,14 +492,18 @@ public sealed class RetailTooltipPresenterTests
|
|||
|
||||
UiElement popup = root.Children[^1];
|
||||
Assert.True(root.Children.Count > childrenBefore, "popup did not mount");
|
||||
Assert.True(popup.Width <= 200f, $"popup width {popup.Width} escaped the clamp");
|
||||
// Root = authored 30 frame grown to hold the <=200 text plus padding.
|
||||
Assert.True(popup.Width <= 200f + 30f, $"popup width {popup.Width} escaped the clamp");
|
||||
var text = FindText(popup);
|
||||
Assert.NotNull(text);
|
||||
// Retail's unauthored justification default is LEFT (#410 class).
|
||||
Assert.False(text!.Centered);
|
||||
Assert.False(text.RightAligned);
|
||||
var lines = text!.LinesProvider();
|
||||
Assert.True(lines.Count >= 3,
|
||||
$"expected the clamped width to force >=3 lines, got {lines.Count}");
|
||||
// Every re-wrapped line must fit the clamped popup.
|
||||
Assert.All(lines, l => Assert.True(l.Text.Length * 8f <= 200f + 8f));
|
||||
Assert.All(lines, l => Assert.True(l.Text.Length * 8f <= 200f + 8f, $"line escaped wrap: {l.Text}"));
|
||||
// The root grew to hold the extra lines.
|
||||
float lineHeight = 14f;
|
||||
Assert.True(popup.Height >= lines.Count * lineHeight,
|
||||
|
|
@ -508,12 +512,15 @@ public sealed class RetailTooltipPresenterTests
|
|||
|
||||
private static void HoverAndDwellWithClampedPopup(UiRoot root, int maxWidth)
|
||||
{
|
||||
// A presenter whose popup skin authors ResizeMaxWidth, mirroring the
|
||||
// parchment skins' bounded frames.
|
||||
// The REAL skins bound the wrap on the TEXT CHILD: 0x10000396
|
||||
// authors P0x3D=256 on all four popup skins (live-DAT probed
|
||||
// 2026-08-24) — retail's InqSizewMargins UITS_MAX_WIDTH reads the
|
||||
// text element's 0x3D before the display-width fallback.
|
||||
var presenter = new RetailTooltipPresenter(root, (_, _) =>
|
||||
{
|
||||
ImportedLayout layout = BuildPopup();
|
||||
layout.Root.AuthoredResizeMaxWidth = maxWidth;
|
||||
if (layout.FindElement(TextChildId) is { } textChild)
|
||||
textChild.AuthoredResizeMaxWidth = maxWidth;
|
||||
return layout;
|
||||
});
|
||||
HoverAndDwell(root);
|
||||
|
|
|
|||
45
tests/AcDream.App.Tests/UI/Layout/TooltipSkinLiveDatTests.cs
Normal file
45
tests/AcDream.App.Tests/UI/Layout/TooltipSkinLiveDatTests.cs
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
using AcDream.App.UI.Layout;
|
||||
using DatReaderWriter;
|
||||
|
||||
namespace AcDream.App.Tests.UI.Layout;
|
||||
|
||||
/// <summary>
|
||||
/// CA5 re-check pin (2026-08-24): the popup skins' shared TEXT CHILD
|
||||
/// (0x10000396) authors <c>P0x3D=256</c> — the wrap width retail's
|
||||
/// <c>InqSizewMargins UITS_MAX_WIDTH</c> reads BEFORE its display-width
|
||||
/// fallback. TS-85's earlier "zero elements author P0x3D" sweep covered
|
||||
/// hover TARGETS only, never the popup skins; this pin closes that gap so
|
||||
/// a DAT revision (or importer regression) that loses the bound fails
|
||||
/// loudly instead of silently un-wrapping every tooltip.
|
||||
/// </summary>
|
||||
[Trait("Lane", "InstalledDat")]
|
||||
public sealed class TooltipSkinLiveDatTests
|
||||
{
|
||||
private static string DatDirectory =>
|
||||
Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
||||
"Documents", "Asheron's Call");
|
||||
|
||||
[InstalledDatFact]
|
||||
public void EveryPopupSkinTextChildAuthorsTheRetailWrapBound()
|
||||
{
|
||||
using var dats = new DatCollection(
|
||||
DatDirectory, DatReaderWriter.Options.DatAccessType.Read);
|
||||
ElementInfo? tree = LayoutImporter.ImportInfos(dats, 0x21000041u);
|
||||
Assert.NotNull(tree);
|
||||
|
||||
int textChildren = 0;
|
||||
void Walk(ElementInfo e)
|
||||
{
|
||||
if (e.Id == 0x10000396u && e.Type == 12)
|
||||
{
|
||||
textChildren++;
|
||||
Assert.Equal(256, e.MaxWidth);
|
||||
}
|
||||
foreach (var c in e.Children) Walk(c);
|
||||
}
|
||||
Walk(tree!);
|
||||
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