diff --git a/src/AcDream.App/UI/Layout/ChatWindowController.cs b/src/AcDream.App/UI/Layout/ChatWindowController.cs
index d3b33019..5b6199db 100644
--- a/src/AcDream.App/UI/Layout/ChatWindowController.cs
+++ b/src/AcDream.App/UI/Layout/ChatWindowController.cs
@@ -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
///
/// Greedy word-wrap: split into fragments that each fit in
/// pixels (per ), 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).
///
public static IEnumerable WrapText(string text, float maxW, Func 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();
diff --git a/src/AcDream.App/UI/UiChatView.cs b/src/AcDream.App/UI/UiChatView.cs
index 9dbe9cd3..cff1ea6c 100644
--- a/src/AcDream.App/UI/UiChatView.cs
+++ b/src/AcDream.App/UI/UiChatView.cs
@@ -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);
}
}