feat(ui): chat Copy mode — select + Ctrl+C any text in the chat tail

User reported wanting to mark text in-game and copy it out (item names,
coordinates, NPC dialogue, etc). ImGui doesn't natively let you select
across multiple TextColored widgets, but a read-only multi-line
InputText is fully click-drag selectable + Ctrl+C copyable. This
commit adds a "Copy mode" toggle to ChatPanel that swaps the chat
tail's render path between the colored-line view and a single
selectable text region.

New IPanelRenderer primitive:

  void TextMultilineReadOnly(string id, string content, Vector2 size);

ImGui maps this to InputTextMultiline with the ReadOnly flag — same
selection + Ctrl+C UX a user expects from any text-input widget.
FakePanelRenderer records the call for tests. The future D.2b
custom retail-look backend implements its own equivalent (likely
the same widget pattern with retail font/skin).

ChatPanel rendering:

  · A "Copy mode (select text to Ctrl+C)" Checkbox at the top of
    the panel toggles _copyMode.
  · Off (default) — current per-line render with colored combat
    entries. Visually unchanged from before.
  · On — the chat tail becomes a single TextMultilineReadOnly
    widget holding every visible line joined with newlines. Loses
    per-line color, gains arbitrary-span text selection.
  · Footer (separator + input field) renders identically in both
    modes so the user can still type while in copy mode.

Existing ChatPanelLayoutTests's footer-separator probe was using
IndexOf("Separator") — which now matches the new pre-tail separator
between the Checkbox and the chat tail. Switched to LastIndexOf
which still pins the footer separator (between EndChild and
InputTextSubmit). Behaviour and intent unchanged.

DisplaySettingsTests' With_expression test was still asserting the
old "1920x1080" Default.Resolution; updated to the new "1280x720"
that the previous wire-up commit introduced (the earlier commit
forgot this one).

dotnet build green (0 warnings); dotnet test 1,309 / 1,309 green
(243 Core.Net + 393 UI.Abstractions + 673 Core).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-26 21:45:39 +02:00
parent fc1e1933aa
commit 4c75ced92b
6 changed files with 87 additions and 9 deletions

View file

@ -231,4 +231,7 @@ internal sealed class FakePanelRenderer : IPanelRenderer
}
public void EndTabItem() => Calls.Add(("EndTabItem", Array.Empty<object?>()));
public void TextMultilineReadOnly(string id, string content, Vector2 size)
=> Calls.Add(("TextMultilineReadOnly", new object?[] { id, content, size }));
}

View file

@ -33,7 +33,11 @@ public sealed class ChatPanelLayoutTests
int beginIdx = methods.IndexOf("Begin");
int beginChildIdx = methods.IndexOf("BeginChild");
int endChildIdx = methods.IndexOf("EndChild");
int separatorIdx = methods.IndexOf("Separator");
// L.0 follow-up: Copy-mode toggle adds a Separator above the
// chat tail, so multiple Separators now exist. The footer
// separator (the one we care about for input layout) is the
// LAST one — between EndChild and the input field.
int separatorIdx = methods.LastIndexOf("Separator");
int inputSubmitIdx = methods.IndexOf("InputTextSubmit");
int endIdx = methods.IndexOf("End");

View file

@ -13,12 +13,16 @@ public sealed class DisplaySettingsTests
[Fact]
public void Default_values_match_pre_L0_runtime_state()
{
// Defaults pinned to match the camera FovY (60° = π/3) and the
// pre-L.0 window options (VSync off, FPS in title bar). Opening
// Display + Save without touching anything must NOT change the
// user's visual experience.
// Defaults pinned to match the actual pre-L.0 startup state:
// · Resolution matches WindowOptions (1280×720 in GameWindow.Run)
// · FieldOfView matches camera FovY (60° = π/3)
// · VSync matches WindowOptions (false during dev)
// · ShowFps true preserves the perf string in the title bar
// Net effect: opening Display + Save with no edits is a visual
// no-op (no window resize, no camera FovY change, no title
// bar change).
var d = DisplaySettings.Default;
Assert.Equal("1920x1080", d.Resolution);
Assert.Equal("1280x720", d.Resolution);
Assert.False(d.Fullscreen);
Assert.False(d.VSync);
Assert.Equal(60f, d.FieldOfView);
@ -59,7 +63,7 @@ public sealed class DisplaySettingsTests
var d = DisplaySettings.Default with { FieldOfView = 90f };
Assert.Equal(90f, d.FieldOfView);
// Other fields untouched.
Assert.Equal("1920x1080", d.Resolution);
Assert.Equal("1280x720", d.Resolution);
Assert.False(d.VSync);
Assert.True(d.ShowFps);
}