From d087b50aa3495db50a40f415365d2ec9321327a2 Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 24 Aug 2026 18:29:03 +0200 Subject: [PATCH] fix(ui): tooltip wrap bound comes from the popup text child's authored P0x3D, and tooltip text left-aligns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/ISSUES.md | 9 ++++ .../UI/Layout/RetailTooltipPresenter.cs | 23 +++++++++- .../UI/Layout/RetailTooltipPresenterTests.cs | 17 ++++--- .../UI/Layout/TooltipSkinLiveDatTests.cs | 45 +++++++++++++++++++ 4 files changed, 88 insertions(+), 6 deletions(-) create mode 100644 tests/AcDream.App.Tests/UI/Layout/TooltipSkinLiveDatTests.cs diff --git a/docs/ISSUES.md b/docs/ISSUES.md index 6b9fd3fa..5b2a7dc5 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -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- diff --git a/src/AcDream.App/UI/Layout/RetailTooltipPresenter.cs b/src/AcDream.App/UI/Layout/RetailTooltipPresenter.cs index cef1ced8..4982e10c 100644 --- a/src/AcDream.App/UI/Layout/RetailTooltipPresenter.cs +++ b/src/AcDream.App/UI/Layout/RetailTooltipPresenter.cs @@ -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); diff --git a/tests/AcDream.App.Tests/UI/Layout/RetailTooltipPresenterTests.cs b/tests/AcDream.App.Tests/UI/Layout/RetailTooltipPresenterTests.cs index 4cb985a3..30050e50 100644 --- a/tests/AcDream.App.Tests/UI/Layout/RetailTooltipPresenterTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/RetailTooltipPresenterTests.cs @@ -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); diff --git a/tests/AcDream.App.Tests/UI/Layout/TooltipSkinLiveDatTests.cs b/tests/AcDream.App.Tests/UI/Layout/TooltipSkinLiveDatTests.cs new file mode 100644 index 00000000..764ef742 --- /dev/null +++ b/tests/AcDream.App.Tests/UI/Layout/TooltipSkinLiveDatTests.cs @@ -0,0 +1,45 @@ +using AcDream.App.UI.Layout; +using DatReaderWriter; + +namespace AcDream.App.Tests.UI.Layout; + +/// +/// CA5 re-check pin (2026-08-24): the popup skins' shared TEXT CHILD +/// (0x10000396) authors P0x3D=256 — the wrap width retail's +/// InqSizewMargins UITS_MAX_WIDTH 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. +/// +[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}"); + } +}