feat(ui): ChatPanel — second devtools panel proves the abstraction

Adds a second real panel behind ACDREAM_DEVTOOLS=1. Shows the tail
of ChatLog (last 20 entries by default) formatted per ChatKind:

  "Caith: hello"                  — LocalSpeech
  "Regal says distantly: hi"      — RangedSpeech
  "[ch 7] Caith: g'day"           — Channel
  "[Tell] Regal: psst"            — Tell
  "[System] Your spell fizzled!"  — System
  "[Popup] A door stands..."      — Popup

Why now: proves the D.2a IPanelRenderer contract survives beyond a
single progress-bar panel. ChatPanel exercises Text() + Separator()
on a variable-length list where VitalsPanel was a fixed three-widget
layout. No renderer primitives needed to grow — the contract held,
which is the whole point of the abstraction layer.

Files:
  - src/AcDream.UI.Abstractions/Panels/Chat/ChatVM.cs (new)
      Snapshots ChatLog tail every frame. Cheap at default 500-entry
      cap. Per-kind formatting lives here (not in the panel) so the
      D.2b retail-look swap inherits plain-text fallbacks.
  - src/AcDream.UI.Abstractions/Panels/Chat/ChatPanel.cs (new)
      IPanel implementation. Separator + N Text lines. "(no messages
      yet)" fallback when the log is empty.
  - src/AcDream.App/Rendering/GameWindow.cs
      Registers the ChatPanel alongside VitalsPanel in the devtools
      init block. Uses the existing GameWindow.Chat field already
      fed by H.1's wire layer + GameEventWiring.WireAll.
  - tests/AcDream.UI.Abstractions.Tests/ChatVMTests.cs (new)
      12 tests covering tail selection, display-limit bounds, every
      ChatKind's formatting, null-log + zero-limit guards, no stale
      caching across appends.

Also fixes one stale "Hexa.NET.ImGui" mention in VitalsPanel's xmldoc
(pivoted to ImGui.NET in 55aaca7; doc needed a trailing update).

Build: 0 warnings, 0 errors. Tests: 23 UI.Abstractions (up from 11,
all Core + Core.Net still green), 0 failures.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-25 00:48:07 +02:00
parent 4d1b8b8aee
commit 9faf9d7e3a
5 changed files with 273 additions and 2 deletions

View file

@ -0,0 +1,64 @@
namespace AcDream.UI.Abstractions.Panels.Chat;
/// <summary>
/// Second real UI panel — shows the tail of the chat log.
/// Exercises <see cref="IPanelRenderer.Text"/> + <see cref="IPanelRenderer.Separator"/>
/// on a non-trivial render pattern (N lines, not a single widget) —
/// proving the D.2a abstraction contract holds for more than the vitals
/// HUD before we grow the panel catalog further.
///
/// <para>
/// D.2a scope: show the last <see cref="ChatVM.DefaultDisplayLimit"/>
/// lines with a separator above the tail and each entry as a single
/// <c>Text</c> call. No input field (outbound chat already has wire
/// support via <c>SendChat</c> — a text-input widget on <see cref="IPanelRenderer"/>
/// lands with the first panel that actually needs one, not here).
/// </para>
/// </summary>
public sealed class ChatPanel : IPanel
{
private readonly ChatVM _vm;
public ChatPanel(ChatVM vm)
{
_vm = vm ?? throw new ArgumentNullException(nameof(vm));
}
/// <inheritdoc />
public string Id => "acdream.chat";
/// <inheritdoc />
public string Title => "Chat";
/// <inheritdoc />
public bool IsVisible { get; set; } = true;
/// <inheritdoc />
public void Render(PanelContext ctx, IPanelRenderer renderer)
{
if (!renderer.Begin(Title))
{
renderer.End();
return;
}
var lines = _vm.RecentLines();
if (lines.Count == 0)
{
renderer.Text("(no messages yet)");
}
else
{
// Header separator so the reader always sees the tail start;
// the IPanel contract promises pressing Begin opens the window
// at a stable anchor.
renderer.Separator();
for (int i = 0; i < lines.Count; i++)
{
renderer.Text(lines[i]);
}
}
renderer.End();
}
}