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); } }