feat(D.2b): scrollable retail chat window (read-only foundation)

Add UiChatView, a transcript widget for the retail-look UI: renders the
ChatVM tail bottom-pinned (newest at the bottom, like retail) with
mouse-wheel scrollback and whole-line vertical clipping so text stays
inside the frame. Hosted in a draggable/resizable UiNineSlicePanel and
wired into the UiHost next to the vitals window, fed by a dedicated
ChatVM (200-line tail) over the same live ChatLog. Per-ChatKind colour
palette (speech white, tells magenta, channels blue, system yellow,
emotes grey, combat orange).

This is the read-only foundation. The next sub-step adds glScissor
clipping + word-wrap, drag-to-select, and Ctrl+C copy -- the last needs
a CapturesPointerDrag opt-out on UiElement so an interior drag selects
text instead of moving the window (today an interior drag still moves
the window, same as the vitals panel).

Tests: UiChatView.ClampScroll (pin-to-bottom, cap-at-overflow,
never-negative).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-14 22:12:12 +02:00
parent 1453ff7da2
commit ada863980c
3 changed files with 169 additions and 0 deletions

View file

@ -1773,6 +1773,56 @@ public sealed class GameWindow : IDisposable
_uiHost.Root.AddChild(panel);
Console.WriteLine("[D.2b] retail UI active — vitals panel from vitals.xml markup.");
// Retail chat window — a draggable/resizable nine-slice frame hosting a
// scrollable transcript (UiChatView). Read-only + wheel-scroll for now;
// drag-select + Ctrl+C copy land in the next D.2b sub-step. A dedicated
// ChatVM with a deeper tail (200) feeds the scrollback; it shares the
// same live ChatLog (Chat) as the ImGui panel.
var retailChatVm = new AcDream.UI.Abstractions.Panels.Chat.ChatVM(Chat, displayLimit: 200);
var chatWindow = new AcDream.App.UI.UiNineSlicePanel(ResolveChrome)
{
Left = 10, Top = 432, Width = 440, Height = 184,
MinWidth = 180, MinHeight = 80,
};
var chatView = new AcDream.App.UI.UiChatView
{
Left = 8, Top = 8, Width = 424, Height = 168,
Anchors = AcDream.App.UI.AnchorEdges.Left | AcDream.App.UI.AnchorEdges.Top
| AcDream.App.UI.AnchorEdges.Right | AcDream.App.UI.AnchorEdges.Bottom,
Font = _debugFont,
LinesProvider = () => BuildRetailChatLines(retailChatVm),
};
chatWindow.AddChild(chatView);
_uiHost.Root.AddChild(chatWindow);
// Map the VM's formatted tail into coloured view lines. Per-ChatKind
// palette (retail-ish): speech white, tells magenta, channels blue,
// system yellow, emotes grey, combat orange. Refined later if needed.
static System.Collections.Generic.IReadOnlyList<AcDream.App.UI.UiChatView.Line> BuildRetailChatLines(
AcDream.UI.Abstractions.Panels.Chat.ChatVM vm)
{
var detailed = vm.RecentLinesDetailed();
var result = new AcDream.App.UI.UiChatView.Line[detailed.Count];
for (int i = 0; i < detailed.Count; i++)
result[i] = new AcDream.App.UI.UiChatView.Line(
detailed[i].Text, RetailChatColor(detailed[i].Kind));
return result;
}
static System.Numerics.Vector4 RetailChatColor(AcDream.Core.Chat.ChatKind kind) => kind switch
{
AcDream.Core.Chat.ChatKind.LocalSpeech => new(1f, 1f, 1f, 1f),
AcDream.Core.Chat.ChatKind.RangedSpeech => new(1f, 0.95f, 0.8f, 1f),
AcDream.Core.Chat.ChatKind.Channel => new(0.6f, 0.8f, 1f, 1f),
AcDream.Core.Chat.ChatKind.Tell => new(1f, 0.5f, 1f, 1f),
AcDream.Core.Chat.ChatKind.System => new(1f, 1f, 0.45f, 1f),
AcDream.Core.Chat.ChatKind.Popup => new(1f, 0.85f, 0.4f, 1f),
AcDream.Core.Chat.ChatKind.Emote => new(0.8f, 0.8f, 0.7f, 1f),
AcDream.Core.Chat.ChatKind.SoulEmote => new(0.8f, 0.8f, 0.7f, 1f),
AcDream.Core.Chat.ChatKind.Combat => new(1f, 0.6f, 0.25f, 1f),
_ => new(0.9f, 0.9f, 0.9f, 1f),
};
// Drain plugin-registered markup panels (buffered before the GL
// window opened) into the same UiRoot tree. A faulty plugin markup
// file is isolated — logged + skipped, never crashes the client.