diff --git a/src/AcDream.UI.Abstractions/IPanelRenderer.cs b/src/AcDream.UI.Abstractions/IPanelRenderer.cs
index 3656d75..70696a2 100644
--- a/src/AcDream.UI.Abstractions/IPanelRenderer.cs
+++ b/src/AcDream.UI.Abstractions/IPanelRenderer.cs
@@ -262,4 +262,21 @@ public interface IPanelRenderer
/// Close the tab opened by .
void EndTabItem();
+
+ ///
+ /// Render a read-only multi-line text region the user can
+ /// select with click+drag and copy with Ctrl+C.
+ /// Matches the typical "click into a textbox to grab text" UX —
+ /// chat panels, log viewers, etc. use this to make text
+ /// extractable without the user having to alt-tab + retype.
+ ///
+ ///
+ /// The widget is sized to ; pass
+ /// (0, 0) for "fill the current content region" semantics
+ /// (matches ImGui defaults). is the ImGui
+ /// stable identifier — typically "##chatcopy" or similar
+ /// hidden-label form.
+ ///
+ ///
+ void TextMultilineReadOnly(string id, string content, Vector2 size);
}
diff --git a/src/AcDream.UI.Abstractions/Panels/Chat/ChatPanel.cs b/src/AcDream.UI.Abstractions/Panels/Chat/ChatPanel.cs
index f3a4a07..28e035b 100644
--- a/src/AcDream.UI.Abstractions/Panels/Chat/ChatPanel.cs
+++ b/src/AcDream.UI.Abstractions/Panels/Chat/ChatPanel.cs
@@ -1,3 +1,4 @@
+using System.Linq;
using System.Numerics;
using AcDream.Core.Chat;
using AcDream.Core.Combat;
@@ -50,6 +51,13 @@ public sealed class ChatPanel : IPanel
// click into another widget.
private bool _focusRequested;
+ // L.0 follow-up: "Copy mode" — when true, render the chat tail as
+ // a read-only multi-line text widget the user can click+drag to
+ // select + Ctrl+C to copy. Trades per-line color for selectability;
+ // user toggles when they want to grab specific text out of the
+ // log (item names, coordinates, NPC dialogue, etc).
+ private bool _copyMode;
+
public ChatPanel(ChatVM vm)
{
_vm = vm ?? throw new ArgumentNullException(nameof(vm));
@@ -82,6 +90,17 @@ public sealed class ChatPanel : IPanel
return;
}
+ // L.0 follow-up: top-of-panel "Copy mode" toggle. When on, the
+ // chat tail rendering swaps to TextMultilineReadOnly so the
+ // user can mark + Ctrl+C any text. Off (default) preserves the
+ // colored per-line render with combat highlights. The checkbox
+ // sits ABOVE the chat tail (not in the footer) so it's always
+ // visible regardless of scroll position.
+ bool copyMode = _copyMode;
+ if (renderer.Checkbox("Copy mode (select text to Ctrl+C)", ref copyMode))
+ _copyMode = copyMode;
+ renderer.Separator();
+
// Phase J Tier 3: keep the input field at the bottom of the
// window across resizes by reserving footer space and putting
// the chat tail in a scrollable child that fills the rest.
@@ -95,7 +114,21 @@ public sealed class ChatPanel : IPanel
// the plain Text path (visually identical to the I.4 panel).
var lines = _vm.RecentLinesDetailed();
- if (renderer.BeginChild("##chattail", new System.Numerics.Vector2(0, -footerHeight)))
+ if (_copyMode)
+ {
+ // Copy mode: one big read-only multiline text widget
+ // holding every visible line, joined with newlines. Loses
+ // per-line color but lets the user click+drag to select
+ // arbitrary spans of text + Ctrl+C to copy. Sized to fill
+ // the available space minus the footer.
+ string joined = lines.Count == 0
+ ? "(no messages yet)"
+ : string.Join("\n", lines.Select(l => l.Text));
+ renderer.TextMultilineReadOnly(
+ "##chattailcopy", joined,
+ new System.Numerics.Vector2(0f, -footerHeight));
+ }
+ else if (renderer.BeginChild("##chattail", new System.Numerics.Vector2(0, -footerHeight)))
{
if (lines.Count == 0)
{
@@ -127,7 +160,7 @@ public sealed class ChatPanel : IPanel
}
_lastRenderedCount = lines.Count;
}
- renderer.EndChild();
+ if (!_copyMode) renderer.EndChild();
// Phase I.4: input field. Backend implementation clears _input
// on submit per the IPanelRenderer contract.
diff --git a/src/AcDream.UI.ImGui/ImGuiPanelRenderer.cs b/src/AcDream.UI.ImGui/ImGuiPanelRenderer.cs
index d5eb978..4396874 100644
--- a/src/AcDream.UI.ImGui/ImGuiPanelRenderer.cs
+++ b/src/AcDream.UI.ImGui/ImGuiPanelRenderer.cs
@@ -207,4 +207,21 @@ public sealed class ImGuiPanelRenderer : IPanelRenderer
///
public void EndTabItem() => ImGuiNET.ImGui.EndTabItem();
+
+ // -- Selectable / copyable text ---------------------------------------
+
+ ///
+ public void TextMultilineReadOnly(string id, string content, Vector2 size)
+ {
+ // ImGui's InputTextMultiline takes a `ref string` even with the
+ // ReadOnly flag — we just hand it a local copy. maxLength caps
+ // what the user could type if ReadOnly were ever cleared; we
+ // size it to the current content (+1 for ImGui's internal NUL
+ // terminator in some bindings). Min of 1 keeps the empty case
+ // from confusing native bindings.
+ string buffer = content;
+ uint maxLen = (uint)System.Math.Max(content.Length + 1, 1);
+ ImGuiNET.ImGui.InputTextMultiline(id, ref buffer, maxLen, size,
+ ImGuiInputTextFlags.ReadOnly);
+ }
}
diff --git a/tests/AcDream.UI.Abstractions.Tests/FakePanelRenderer.cs b/tests/AcDream.UI.Abstractions.Tests/FakePanelRenderer.cs
index 3706188..9df8b12 100644
--- a/tests/AcDream.UI.Abstractions.Tests/FakePanelRenderer.cs
+++ b/tests/AcDream.UI.Abstractions.Tests/FakePanelRenderer.cs
@@ -231,4 +231,7 @@ internal sealed class FakePanelRenderer : IPanelRenderer
}
public void EndTabItem() => Calls.Add(("EndTabItem", Array.Empty