feat(D.2b): chat wiring — menu/input sprites, button reflow, char-wrap, panel wash fix

- ChatWindowController: wires the menu chrome (popup bevel, row/checkbox
  sprites), the input focused-field sprite + keyboard, and autosizes the channel
  button + reflows the input field to start after it (anchor re-capture so the
  per-frame layout doesn't fight it). DefaultTextInput / write-mode focus hooked
  up.
- WrapText now breaks an over-long UNBROKEN token at character boundaries (no
  hyphen), packed onto the current line first — so a spaceless token wraps
  instead of overflowing, and a "You say," prefix stays on the same row as the
  start of the message.
- UiChatView: transcript background + selection highlight use DrawFill (sprite
  bucket) so the transcript text draws ON TOP instead of being dimmed by its own
  translucent rect background.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-16 15:24:30 +02:00
parent 2284a376ae
commit ce848c154d
2 changed files with 58 additions and 11 deletions

View file

@ -49,6 +49,9 @@ public sealed class ChatWindowController
private const uint UpSprite = 0x06004C6Cu; // up arrow (top button)
private const uint DownSprite = 0x06004C69u; // down arrow (bottom button)
// Chat input focused-field background (element 0x10000016 Normal_focussed state).
private const uint InputFocusField = 0x060011ABu; // gold "lit" field when in write mode
// Channel menu sprite ids (confirmed in chat element dump).
private const uint MenuNormal = 0x06004D65u; // button face
private const uint MenuPressed = 0x06004D66u; // button pressed
@ -187,6 +190,8 @@ public sealed class ChatWindowController
Anchors = ElementReader.ToAnchors(iInfo.Left, iInfo.Top, iInfo.Right, iInfo.Bottom),
DatFont = datFont,
Font = debugFont,
SpriteResolve = resolve,
FocusFieldSprite = InputFocusField,
};
inputBar.AddChild(c.Input);
c.Input.OnSubmit = text => ChatCommandRouter.Submit(text, vm, busProvider(), c._activeChannel);
@ -257,6 +262,28 @@ public sealed class ChatWindowController
sendEl.LabelColor = new Vector4(1f, 0.92f, 0.72f, 1f);
}
// ── Size the channel button to its label + reflow the input field ─
// Retail's talk-focus button autosizes to the selected channel name; the input
// field then fills the gap from the button's right edge to the Send button. The
// dat authors the button at a fixed 46px (too narrow for "Chat" once the LED +
// arrow are accounted for), so widen it to its content and shift the input.
// Recompute on every channel change (the button grows/shrinks with the label).
if (c.Menu is not null)
{
float inputRight = c.Input.Left + c.Input.Width; // == Send button's left edge
void ReflowInputRow()
{
c.Menu.Width = System.MathF.Round(c.Menu.NaturalButtonWidth());
c.Menu.ResetAnchorCapture();
c.Input.Left = c.Menu.Left + c.Menu.Width;
c.Input.Width = System.MathF.Max(40f, inputRight - c.Input.Left);
c.Input.ResetAnchorCapture();
}
var onChanged = c.Menu.OnChannelChanged;
c.Menu.OnChannelChanged = k => { onChanged?.Invoke(k); ReflowInputRow(); };
ReflowInputRow();
}
// ── Max/min toggle — simplified gmMainChatUI::HandleMaximizeButton ──
if (layout.FindElement(MaxMinId) is UiDatElement maxMinEl)
{
@ -349,33 +376,47 @@ public sealed class ChatWindowController
/// <summary>
/// Greedy word-wrap: split <paramref name="text"/> into fragments that each fit in
/// <paramref name="maxW"/> pixels (per <paramref name="measure"/>), breaking at spaces.
/// A single word longer than the width overflows its own line (retail does not
/// hyphenate chat). Mirrors retail GlyphList::Recalculate's per-GlyphLine emission.
/// A word that is itself wider than the line is broken at CHARACTER boundaries (no
/// hyphen), packed onto the current line first — so a long unbroken token (e.g. a URL
/// or "wwwww…") wraps instead of overflowing, and a "You say," prefix stays on the same
/// row as the start of the message. Mirrors retail GlyphList::Recalculate's per-GlyphLine
/// emission (which breaks mid-glyph-run when a run exceeds the wrap width).
/// </summary>
public static IEnumerable<string> WrapText(string text, float maxW, Func<string, float> measure)
{
if (string.IsNullOrEmpty(text) || maxW <= 0f || measure(text) <= maxW)
{
yield return text;
yield return text ?? string.Empty;
yield break;
}
var line = new System.Text.StringBuilder();
foreach (var word in text.Split(' '))
{
if (line.Length == 0)
string sep = line.Length > 0 ? " " : string.Empty;
if (measure(line.ToString() + sep + word) <= maxW)
{
line.Append(word);
line.Append(sep).Append(word); // fits on the current line
continue;
}
else if (measure(line.ToString() + " " + word) > maxW)
if (line.Length > 0 && measure(word) <= maxW)
{
yield return line.ToString();
yield return line.ToString(); // word fits alone → push to a new line
line.Clear();
line.Append(word);
continue;
}
else
// Word too long for any single line: char-wrap it, packing onto the current
// line's remaining space first (keeps the prefix with the message start).
if (line.Length > 0) line.Append(' ');
foreach (char ch in word)
{
line.Append(' ').Append(word);
if (line.Length > 0 && measure(line.ToString() + ch) > maxW)
{
yield return line.ToString();
line.Clear();
}
line.Append(ch);
}
}
if (line.Length > 0) yield return line.ToString();

View file

@ -93,7 +93,11 @@ public sealed class UiChatView : UiElement
protected override void OnDraw(UiRenderContext ctx)
{
ctx.DrawRect(0, 0, Width, Height, BackgroundColor);
// Background must draw UNDER the transcript text. DrawStringDat emits into the
// sprite bucket which flushes BEFORE rects, so a DrawRect background would wash
// over the text. DrawFill routes the background through the sprite bucket too,
// submitted first → text on top.
ctx.DrawFill(0, 0, Width, Height, BackgroundColor);
// Prefer the retail dat font when set; fall back to BitmapFont.
var datFont = DatFont;
@ -161,7 +165,9 @@ public sealed class UiChatView : UiElement
hx = Padding + bitmapFont!.MeasureWidth(text.Substring(0, c0));
hw = bitmapFont.MeasureWidth(text.Substring(c0, c1 - c0));
}
ctx.DrawRect(hx, y, hw, lh, SelectionColor);
// Highlight sits BEHIND the line's text → sprite bucket, submitted
// before this line's DrawStringDat.
ctx.DrawFill(hx, y, hw, lh, SelectionColor);
}
}