fix(ui): talk button keeps authored 46x17; white authored captions + font for talk/Send
Owner report (2026-08-24, post gate-pass): the chat channel button was
bigger than retail and both its caption and the Send caption were warm
gold instead of retail's near-white.
Size: retail never resizes the talk button — HandleSelection
@0x004cd540 only swaps the caption string; the authored 46x17 element
stands, and the authored SHORT captions ('Gen', 'Fell', ...) fit it —
that is why retail abbreviates. Our content-widening reflow (grow the
button to its label, shift the input) was a compensation for the
now-retired invented long captions, measured with the wrong font on
top. Deleted; the authored row layout stands.
Color + font: the button caption child (0x10000015) and the Send
button (0x10000019) both author pure white text with their OWN FontDid
0x40000002 (live-DAT probed) — different from the transcript font,
which is the other half of why 'Chat' fits 46px. UiMenu gains a
ButtonDatFont for the caption (popup rows keep the menu font);
the controller reads both elements' authored FontColor/FontDid instead
of the invented (1,.92,.72) constants.
The old widening pin is rewritten to the retail contract; a new
conformance test pins authored width, white captions, and the authored
font DID being requested for both buttons.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
c12a95b6e8
commit
67e963524b
4 changed files with 106 additions and 33 deletions
|
|
@ -62,6 +62,7 @@ public sealed class ChatWindowController : IRetainedWindowStateController, IReta
|
|||
private const uint TrackId = 0x10000012u;
|
||||
private const uint InputBarId = 0x10000013u;
|
||||
private const uint MenuId = 0x10000014u;
|
||||
private const uint MenuLabelId = 0x10000015u; // button caption child: white, FontDid 0x40000002
|
||||
private const uint InputId = 0x10000016u; // Type-12 Text + Editable 0x16 → UiField
|
||||
private const uint SendId = 0x10000019u;
|
||||
private const uint MaxMinId = 0x1000046Fu;
|
||||
|
|
@ -301,7 +302,8 @@ public sealed class ChatWindowController : IRetainedWindowStateController, IReta
|
|||
BitmapFont? debugFont,
|
||||
Func<uint, (uint tex, int w, int h)> resolve,
|
||||
Func<string?>? selectedTargetName = null,
|
||||
Func<string, string?>? chatStrings = null)
|
||||
Func<string, string?>? chatStrings = null,
|
||||
Func<uint, UiDatFont?>? resolveFont = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(windowFilters);
|
||||
|
||||
|
|
@ -478,6 +480,19 @@ public sealed class ChatWindowController : IRetainedWindowStateController, IReta
|
|||
if (layout.FindElement(MenuId) is UiMenu menu)
|
||||
{
|
||||
menu.DatFont = datFont; menu.Font = debugFont; menu.SpriteResolve = resolve;
|
||||
// The authored label child (0x10000015) carries the button
|
||||
// caption's OWN color and font: pure white, FontDid 0x40000002
|
||||
// (live-DAT probed 2026-08-24) — not the transcript font and not
|
||||
// the invented warm gold. Falls back to the previous defaults
|
||||
// when the fixture lacks the child (hermetic tests).
|
||||
if (FindInfo(rootInfo, MenuLabelId) is { } labelInfo)
|
||||
{
|
||||
if (labelInfo.FontColor is { } authoredColor)
|
||||
menu.TextColor = authoredColor;
|
||||
if (labelInfo.FontDid != 0u
|
||||
&& resolveFont?.Invoke(labelInfo.FontDid) is { } buttonFont)
|
||||
menu.ButtonDatFont = buttonFont;
|
||||
}
|
||||
menu.NormalSprite = MenuNormal; menu.PressedSprite = MenuPressed;
|
||||
menu.PopupBgSprite = MenuPopupBg;
|
||||
menu.ItemNormalSprite = MenuItemRow; menu.ItemHighlightSprite = MenuItemSelected;
|
||||
|
|
@ -559,33 +574,28 @@ public sealed class ChatWindowController : IRetainedWindowStateController, IReta
|
|||
if (layout.FindElement(SendId) is UiButton sendEl)
|
||||
{
|
||||
sendEl.OnClick = () => c.Input.Submit();
|
||||
// The Send sprite is a blank gold button — retail draws the caption as text.
|
||||
// The Send sprite is a blank gold button — retail draws the caption
|
||||
// as text with the element's OWN authored style: pure white,
|
||||
// FontDid 0x40000002 (live-DAT probed 2026-08-24; the previous
|
||||
// warm gold was invented).
|
||||
sendEl.Label = "Send";
|
||||
sendEl.LabelFont = datFont;
|
||||
sendEl.LabelColor = new Vector4(1f, 0.92f, 0.72f, 1f);
|
||||
ElementInfo? sendInfo = FindInfo(rootInfo, SendId);
|
||||
sendEl.LabelFont =
|
||||
(sendInfo?.FontDid is { } sendFontDid and not 0u
|
||||
? resolveFont?.Invoke(sendFontDid)
|
||||
: null) ?? datFont;
|
||||
sendEl.LabelColor = sendInfo?.FontColor ?? new Vector4(1f, 1f, 1f, 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 onSelect = c.Menu.OnSelect;
|
||||
c.Menu.OnSelect = p => { onSelect?.Invoke(p); ReflowInputRow(); };
|
||||
ReflowInputRow();
|
||||
}
|
||||
// 2026-08-24: the previous content-widening reflow here (grow the
|
||||
// button to its label, shift the input) was a compensation for the
|
||||
// invented LONG captions ("General"). Retail keeps the authored
|
||||
// 46x17 button — the authored SHORT captions ('Gen', font
|
||||
// 0x40000002) fit it, which is exactly why retail abbreviates
|
||||
// (gmMainChatUI::HandleSelection @0x004cd540 never resizes the
|
||||
// element). The authored row layout stands untouched.
|
||||
|
||||
// ── Max/min toggle — gmMainChatUI::HandleMaximizeButton ──
|
||||
// The dat already authors max/min (368,5,16,16) just left of the scrollbar
|
||||
|
|
|
|||
|
|
@ -1599,7 +1599,8 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
// Authored talk-focus labels (StringTable 0x23000001, ID_Chat_*
|
||||
// keys via compute_str_hash — the retail "Gen"/"Tell to X" set).
|
||||
chatStrings: key => new DatStringResolver(_bindings.Assets.Dats)
|
||||
.Resolve(0x23000001u, DatStringResolver.ComputeHash(key)));
|
||||
.Resolve(0x23000001u, DatStringResolver.ComputeHash(key)),
|
||||
resolveFont: _bindings.Assets.ResolveFont);
|
||||
if (controller is null)
|
||||
{
|
||||
Console.WriteLine("[D.2b] chat: required role elements missing in 0x2100006F.");
|
||||
|
|
|
|||
|
|
@ -202,6 +202,17 @@ public sealed class UiMenu : UiElement
|
|||
public uint CurrentFaceSpriteForTest => _facePressed ? PressedSprite : NormalSprite;
|
||||
|
||||
public UiDatFont? DatFont { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional separate font for the BUTTON caption. The chat talk button's
|
||||
/// authored label child (0x10000015) carries its own FontDid 0x40000002 —
|
||||
/// different from the popup rows/transcript font — which is how retail
|
||||
/// fits 'Chat'/'Gen' inside the authored 46x17 face (live-DAT probed
|
||||
/// 2026-08-24, the owner-reported "button too big" delta). Null falls
|
||||
/// back to <see cref="DatFont"/>.
|
||||
/// </summary>
|
||||
public UiDatFont? ButtonDatFont { get; set; }
|
||||
|
||||
public AcDream.App.Rendering.BitmapFont? Font { get; set; }
|
||||
|
||||
/// <summary>Retail LayoutDesc property <c>0x21</c> (two-pass glyph outline,
|
||||
|
|
@ -400,10 +411,17 @@ public sealed class UiMenu : UiElement
|
|||
// label child spans the button MINUS the arrow-cap overlay's right
|
||||
// socket (0x10000355 is 100 wide of the 117 button, docked; the
|
||||
// arrow child overlays the last 17px — menuprobe3).
|
||||
UiDatFont? captionFont = ButtonDatFont ?? DatFont;
|
||||
float captionW = captionFont?.MeasureWidth(caption)
|
||||
?? Font?.MeasureWidth(caption) ?? caption.Length * 7f;
|
||||
float captionLineH = captionFont?.LineHeight ?? Font?.LineHeight ?? 14f;
|
||||
float capX = ButtonTextCentered
|
||||
? MathF.Max(0f, (Width - (ArrowCapClosedSprite != 0 ? ArrowCapWidth : 0f) - MeasureText(caption)) * 0.5f)
|
||||
? MathF.Max(0f, (Width - (ArrowCapClosedSprite != 0 ? ArrowCapWidth : 0f) - captionW) * 0.5f)
|
||||
: ButtonTextIndent;
|
||||
DrawLabel(ctx, caption, capX, (Height - LineH()) * 0.5f, TextColor);
|
||||
if (captionFont is { } cf)
|
||||
ctx.DrawStringDat(cf, caption, capX, (Height - captionLineH) * 0.5f, TextColor, Outline, OutlineColor);
|
||||
else
|
||||
ctx.DrawString(caption, capX, (Height - captionLineH) * 0.5f, TextColor, Font);
|
||||
|
||||
// G6: the open/closed arrow-cap overlay — see ArrowCapClosedSprite's doc comment.
|
||||
if (resolve is not null) DrawArrowCap(ctx, resolve);
|
||||
|
|
@ -443,7 +461,8 @@ public sealed class UiMenu : UiElement
|
|||
public float NaturalButtonWidth()
|
||||
{
|
||||
string text = ButtonLabelProvider?.Invoke() ?? "";
|
||||
float textW = DatFont?.MeasureWidth(text) ?? Font?.MeasureWidth(text) ?? text.Length * 7f;
|
||||
UiDatFont? nf = ButtonDatFont ?? DatFont;
|
||||
float textW = nf?.MeasureWidth(text) ?? Font?.MeasureWidth(text) ?? text.Length * 7f;
|
||||
return ButtonTextIndent + textW + 4f + FaceCapR; // text start (clears LED) + text + gap + arrow cap
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -368,8 +368,15 @@ public class ChatLayoutConformanceTests
|
|||
Assert.Equal(605f, inputBar.Width);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 2026-08-24 rewrite: this test used to pin the content-widening
|
||||
/// reflow (button grown past 46px for the invented long captions).
|
||||
/// Retail never resizes the talk button — the authored 46x17 stands
|
||||
/// through selection changes AND layout passes, and the input keeps its
|
||||
/// authored start (owner-reported "button too big" delta).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ChatFixture_ChannelCaptionWidth_SurvivesImportedLayoutPass()
|
||||
public void ChatFixture_ChannelButton_KeepsAuthoredWidthThroughLayoutPass()
|
||||
{
|
||||
var infos = FixtureLoader.LoadChatInfos();
|
||||
var layout = LayoutImporter.Build(infos, NoTex, null);
|
||||
|
|
@ -384,13 +391,12 @@ public class ChatLayoutConformanceTests
|
|||
NoTex);
|
||||
Assert.NotNull(controller);
|
||||
|
||||
controller!.Menu.OnSelect!.Invoke(ChatChannelKind.General);
|
||||
float fittedWidth = controller.Menu.Width;
|
||||
float inputLeft = controller!.Input.Left;
|
||||
controller.Menu.OnSelect!.Invoke(ChatChannelKind.General);
|
||||
controller.Menu.ApplyAnchor(controller.Menu.Parent!.Width, controller.Menu.Parent.Height);
|
||||
|
||||
Assert.True(fittedWidth > 46f);
|
||||
Assert.Equal(fittedWidth, controller.Menu.Width);
|
||||
Assert.Equal(controller.Menu.Left + fittedWidth, controller.Input.Left);
|
||||
Assert.Equal(46f, controller.Menu.Width);
|
||||
Assert.Equal(inputLeft, controller.Input.Left);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -734,4 +740,41 @@ public class ChatLayoutConformanceTests
|
|||
ApplyLayoutPassLocal(child);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 2026-08-24 owner report ("chat button too big; caption should be
|
||||
/// whiter — same for Send"): retail keeps the AUTHORED 46x17 talk
|
||||
/// button (HandleSelection @0x004cd540 never resizes it — the authored
|
||||
/// SHORT captions fit, which is why retail abbreviates), and both the
|
||||
/// button caption child (0x10000015) and the Send button (0x10000019)
|
||||
/// author PURE WHITE text with their own FontDid 0x40000002. The
|
||||
/// previous content-widening reflow and invented warm-gold labels are
|
||||
/// retired.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void TalkButtonAndSend_KeepAuthoredSizeFontAndWhiteCaptions()
|
||||
{
|
||||
var infos = FixtureLoader.LoadChatInfos();
|
||||
ImportedLayout layout = LayoutImporter.Build(infos, NoTex, null);
|
||||
var requestedFonts = new List<uint>();
|
||||
var controller = ChatWindowController.Bind(
|
||||
infos, layout, new ChatVM(new ChatLog()), () => NullCommandBus.Instance,
|
||||
new ChatWindowState(), null, null, NoTex,
|
||||
resolveFont: did => { requestedFonts.Add(did); return null; });
|
||||
Assert.NotNull(controller);
|
||||
|
||||
UiMenu menu = Assert.IsType<UiMenu>(layout.FindElement(0x10000014u));
|
||||
// Authored 46x17 stands — no content widening.
|
||||
Assert.Equal(46f, menu.Width);
|
||||
menu.OnSelect!.Invoke(ChatChannelKind.General);
|
||||
Assert.Equal(46f, menu.Width);
|
||||
// Authored caption color: pure white.
|
||||
Assert.Equal(new System.Numerics.Vector4(1f, 1f, 1f, 1f), menu.TextColor);
|
||||
|
||||
var send = Assert.IsType<UiButton>(layout.FindElement(0x10000019u));
|
||||
Assert.Equal(new System.Numerics.Vector4(1f, 1f, 1f, 1f), send.LabelColor);
|
||||
|
||||
// Both caption fonts were requested from the authored FontDid.
|
||||
Assert.Contains(0x40000002u, requestedFonts);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue