using System.Collections.Generic;
using AcDream.App.UI;
using DatReaderWriter.Types;
namespace AcDream.App.Tests.UI;
///
/// Pure pen-advance / MeasureWidth math for the retail dat font (no GL, no dat).
/// The advance per glyph is the retail
/// HorizontalOffsetBefore + Width + HorizontalOffsetAfter
/// (SurfaceWindow::DrawCharacter, acclient 0x00442c3a), accumulated across the
/// string the way the retail string loop does (0x00467ed4 edi_3 += var_98).
///
public class UiDatFontTests
{
private static FontCharDesc Glyph(
ushort unicode, byte width,
sbyte before = 0, sbyte after = 0,
ushort offsetX = 0, ushort offsetY = 0, byte height = 16, sbyte vBefore = 0)
=> new()
{
Unicode = unicode,
Width = width,
Height = height,
OffsetX = offsetX,
OffsetY = offsetY,
HorizontalOffsetBefore = before,
HorizontalOffsetAfter = after,
VerticalOffsetBefore = vBefore,
};
[Fact]
public void GlyphAdvance_SumsBeforeWidthAfter()
{
var g = Glyph('A', width: 8, before: 1, after: 2);
Assert.Equal(11f, UiDatFont.GlyphAdvance(g));
}
[Fact]
public void GlyphAdvance_HandlesNegativeBearings()
{
// Kerned glyph: a negative left-bearing pulls it leftward; the advance
// still nets out to before + width + after.
var g = Glyph('j', width: 4, before: -1, after: 0);
Assert.Equal(3f, UiDatFont.GlyphAdvance(g));
}
[Fact]
public void MeasureWidth_SumsEachGlyphAdvance()
{
var table = new Dictionary
{
['2'] = Glyph('2', width: 7, before: 1, after: 1), // advance 9
['9'] = Glyph('9', width: 7, before: 1, after: 1), // advance 9
['1'] = Glyph('1', width: 3, before: 2, after: 1), // advance 6
['/'] = Glyph('/', width: 4, before: 0, after: 1), // advance 5
};
FontCharDesc? Lookup(char c) => table.TryGetValue(c, out var g) ? g : null;
// "291/291" = 9 + 9 + 6 + 5 + 9 + 9 + 6 = 53
Assert.Equal(53f, UiDatFont.MeasureWidth("291/291", Lookup));
}
[Fact]
public void MeasureWidth_SkipsCharactersNotInFont()
{
var table = new Dictionary
{
['5'] = Glyph('5', width: 6, before: 1, after: 1), // advance 8
};
FontCharDesc? Lookup(char c) => table.TryGetValue(c, out var g) ? g : null;
// 'X' has no glyph → contributes nothing; only the two '5's count.
Assert.Equal(16f, UiDatFont.MeasureWidth("5X5", Lookup));
}
[Fact]
public void MeasureWidth_EmptyOrNullIsZero()
{
FontCharDesc? Lookup(char c) => null;
Assert.Equal(0f, UiDatFont.MeasureWidth("", Lookup));
Assert.Equal(0f, UiDatFont.MeasureWidth(null, Lookup));
}
[Fact]
public void InstanceMeasureWidth_ReusesGlyphTableWithoutAllocating()
{
var glyphs = new Dictionary
{
['A'] = Glyph('A', width: 8, before: 1, after: 2),
['B'] = Glyph('B', width: 7, before: 1, after: 1),
};
var font = new UiDatFont(
fgTex: 0,
fgW: 0,
fgH: 0,
bgTex: 0,
bgW: 0,
bgH: 0,
lineHeight: 16f,
baselineOffset: 12f,
glyphs);
const string Text = "ABBA";
float expected = font.MeasureWidth(Text);
float actual = 0f;
long before = GC.GetAllocatedBytesForCurrentThread();
for (int iteration = 0; iteration < 10_000; iteration++)
actual = font.MeasureWidth(Text);
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
Assert.Equal(expected, actual);
Assert.Equal(0, allocated);
}
}