fix(chat): CH6a/b rework — grip media, retail window-id model, floaty fixture
Applies docs/research/2026-08-10-ch6ab-review-findings.md in full:
- BLOCKER 1: UiResizeGrip now carries its ElementInfo/resolve pair and
draws its own authored DirectState media (a synthetic parameterless
grip still draws nothing, preserving existing resize-drag tests).
DatWidgetFactory.BuildResizeGrip threads resolve through. All seven
live grips on the main chat window now resolve a non-zero sprite,
restoring the visible borders/corners CH6a silently dropped.
- SHOULD-FIX 2: ChatWindowState gains BroadcastTargetWindow, a sentinel
distinct from every real window id (0-4), fixing the bug where the
main window's explicit-addressing branch coincided with the broadcast
check (both were literal 0). SetFilter's main-window no-op is dropped
— the main window's filter is now genuinely settable. ChatWindowController
.Bind takes a ChatWindowState (the same canonical instance the floating
windows already share) and GetTranscriptLines builds a real accept
predicate instead of accept:null. Verified safe: ClientLocal (0x1A)
never reaches ChatLog (AddText routes it to the SpewBox and returns),
so nothing observable regresses.
- SHOULD-FIX 3: UiButton.SuppressSelfToggle stops the four chat-window
indicator buttons (DAT property 0x0B=true, no retail click handler)
from flipping their own Selected mirror on a stray click.
- SHOULD-FIX 4: generated and committed chat_floaty_2100005b.json from
the real installed dats; added the permanent RetailLayoutFixtureGenerator
entry. All three flagged FloatingChatWindowController assumptions
(input field, title bar, close button) are confirmed correct against
real data — no controller code changes needed. New finding: unlike the
main window, ALL EIGHT floaty border/corner elements are live Type-9
grips (the floaty's own title bar is its move handle), so a floaty
window resizes from every edge and corner.
- SHOULD-FIX 5: register row AP-189 documents the shared-500-entry/
200-line-tail vs retail's per-window 10,000-line scrollback depth gap.
- NITs 1-5: documented the filter-persistence-only-on-/saveautoui
asymmetry and the reconnect-preserves-filters intent; corrected the
research doc's modifier-mask mislabel and the "ONLY function" false
superlative; moved WrapText off ChatWindowController onto
ChatTranscriptRenderer, closing the circular dependency.
Full Release suite: 12,420 passed / 4 skipped / 0 failed (baseline
12,392/4/0 at 22020ef2; net +28 tests, zero regressions).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
56b84deeab
commit
1aa7709988
23 changed files with 6284 additions and 248 deletions
|
|
@ -152,7 +152,7 @@ public class ChatWindowControllerTests
|
|||
var (rootInfo, layout, vm) = BuildTestTree();
|
||||
var bus = new CaptureBus();
|
||||
|
||||
var ctrl = ChatWindowController.Bind(rootInfo, layout, vm, () => bus, null, null, NoTex);
|
||||
var ctrl = ChatWindowController.Bind(rootInfo, layout, vm, () => bus, new ChatWindowState(), null, null, NoTex);
|
||||
|
||||
Assert.NotNull(ctrl);
|
||||
}
|
||||
|
|
@ -165,7 +165,7 @@ public class ChatWindowControllerTests
|
|||
var (rootInfo, layout, vm) = BuildTestTree();
|
||||
var bus = new CaptureBus();
|
||||
|
||||
var ctrl = ChatWindowController.Bind(rootInfo, layout, vm, () => bus, null, null, NoTex);
|
||||
var ctrl = ChatWindowController.Bind(rootInfo, layout, vm, () => bus, new ChatWindowState(), null, null, NoTex);
|
||||
|
||||
Assert.NotNull(ctrl);
|
||||
var panel = layout.FindElement(0x10000010u);
|
||||
|
|
@ -182,7 +182,7 @@ public class ChatWindowControllerTests
|
|||
var (rootInfo, layout, vm) = BuildTestTree();
|
||||
var bus = new CaptureBus();
|
||||
|
||||
var ctrl = ChatWindowController.Bind(rootInfo, layout, vm, () => bus, null, null, NoTex);
|
||||
var ctrl = ChatWindowController.Bind(rootInfo, layout, vm, () => bus, new ChatWindowState(), null, null, NoTex);
|
||||
|
||||
Assert.NotNull(ctrl);
|
||||
Assert.False(ctrl!.Transcript.Centered);
|
||||
|
|
@ -195,7 +195,7 @@ public class ChatWindowControllerTests
|
|||
var (rootInfo, layout, vm) = BuildTestTree();
|
||||
var bus = new CaptureBus();
|
||||
|
||||
var ctrl = ChatWindowController.Bind(rootInfo, layout, vm, () => bus, null, null, NoTex);
|
||||
var ctrl = ChatWindowController.Bind(rootInfo, layout, vm, () => bus, new ChatWindowState(), null, null, NoTex);
|
||||
|
||||
Assert.NotNull(ctrl);
|
||||
var bar = layout.FindElement(0x10000013u);
|
||||
|
|
@ -209,7 +209,7 @@ public class ChatWindowControllerTests
|
|||
var (rootInfo, layout, vm) = BuildTestTree();
|
||||
var bus = new CaptureBus();
|
||||
var ctrl = ChatWindowController.Bind(
|
||||
rootInfo, layout, vm, () => bus, null, null, NoTex)!;
|
||||
rootInfo, layout, vm, () => bus, new ChatWindowState(), null, null, NoTex)!;
|
||||
|
||||
vm.ShowSystemMessage("one wrapped transcript line");
|
||||
IReadOnlyList<UiText.Line> first = ctrl.Transcript.LinesProvider();
|
||||
|
|
@ -247,8 +247,14 @@ public class ChatWindowControllerTests
|
|||
var log = new ChatLog();
|
||||
var (rootInfo, layout, vm) = BuildTestTree(log);
|
||||
var bus = new CaptureBus();
|
||||
// 0x22 is above the default main filter's low dword (retail's
|
||||
// 0xFBFFFFFF only covers types 0-31) — accept everything so this
|
||||
// test isolates the color-carry-forward behavior it's actually
|
||||
// about from the (correctly) separate window-filter concern.
|
||||
var filters = new ChatWindowState();
|
||||
filters.SetFilter(ChatWindowState.MainWindowId, ulong.MaxValue);
|
||||
var ctrl = ChatWindowController.Bind(
|
||||
rootInfo, layout, vm, () => bus, null, null, NoTex)!;
|
||||
rootInfo, layout, vm, () => bus, filters, null, null, NoTex)!;
|
||||
|
||||
log.OnSystemMessage("first", chatType: 0x05u); // System, colorBrightPurple
|
||||
log.OnSystemMessage("middle", chatType: 0x22u); // out of range — carries 0x05's color
|
||||
|
|
@ -269,7 +275,7 @@ public class ChatWindowControllerTests
|
|||
var (rootInfo, layout, vm) = BuildTestTree();
|
||||
var bus = new CaptureBus();
|
||||
|
||||
var ctrl = ChatWindowController.Bind(rootInfo, layout, vm, () => bus, null, null, NoTex);
|
||||
var ctrl = ChatWindowController.Bind(rootInfo, layout, vm, () => bus, new ChatWindowState(), null, null, NoTex);
|
||||
|
||||
Assert.NotNull(ctrl);
|
||||
ctrl!.Input.OnSubmit!.Invoke("hello world");
|
||||
|
|
@ -286,7 +292,7 @@ public class ChatWindowControllerTests
|
|||
var (rootInfo, layout, vm) = BuildTestTree();
|
||||
var bus = new CaptureBus();
|
||||
|
||||
var ctrl = ChatWindowController.Bind(rootInfo, layout, vm, () => bus, null, null, NoTex);
|
||||
var ctrl = ChatWindowController.Bind(rootInfo, layout, vm, () => bus, new ChatWindowState(), null, null, NoTex);
|
||||
|
||||
Assert.NotNull(ctrl);
|
||||
ctrl!.Input.OnSubmit!.Invoke("/ls");
|
||||
|
|
@ -303,7 +309,7 @@ public class ChatWindowControllerTests
|
|||
var (rootInfo, layout, vm) = BuildTestTree();
|
||||
var bus = new CaptureBus();
|
||||
|
||||
var ctrl = ChatWindowController.Bind(rootInfo, layout, vm, () => bus, null, null, NoTex);
|
||||
var ctrl = ChatWindowController.Bind(rootInfo, layout, vm, () => bus, new ChatWindowState(), null, null, NoTex);
|
||||
|
||||
Assert.NotNull(ctrl);
|
||||
// Switch channel to General via the generic OnSelect (payload is ChatChannelKind).
|
||||
|
|
@ -328,7 +334,7 @@ public class ChatWindowControllerTests
|
|||
var vm = new ChatVM(new ChatLog());
|
||||
var bus = new CaptureBus();
|
||||
|
||||
var ctrl = ChatWindowController.Bind(root, layout, vm, () => bus, null, null, NoTex);
|
||||
var ctrl = ChatWindowController.Bind(root, layout, vm, () => bus, new ChatWindowState(), null, null, NoTex);
|
||||
|
||||
Assert.Null(ctrl);
|
||||
}
|
||||
|
|
@ -351,7 +357,7 @@ public class ChatWindowControllerTests
|
|||
// AnchorEdges fallback branch of the fix.
|
||||
var (rootInfo, layout, vm) = BuildTestTree();
|
||||
var bus = new CaptureBus();
|
||||
var ctrl = ChatWindowController.Bind(rootInfo, layout, vm, () => bus, null, null, NoTex);
|
||||
var ctrl = ChatWindowController.Bind(rootInfo, layout, vm, () => bus, new ChatWindowState(), null, null, NoTex);
|
||||
Assert.NotNull(ctrl);
|
||||
Assert.Null(ctrl!.Input.LayoutPolicy);
|
||||
Assert.Equal(AnchorEdges.Left | AnchorEdges.Right, ctrl.Input.Anchors & (AnchorEdges.Left | AnchorEdges.Right));
|
||||
|
|
@ -377,7 +383,7 @@ public class ChatWindowControllerTests
|
|||
{
|
||||
var (rootInfo, layout, vm) = BuildTestTree();
|
||||
var bus = new CaptureBus();
|
||||
var ctrl = ChatWindowController.Bind(rootInfo, layout, vm, () => bus, null, null, NoTex);
|
||||
var ctrl = ChatWindowController.Bind(rootInfo, layout, vm, () => bus, new ChatWindowState(), null, null, NoTex);
|
||||
Assert.NotNull(ctrl);
|
||||
|
||||
const float authoredParentWidth = 490f;
|
||||
|
|
@ -415,7 +421,7 @@ public class ChatWindowControllerTests
|
|||
// inputs (BuildTestTree already built one without them).
|
||||
layout = LayoutImporter.Build(rootInfo, NoTex, null);
|
||||
var bus = new CaptureBus();
|
||||
var ctrl = ChatWindowController.Bind(rootInfo, layout, vm, () => bus, null, null, NoTex);
|
||||
var ctrl = ChatWindowController.Bind(rootInfo, layout, vm, () => bus, new ChatWindowState(), null, null, NoTex);
|
||||
|
||||
Assert.NotNull(ctrl);
|
||||
Assert.NotNull(ctrl!.Input.LayoutPolicy);
|
||||
|
|
@ -439,76 +445,8 @@ public class ChatWindowControllerTests
|
|||
return null;
|
||||
}
|
||||
|
||||
// ── WrapText: Campaign CH user-gate round 1, item F ──────────────────────
|
||||
// /help (and "probably many places") never split on embedded '\n' — the
|
||||
// whole multi-line blob rode the single early-out as ONE line. Split on
|
||||
// '\n' first, then word-wrap each segment; a single-segment text keeps
|
||||
// the pre-existing early-out behavior exactly.
|
||||
|
||||
private static float MeasureByCharCount(string s) => s.Length;
|
||||
|
||||
[Fact]
|
||||
public void WrapText_EmbeddedNewlines_ProduceOneRenderedLinePerSegment()
|
||||
{
|
||||
string text = "line one\nline two\nline three";
|
||||
|
||||
// maxW is generous — every segment fits without word-wrapping, so
|
||||
// this isolates the newline-split behavior specifically.
|
||||
var lines = new List<string>(ChatWindowController.WrapText(text, 1000f, MeasureByCharCount));
|
||||
|
||||
Assert.Equal(new[] { "line one", "line two", "line three" }, lines);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WrapText_CarriageReturnNewline_NormalizesTheSameAsBareNewline()
|
||||
{
|
||||
string text = "line one\r\nline two";
|
||||
|
||||
var lines = new List<string>(ChatWindowController.WrapText(text, 1000f, MeasureByCharCount));
|
||||
|
||||
Assert.Equal(new[] { "line one", "line two" }, lines);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WrapText_SegmentLongerThanMaxWidth_StillWordWraps()
|
||||
{
|
||||
// Each segment is independently word-wrapped by the SAME algorithm
|
||||
// the single-line path always used — a multi-line server message
|
||||
// whose second line overflows the window still wraps that line.
|
||||
string text = "short\nthis segment is much too long to fit on one line";
|
||||
|
||||
var lines = new List<string>(ChatWindowController.WrapText(text, 10f, MeasureByCharCount));
|
||||
|
||||
Assert.Equal("short", lines[0]);
|
||||
Assert.True(lines.Count > 2, "the long second segment should have wrapped into multiple lines");
|
||||
Assert.All(lines, line => Assert.True(MeasureByCharCount(line) <= 10f));
|
||||
Assert.Equal(
|
||||
"this segment is much too long to fit on one line",
|
||||
string.Join(" ", lines.Skip(1)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WrapText_SingleSegmentText_KeepsTheEarlyOutBehavior()
|
||||
{
|
||||
// No '\n' at all — the pre-existing single-line early-out path
|
||||
// (whole text fits => returned verbatim as one fragment) is
|
||||
// unchanged.
|
||||
string text = "no newlines here";
|
||||
|
||||
var lines = new List<string>(ChatWindowController.WrapText(text, 1000f, MeasureByCharCount));
|
||||
|
||||
Assert.Equal(new[] { text }, lines);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WrapText_ConsecutiveNewlines_ProduceABlankLine()
|
||||
{
|
||||
string text = "first\n\nthird";
|
||||
|
||||
var lines = new List<string>(ChatWindowController.WrapText(text, 1000f, MeasureByCharCount));
|
||||
|
||||
Assert.Equal(new[] { "first", "", "third" }, lines);
|
||||
}
|
||||
// WrapText moved to ChatTranscriptRendererTests.cs (CH6a/b REJECT-review
|
||||
// NIT 5 — WrapText itself moved from this class to ChatTranscriptRenderer).
|
||||
|
||||
// ── SetIndicatorOpen: Campaign CH slice CH6b — the button mirror ─────────
|
||||
// gmMainChatUI::RecvNotice_SetPanelVisibility @0x004CCD80: State 6
|
||||
|
|
@ -524,7 +462,7 @@ public class ChatWindowControllerTests
|
|||
{
|
||||
var (rootInfo, layout, vm) = BuildTestTree();
|
||||
var bus = new CaptureBus();
|
||||
var ctrl = ChatWindowController.Bind(rootInfo, layout, vm, () => bus, null, null, NoTex)!;
|
||||
var ctrl = ChatWindowController.Bind(rootInfo, layout, vm, () => bus, new ChatWindowState(), null, null, NoTex)!;
|
||||
var indicator = Assert.IsType<UiButton>(layout.FindElement(indicatorId));
|
||||
|
||||
ctrl.SetIndicatorOpen(windowId, open: true);
|
||||
|
|
@ -537,7 +475,7 @@ public class ChatWindowControllerTests
|
|||
{
|
||||
var (rootInfo, layout, vm) = BuildTestTree();
|
||||
var bus = new CaptureBus();
|
||||
var ctrl = ChatWindowController.Bind(rootInfo, layout, vm, () => bus, null, null, NoTex)!;
|
||||
var ctrl = ChatWindowController.Bind(rootInfo, layout, vm, () => bus, new ChatWindowState(), null, null, NoTex)!;
|
||||
var indicator = Assert.IsType<UiButton>(layout.FindElement(0x10000522u));
|
||||
|
||||
ctrl.SetIndicatorOpen(1, open: true);
|
||||
|
|
@ -551,7 +489,7 @@ public class ChatWindowControllerTests
|
|||
{
|
||||
var (rootInfo, layout, vm) = BuildTestTree();
|
||||
var bus = new CaptureBus();
|
||||
var ctrl = ChatWindowController.Bind(rootInfo, layout, vm, () => bus, null, null, NoTex)!;
|
||||
var ctrl = ChatWindowController.Bind(rootInfo, layout, vm, () => bus, new ChatWindowState(), null, null, NoTex)!;
|
||||
var indicator2 = Assert.IsType<UiButton>(layout.FindElement(0x10000523u));
|
||||
|
||||
ctrl.SetIndicatorOpen(1, open: true);
|
||||
|
|
@ -566,7 +504,7 @@ public class ChatWindowControllerTests
|
|||
{
|
||||
var (rootInfo, layout, vm) = BuildTestTree();
|
||||
var bus = new CaptureBus();
|
||||
var ctrl = ChatWindowController.Bind(rootInfo, layout, vm, () => bus, null, null, NoTex)!;
|
||||
var ctrl = ChatWindowController.Bind(rootInfo, layout, vm, () => bus, new ChatWindowState(), null, null, NoTex)!;
|
||||
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => ctrl.SetIndicatorOpen(windowId, open: true));
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue