diff --git a/src/AcDream.App/UI/Layout/DatWidgetFactory.cs b/src/AcDream.App/UI/Layout/DatWidgetFactory.cs index ec053cee..11512b0b 100644 --- a/src/AcDream.App/UI/Layout/DatWidgetFactory.cs +++ b/src/AcDream.App/UI/Layout/DatWidgetFactory.cs @@ -707,11 +707,17 @@ public static class DatWidgetFactory // newlines (the fellowship empty-state is three sentences over // '\n's). A single Line renders them as one clipped run — split // into one Line per authored line, exactly as retail's multiline - // UIElement_Text draws them. The provider re-reads DefaultColor + // UIElement_Text draws them. Gate round 2: the DAT stores the + // LITERAL two-character escape "\n" (0x5C 0x6E — probe-verified: + // the dump printed backslash-n, not a line break), so normalize + // the escape before splitting. The provider re-reads DefaultColor // per call (NOT captured eagerly) so state-driven font-color // changes keep tracking, the same live-color contract the // single-line provider always had. - string[] parts = [.. authored.Split('\n').Select(static p => p.TrimEnd('\r'))]; + string[] parts = [.. authored + .Replace("\\n", "\n") + .Split('\n') + .Select(static p => p.TrimEnd('\r'))]; t.LinesProvider = () => [.. parts.Select(p => new UiText.Line(p, t.DefaultColor))]; } diff --git a/src/AcDream.App/UI/Layout/SocialFellowshipPageController.cs b/src/AcDream.App/UI/Layout/SocialFellowshipPageController.cs index 14fb4ea7..69f50dab 100644 --- a/src/AcDream.App/UI/Layout/SocialFellowshipPageController.cs +++ b/src/AcDream.App/UI/Layout/SocialFellowshipPageController.cs @@ -102,6 +102,13 @@ public sealed class SocialFellowshipPageController private const uint ShareLootCheckboxId = 0x10000273u; // ── Row-template element ids (same doc, the 8-part fellow row) ── + /// Gate round 2 (2026-08-13): the row's name BAND — probe-verified + /// to author the retail selected-row art: DirectState media + /// 0x06001450 + a 'Highlight' state with the amber + /// 0x06001451 (ProbeSocialClickRouting's ROWSTATE dump). Selection + /// flips its — the authored + /// mechanism, replacing the retired invented tints. + private const uint RowNameBandId = 0x10000282u; private const uint RowNameTextId = 0x10000283u; private const uint RowStatsTextId = 0x10000284u; private const uint RowHealthMeterId = 0x10000285u; @@ -168,6 +175,7 @@ public sealed class SocialFellowshipPageController Func ResolveString); private readonly record struct FellowRowWidgets( + UiDatElement? NameBand, UiText? Name, UiText? Stats, UiMeter? Health, @@ -716,6 +724,7 @@ public sealed class SocialFellowshipPageController } var widgets = new FellowRowWidgets( + UiElement.FindDescendant(row, RowNameBandId) as UiDatElement, UiElement.FindDescendant(row, RowNameTextId) as UiText, UiElement.FindDescendant(row, RowStatsTextId) as UiText, UiElement.FindDescendant(row, RowHealthMeterId) as UiMeter, @@ -757,6 +766,11 @@ public sealed class SocialFellowshipPageController nameText.LinesProvider = () => [new UiText.Line(name, MemberNameColor)]; } + // Gate round 2: the AUTHORED selected-row cue — the name band's + // 'Highlight' state is retail's amber (see RowNameBandId's doc). + if (widgets.NameBand is { } nameBand) + nameBand.ActiveState = _selectedFellowGuid == member.Guid ? "Highlight" : ""; + if (widgets.Stats is { } statsText) { string text = FormatStatsText(member, snapshot); diff --git a/src/AcDream.App/UI/UiRoot.cs b/src/AcDream.App/UI/UiRoot.cs index 61dac04e..84f7f9f1 100644 --- a/src/AcDream.App/UI/UiRoot.cs +++ b/src/AcDream.App/UI/UiRoot.cs @@ -148,9 +148,13 @@ public sealed class UiRoot : UiElement var window = FindWindow(target); if (window is not { Draggable: true }) return false; - return !target.IsDragSource - && !target.CapturesPointerDrag - && !target.HandlesClick; + // 2026-08-13 gate (user-directed, all windows): the move cursor + // advertises ONLY on the window's own chrome — the frame's border + // pixels are the only place the frame element itself wins the + // hit-test (interior points resolve to content children), which + // matches retail's Dragbar-chrome-only move cursor. Whole-surface + // dragging still WORKS; it just no longer advertises over content. + return ReferenceEquals(target, window); } } private (uint tex, int w, int h)? _dragGhost; diff --git a/tests/AcDream.App.Tests/UI/Layout/SocialPanelLiveMountProbeTests.cs b/tests/AcDream.App.Tests/UI/Layout/SocialPanelLiveMountProbeTests.cs index 620055a3..5e4eeb8f 100644 --- a/tests/AcDream.App.Tests/UI/Layout/SocialPanelLiveMountProbeTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/SocialPanelLiveMountProbeTests.cs @@ -637,6 +637,20 @@ public sealed class SocialPanelLiveMountProbeTests $"[clickprobe] NOT-in-fellowship synthetic click -> optionWrites=[{string.Join(",", optionWrites)}]"); } + // The fellow row template's authored STATES (gate round 2: retail's + // selected-row amber — which element carries a Highlight/selected + // state, and what media?). + if (UiElement.FindDescendant(uiRoot, 0x10000279u) is UiTemplateListBox rosterBox + && rosterBox.Templates.Count > 0) + { + ElementInfo? rowInfo = LayoutImporter.ImportInfos( + dats, + rosterBox.Templates[0].TemplateLayoutId, + rosterBox.Templates[0].TemplateElementId); + if (rowInfo is not null) + DumpStates(rowInfo, 0); + } + // Friends + Squelch action widgets: authored labels → button roles // (never guess an id's role). foreach (uint pageId in new[] { 0x10000513u, 0x1000054Au }) @@ -672,6 +686,19 @@ public sealed class SocialPanelLiveMountProbeTests return (x, y); } + private static void DumpStates(ElementInfo info, int depth) + { + string states = string.Join( + ",", + info.StateMedia.Select(kv => $"'{kv.Key}'=0x{kv.Value.File:X8}")); + Console.WriteLine( + $"[clickprobe] {new string(' ', depth * 2)}ROWSTATE 0x{info.Id:X8} type=0x{info.Type:X8} " + + $"({info.X},{info.Y} {info.Width}x{info.Height}) default='{info.DefaultStateName}' " + + $"media=[{states}]"); + foreach (ElementInfo c in info.Children) + DumpStates(c, depth + 1); + } + private static void DumpActionWidgets(UiElement el, int depth) { string extra = el switch