@
feat(D.2b): chat polish — typing fix, opacity, scrollbar 3-slice, retail channel menu Visual-iteration batch (decomp-grounded), each fix verified against the retail screenshots: - typing: UiElement.HitTest aborted on ClickThrough BEFORE walking children, so the ClickThrough UiDatElement panels blocked hit-testing to the input/transcript inside them. Check ClickThrough AFTER the child walk (it only gates whether THIS element claims the hit). Restores input focus + typing. - opacity: UiElement.Opacity + a UiRenderContext alpha stack applied to sprite/rect draws (text bypasses it, stays sharp); chat frame Opacity=0.75 → translucent chat. - brown sliver: grow the transcript panel up 9px to cover the dropped resize-bar strip. - scrollbar: real 3-slice thumb (caps 0x06004C60/66 + tiled mid) + tiled track. - max/min: shifted one button-width left of the scrollbar (dat right-anchors collide). - system text now green (retail ChatMessageType 5; was yellow). - word-wrap: transcript lines wrap to the panel width (greedy, ports GlyphList::Recalculate). - channel menu reworked to retail gmMainChatUI::InitTalkFocusMenu: "Chat" button + a TWO-COLUMN popup of the 14 talk-focus items (Squelch, Tell to Selected, Chat to All, Tell to Fellows, ...) on a tan panel; channel items set the active outbound channel. Build + 392 App tests green. Visual confirmation in progress. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> @
This commit is contained in:
parent
0ec36f6197
commit
1da697ec2a
7 changed files with 329 additions and 126 deletions
|
|
@ -31,6 +31,7 @@ public sealed class ChatWindowController
|
|||
|
||||
// Element ids from chat LayoutDesc 0x21000006 (confirmed in Task D/G1).
|
||||
private const uint RootId = 0x1000000Eu;
|
||||
private const uint ResizeBarId = 0x1000000Fu; // dat top resize bar (800px — dropped; nine-slice grips replace it)
|
||||
private const uint TranscriptPanelId = 0x10000010u;
|
||||
private const uint TranscriptId = 0x10000011u; // Type-12 prototype — skipped by factory
|
||||
private const uint TrackId = 0x10000012u;
|
||||
|
|
@ -41,10 +42,12 @@ public sealed class ChatWindowController
|
|||
private const uint MaxMinId = 0x1000046Fu;
|
||||
|
||||
// Scrollbar sprite ids from base layout 0x2100003E (confirmed in Task D).
|
||||
private const uint TrackSprite = 0x06004C5Fu;
|
||||
private const uint ThumbSprite = 0x06004C63u;
|
||||
private const uint UpSprite = 0x06004C69u;
|
||||
private const uint DownSprite = 0x06004C6Cu;
|
||||
private const uint TrackSprite = 0x06004C5Fu;
|
||||
private const uint ThumbSprite = 0x06004C63u; // 3-slice middle tile
|
||||
private const uint ThumbTopSprite = 0x06004C60u; // 3-slice top cap
|
||||
private const uint ThumbBotSprite = 0x06004C66u; // 3-slice bottom cap
|
||||
private const uint UpSprite = 0x06004C69u;
|
||||
private const uint DownSprite = 0x06004C6Cu;
|
||||
|
||||
// Channel menu sprite ids (confirmed in chat element dump).
|
||||
private const uint MenuNormal = 0x06004D65u;
|
||||
|
|
@ -130,7 +133,29 @@ public sealed class ChatWindowController
|
|||
return null;
|
||||
}
|
||||
|
||||
var c = new ChatWindowController { Root = layout.Root };
|
||||
// LayoutDesc 0x21000006 has SEVERAL top-level elements: the gmMainChatUI window
|
||||
// (RootId 0x1000000E) PLUS stray auxiliary elements that are NOT part of the docked
|
||||
// window — a separate Field+ListBox (0x1000001C/1D, the floaty scrollback), the
|
||||
// talk-focus highlight strip (0x1000001E), and a scroll-button prototype (0x10000526).
|
||||
// LayoutImporter.ImportInfos wraps all top-level elements in a synthetic Type-3 root,
|
||||
// so using layout.Root would render the strays overlapping the real window (the
|
||||
// red-striped garbage in the first live render). Use the gmMainChatUI window itself:
|
||||
// GameWindow adds this to the host, which re-parents it out of the synthetic wrapper,
|
||||
// orphaning the strays so they never draw.
|
||||
var window = layout.FindElement(RootId) ?? layout.Root;
|
||||
var c = new ChatWindowController { Root = window };
|
||||
|
||||
// Drop the dat top resize bar (0x1000000F): it is authored 800px wide and
|
||||
// juts out of the content-width window. The host wraps this content in the
|
||||
// universal nine-slice chrome, whose grips provide the resize affordance.
|
||||
if (layout.FindElement(ResizeBarId) is { Parent: { } rbParent } resizeBar)
|
||||
rbParent.RemoveChild(resizeBar);
|
||||
|
||||
// Reclaim the 9px strip the dropped resize bar occupied (rows 0-8 of the root):
|
||||
// grow the transcript panel up to the window top so its dark bg fills the strip.
|
||||
// Otherwise the root element's brown bg shows through as a sliver along the top.
|
||||
transcriptPanel.Top = 0f;
|
||||
transcriptPanel.Height += 9f; // dat resize-bar height (0x1000000F H=9)
|
||||
|
||||
// ── Transcript ───────────────────────────────────────────────────
|
||||
// Place the behavioral transcript widget inside the transcript panel at the
|
||||
|
|
@ -144,7 +169,7 @@ public sealed class ChatWindowController
|
|||
Anchors = ElementReader.ToAnchors(tInfo.Left, tInfo.Top, tInfo.Right, tInfo.Bottom),
|
||||
DatFont = datFont,
|
||||
Font = debugFont,
|
||||
LinesProvider = () => BuildLines(vm),
|
||||
LinesProvider = () => BuildLines(vm, c.Transcript, datFont, debugFont),
|
||||
};
|
||||
transcriptPanel.AddChild(c.Transcript);
|
||||
|
||||
|
|
@ -178,10 +203,12 @@ public sealed class ChatWindowController
|
|||
Anchors = track.Anchors,
|
||||
Model = c.Transcript.Scroll,
|
||||
SpriteResolve = resolve,
|
||||
TrackSprite = TrackSprite,
|
||||
ThumbSprite = ThumbSprite,
|
||||
UpSprite = UpSprite,
|
||||
DownSprite = DownSprite,
|
||||
TrackSprite = TrackSprite,
|
||||
ThumbSprite = ThumbSprite,
|
||||
ThumbTopSprite = ThumbTopSprite,
|
||||
ThumbBotSprite = ThumbBotSprite,
|
||||
UpSprite = UpSprite,
|
||||
DownSprite = DownSprite,
|
||||
};
|
||||
trackParent.RemoveChild(track);
|
||||
trackParent.AddChild(c.Scrollbar);
|
||||
|
|
@ -220,6 +247,11 @@ public sealed class ChatWindowController
|
|||
// ── Max/min toggle — simplified gmMainChatUI::HandleMaximizeButton ──
|
||||
if (layout.FindElement(MaxMinId) is UiDatElement maxMinEl)
|
||||
{
|
||||
// The dat puts max/min and the scrollbar up-button at the SAME X (both
|
||||
// right-anchored), so at content width they overlap. Retail shows max/min
|
||||
// just LEFT of the scrollbar column — shift it one button-width left.
|
||||
if (track is not null)
|
||||
maxMinEl.Left = track.Left - maxMinEl.Width;
|
||||
maxMinEl.ClickThrough = false;
|
||||
maxMinEl.OnClick = c.ToggleMaximize;
|
||||
}
|
||||
|
|
@ -276,17 +308,66 @@ public sealed class ChatWindowController
|
|||
/// <see cref="UiChatView.Line"/> record format, applying retail-faithful
|
||||
/// per-<see cref="ChatKind"/> colors.
|
||||
/// </summary>
|
||||
private static IReadOnlyList<UiChatView.Line> BuildLines(ChatVM vm)
|
||||
private static IReadOnlyList<UiChatView.Line> BuildLines(
|
||||
ChatVM vm, UiChatView view, UiDatFont? datFont, BitmapFont? debugFont)
|
||||
{
|
||||
var detailed = vm.RecentLinesDetailed();
|
||||
if (detailed.Count == 0) return Array.Empty<UiChatView.Line>();
|
||||
|
||||
var result = new UiChatView.Line[detailed.Count];
|
||||
for (int i = 0; i < detailed.Count; i++)
|
||||
result[i] = new UiChatView.Line(detailed[i].Text, RetailChatColor(detailed[i].Kind));
|
||||
// Word-wrap each message to the transcript's current pixel width (ports retail
|
||||
// GlyphList::Recalculate @0x473800 — break at word boundaries when the line would
|
||||
// exceed wrapWidth). Re-evaluated each frame so wrapping follows window resize.
|
||||
float maxW = view.Width - 2f * view.Padding;
|
||||
Func<string, float> measure =
|
||||
datFont is { } df ? s => df.MeasureWidth(s)
|
||||
: debugFont is { } bf ? s => bf.MeasureWidth(s)
|
||||
: s => s.Length * 7f;
|
||||
|
||||
var result = new List<UiChatView.Line>(detailed.Count);
|
||||
foreach (var d in detailed)
|
||||
{
|
||||
var color = RetailChatColor(d.Kind);
|
||||
foreach (var frag in WrapText(d.Text, maxW, measure))
|
||||
result.Add(new UiChatView.Line(frag, color));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <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.
|
||||
/// </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 break;
|
||||
}
|
||||
|
||||
var line = new System.Text.StringBuilder();
|
||||
foreach (var word in text.Split(' '))
|
||||
{
|
||||
if (line.Length == 0)
|
||||
{
|
||||
line.Append(word);
|
||||
}
|
||||
else if (measure(line.ToString() + " " + word) > maxW)
|
||||
{
|
||||
yield return line.ToString();
|
||||
line.Clear();
|
||||
line.Append(word);
|
||||
}
|
||||
else
|
||||
{
|
||||
line.Append(' ').Append(word);
|
||||
}
|
||||
}
|
||||
if (line.Length > 0) yield return line.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Per-<see cref="ChatKind"/> text color matching retail AC's channel coloring
|
||||
/// (observed from retail client screenshots and holtburger's chat.rs coloring).
|
||||
|
|
@ -297,7 +378,7 @@ public sealed class ChatWindowController
|
|||
ChatKind.RangedSpeech => new(1f, 0.95f, 0.8f, 1f), // warm white — shout
|
||||
ChatKind.Channel => new(0.6f, 0.8f, 1f, 1f), // light blue — channel text
|
||||
ChatKind.Tell => new(1f, 0.5f, 1f, 1f), // magenta — whisper
|
||||
ChatKind.System => new(1f, 1f, 0.45f, 1f), // yellow — system messages
|
||||
ChatKind.System => new(0f, 1f, 0f, 1f), // green — system messages (retail ChatMessageType 5; AC2D eGreen {0,255,0})
|
||||
ChatKind.Popup => new(1f, 0.85f, 0.4f, 1f), // amber — popup/broadcast
|
||||
ChatKind.Emote => new(0.8f, 0.8f, 0.7f, 1f), // off-white — emote
|
||||
ChatKind.SoulEmote => new(0.8f, 0.8f, 0.7f, 1f), // off-white — soul emote
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue