diff --git a/src/AcDream.App/UI/UiChatView.cs b/src/AcDream.App/UI/UiChatView.cs
index a2039c08..1392c26f 100644
--- a/src/AcDream.App/UI/UiChatView.cs
+++ b/src/AcDream.App/UI/UiChatView.cs
@@ -34,6 +34,11 @@ public sealed class UiChatView : UiElement
/// Font for the transcript; falls back to the context default.
public BitmapFont? Font { get; set; }
+ /// Retail dat font (0x40000000) for the transcript. When set, glyphs
+ /// render via the two-pass dat-font blit and measure/hit-test use the dat glyph
+ /// advance; when null, the debug BitmapFont path is used. Set by the controller.
+ public UiDatFont? DatFont { get; set; }
+
/// Keyboard device for clipboard (Ctrl+C) + modifier state. Wired by
/// the host from .
public Silk.NET.Input.IKeyboard? Keyboard { get; set; }
@@ -49,11 +54,12 @@ public sealed class UiChatView : UiElement
// Pixels the transcript is scrolled UP from the newest line (0 = pinned to bottom).
private float _scroll;
- private const float WheelLines = 3f; // lines advanced per wheel notch
+ private const float WheelLines = 1f; // lines advanced per wheel notch (retail = 1 line per notch)
// ── Cached layout from the last OnDraw, so OnEvent hit-tests the SAME geometry ──
private IReadOnlyList _lastLines = Array.Empty();
private BitmapFont? _lastFont;
+ private UiDatFont? _lastDatFont;
private float _lastLineHeight = 16f;
private float _lastBaseY; // top Y of line 0 in local space
private float _lastPadding = 4f;
@@ -85,21 +91,24 @@ public sealed class UiChatView : UiElement
{
ctx.DrawRect(0, 0, Width, Height, BackgroundColor);
- var font = Font ?? ctx.DefaultFont;
- if (font is null) return;
+ // Prefer the retail dat font when set; fall back to BitmapFont.
+ var datFont = DatFont;
+ var bitmapFont = datFont is null ? (Font ?? ctx.DefaultFont) : null;
+ if (datFont is null && bitmapFont is null) return;
var lines = LinesProvider();
// Cache the geometry OnEvent will hit-test against. Even when there are no
// lines we record the font/padding so a stray hit-test is harmless.
_lastLines = lines;
- _lastFont = font;
- _lastLineHeight = font.LineHeight;
+ _lastDatFont = datFont;
+ _lastFont = bitmapFont;
+ _lastLineHeight = datFont is not null ? datFont.LineHeight : bitmapFont!.LineHeight;
_lastPadding = Padding;
if (lines.Count == 0) return;
- float lh = font.LineHeight;
+ float lh = _lastLineHeight;
float top = Padding, bottom = Height - Padding;
float innerH = bottom - top;
float contentH = lines.Count * lh;
@@ -129,13 +138,25 @@ public sealed class UiChatView : UiElement
c1 = Math.Clamp(c1, 0, text.Length);
if (c1 > c0)
{
- float hx = Padding + font.MeasureWidth(text.Substring(0, c0));
- float hw = font.MeasureWidth(text.Substring(c0, c1 - c0));
+ float hx, hw;
+ if (datFont is not null)
+ {
+ hx = Padding + datFont.MeasureWidth(text.Substring(0, c0));
+ hw = datFont.MeasureWidth(text.Substring(c0, c1 - c0));
+ }
+ else
+ {
+ hx = Padding + bitmapFont!.MeasureWidth(text.Substring(0, c0));
+ hw = bitmapFont.MeasureWidth(text.Substring(c0, c1 - c0));
+ }
ctx.DrawRect(hx, y, hw, lh, SelectionColor);
}
}
- ctx.DrawString(text, Padding, y, lines[i].Color, font);
+ if (datFont is not null)
+ ctx.DrawStringDat(datFont, text, Padding, y, lines[i].Color);
+ else
+ ctx.DrawString(text, Padding, y, lines[i].Color, bitmapFont);
}
}
@@ -145,7 +166,7 @@ public sealed class UiChatView : UiElement
{
case UiEventType.Scroll:
{
- float lh = (Font ?? _lastFont)?.LineHeight ?? 16f;
+ float lh = DatFont?.LineHeight ?? (Font ?? _lastFont)?.LineHeight ?? 16f;
// Silk wheel +Y = scroll up = reveal older = shift content down = larger _scroll.
_scroll += e.Data0 * WheelLines * lh; // re-clamped next OnDraw against live content
return true;
@@ -316,11 +337,13 @@ public sealed class UiChatView : UiElement
line = Math.Clamp(line, 0, lines.Count - 1);
string text = lines[line].Text;
- var font = _lastFont;
- int col = font is null
- ? 0
- : CharIndexAt(text, ch => font.TryGetGlyph(ch, out var g) ? g.Advance : 0f,
- localX - _lastPadding);
+ int col = _lastDatFont is { } df
+ ? CharIndexAt(text, ch => df.TryGetGlyph(ch, out var g) ? UiDatFont.GlyphAdvance(g) : 0f,
+ localX - _lastPadding)
+ : (_lastFont is { } bf
+ ? CharIndexAt(text, ch => bf.TryGetGlyph(ch, out var bg) ? bg.Advance : 0f,
+ localX - _lastPadding)
+ : 0);
return new Pos(line, col);
}
diff --git a/tests/AcDream.App.Tests/UI/UiChatViewDatFontTests.cs b/tests/AcDream.App.Tests/UI/UiChatViewDatFontTests.cs
new file mode 100644
index 00000000..c00c9544
--- /dev/null
+++ b/tests/AcDream.App.Tests/UI/UiChatViewDatFontTests.cs
@@ -0,0 +1,30 @@
+using AcDream.App.UI;
+using DatReaderWriter.Types;
+using Xunit;
+
+namespace AcDream.App.Tests.UI;
+
+public class UiChatViewDatFontTests
+{
+ // Synthetic per-char advance: each glyph 10px (Before=2,Width=6,After=2).
+ private static FontCharDesc Glyph(char c) => new()
+ {
+ Unicode = c, HorizontalOffsetBefore = 2, Width = 6, HorizontalOffsetAfter = 2,
+ OffsetX = 0, OffsetY = 0, Height = 12, VerticalOffsetBefore = 0,
+ };
+
+ [Fact]
+ public void CharIndexAt_UsesDatGlyphAdvance()
+ {
+ float Adv(char c) => UiDatFont.GlyphAdvance(Glyph(c));
+ Assert.Equal(0, UiChatView.CharIndexAt("abc", Adv, 4f));
+ Assert.Equal(1, UiChatView.CharIndexAt("abc", Adv, 12f));
+ Assert.Equal(3, UiChatView.CharIndexAt("abc", Adv, 100f));
+ }
+
+ [Fact]
+ public void GlyphAdvance_MatchesRetailFormula()
+ {
+ Assert.Equal(10f, UiDatFont.GlyphAdvance(Glyph('x')));
+ }
+}