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:
parent
2284a376ae
commit
ce848c154d
2 changed files with 58 additions and 11 deletions
|
|
@ -49,6 +49,9 @@ public sealed class ChatWindowController
|
||||||
private const uint UpSprite = 0x06004C6Cu; // up arrow (top button)
|
private const uint UpSprite = 0x06004C6Cu; // up arrow (top button)
|
||||||
private const uint DownSprite = 0x06004C69u; // down arrow (bottom 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).
|
// Channel menu sprite ids (confirmed in chat element dump).
|
||||||
private const uint MenuNormal = 0x06004D65u; // button face
|
private const uint MenuNormal = 0x06004D65u; // button face
|
||||||
private const uint MenuPressed = 0x06004D66u; // button pressed
|
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),
|
Anchors = ElementReader.ToAnchors(iInfo.Left, iInfo.Top, iInfo.Right, iInfo.Bottom),
|
||||||
DatFont = datFont,
|
DatFont = datFont,
|
||||||
Font = debugFont,
|
Font = debugFont,
|
||||||
|
SpriteResolve = resolve,
|
||||||
|
FocusFieldSprite = InputFocusField,
|
||||||
};
|
};
|
||||||
inputBar.AddChild(c.Input);
|
inputBar.AddChild(c.Input);
|
||||||
c.Input.OnSubmit = text => ChatCommandRouter.Submit(text, vm, busProvider(), c._activeChannel);
|
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);
|
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 ──
|
// ── Max/min toggle — simplified gmMainChatUI::HandleMaximizeButton ──
|
||||||
if (layout.FindElement(MaxMinId) is UiDatElement maxMinEl)
|
if (layout.FindElement(MaxMinId) is UiDatElement maxMinEl)
|
||||||
{
|
{
|
||||||
|
|
@ -349,33 +376,47 @@ public sealed class ChatWindowController
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Greedy word-wrap: split <paramref name="text"/> into fragments that each fit in
|
/// Greedy word-wrap: split <paramref name="text"/> into fragments that each fit in
|
||||||
/// <paramref name="maxW"/> pixels (per <paramref name="measure"/>), breaking at spaces.
|
/// <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
|
/// A word that is itself wider than the line is broken at CHARACTER boundaries (no
|
||||||
/// hyphenate chat). Mirrors retail GlyphList::Recalculate's per-GlyphLine emission.
|
/// 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>
|
/// </summary>
|
||||||
public static IEnumerable<string> WrapText(string text, float maxW, Func<string, float> measure)
|
public static IEnumerable<string> WrapText(string text, float maxW, Func<string, float> measure)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(text) || maxW <= 0f || measure(text) <= maxW)
|
if (string.IsNullOrEmpty(text) || maxW <= 0f || measure(text) <= maxW)
|
||||||
{
|
{
|
||||||
yield return text;
|
yield return text ?? string.Empty;
|
||||||
yield break;
|
yield break;
|
||||||
}
|
}
|
||||||
|
|
||||||
var line = new System.Text.StringBuilder();
|
var line = new System.Text.StringBuilder();
|
||||||
foreach (var word in text.Split(' '))
|
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.Clear();
|
||||||
line.Append(word);
|
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();
|
if (line.Length > 0) yield return line.ToString();
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,11 @@ public sealed class UiChatView : UiElement
|
||||||
|
|
||||||
protected override void OnDraw(UiRenderContext ctx)
|
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.
|
// Prefer the retail dat font when set; fall back to BitmapFont.
|
||||||
var datFont = DatFont;
|
var datFont = DatFont;
|
||||||
|
|
@ -161,7 +165,9 @@ public sealed class UiChatView : UiElement
|
||||||
hx = Padding + bitmapFont!.MeasureWidth(text.Substring(0, c0));
|
hx = Padding + bitmapFont!.MeasureWidth(text.Substring(0, c0));
|
||||||
hw = bitmapFont.MeasureWidth(text.Substring(c0, c1 - 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue