use freetype for font display

This commit is contained in:
2026-08-01 15:03:55 +02:00
parent 4a08d19365
commit f5a0b6136f
11 changed files with 605 additions and 292 deletions
+73 -42
View File
@@ -426,44 +426,87 @@ test "map_to_output merges like any other setting" {
// ─── tab titles ──────────────────────────────────────────────────────────────
/// Stands in for a `Face`, so the eliding rules can be checked without a font
/// on the machine running the tests. One cell wide per character, in the 26.6
/// units `font.fit` counts in.
const Mono = struct {
pub fn advance(_: Mono, _: u21) i32 {
return font.fixed(1);
}
pub fn ellipsisAdvance(self: Mono) i32 {
return self.advance(font.ellipsis);
}
};
/// A proportional face: 'i' is half a cell, the ellipsis two. Real faces are
/// like this, and it is where fixed-cell arithmetic goes wrong.
const Prop = struct {
pub fn advance(_: Prop, cp: u21) i32 {
return switch (cp) {
'i' => @divExact(font.fixed(1), 2),
font.ellipsis => font.fixed(2),
else => font.fixed(1),
};
}
pub fn ellipsisAdvance(self: Prop) i32 {
return self.advance(font.ellipsis);
}
};
test "a title that fits is drawn whole" {
const f = font.fit("foot", 10);
const f = font.fit("foot", font.fixed(10), Mono{});
try testing.expectEqual(@as(usize, 4), f.end);
try testing.expectEqual(@as(usize, 4), f.cells);
try testing.expectEqual(font.fixed(4), f.width);
try testing.expect(!f.elided);
// Exactly full is still whole: the ellipsis costs a cell, so only spend it
// Exactly full is still whole: the ellipsis costs room, so only spend it
// when something is actually cut.
const exact = font.fit("foot", 4);
try testing.expectEqual(@as(usize, 4), exact.cells);
const exact = font.fit("foot", font.fixed(4), Mono{});
try testing.expectEqual(@as(usize, 4), exact.end);
try testing.expect(!exact.elided);
}
test "a title too long for the tab loses its last cell to the ellipsis" {
const f = font.fit("firefox", 4);
test "a title too long for the tab is cut short for the ellipsis" {
const f = font.fit("firefox", font.fixed(4), Mono{});
try testing.expectEqualStrings("fir", "firefox"[0..f.end]);
try testing.expectEqual(@as(usize, 3), f.cells);
try testing.expectEqual(font.fixed(4), f.width);
try testing.expect(f.elided);
// One cell of room leaves nothing but the ellipsis, and no room at all
// draws nothing rather than indexing past the end.
const one = font.fit("firefox", 1);
// Room for the ellipsis alone leaves nothing but the ellipsis, and no room
// at all draws nothing rather than indexing past the end.
const one = font.fit("firefox", font.fixed(1), Mono{});
try testing.expectEqual(@as(usize, 0), one.end);
try testing.expect(one.elided);
const none = font.fit("firefox", 0);
const none = font.fit("firefox", 0, Mono{});
try testing.expectEqual(@as(usize, 0), none.end);
try testing.expect(!none.elided);
}
test "eliding gives up as many characters as the ellipsis costs" {
// "wiki" alone would fit in three cells; the two-cell ellipsis means all
// but the first character has to go, which counting cells would miss.
const f = font.fit("wikiw", font.fixed(3), Prop{});
try testing.expect(f.elided);
try testing.expectEqualStrings("w", "wikiw"[0..f.end]);
try testing.expect(f.width <= font.fixed(3));
// Narrow characters earn their place: three of them survive where two wide
// ones would not.
const narrow = font.fit("wiiwwww", font.fixed(4), Prop{});
try testing.expect(narrow.elided);
try testing.expectEqualStrings("wii", "wiiwwww"[0..narrow.end]);
try testing.expect(narrow.width <= font.fixed(4));
}
test "titles are measured in characters, not bytes" {
// Three codepoints, six bytes: a tab four cells wide holds them all.
const f = font.fit("ünïx", 4);
try testing.expectEqual(@as(usize, 4), f.cells);
// Four codepoints, six bytes: a tab four cells wide holds them all.
const f = font.fit("ünïx", font.fixed(4), Mono{});
try testing.expectEqual("ünïx".len, f.end);
try testing.expect(!f.elided);
// And a cut lands on a codepoint boundary, never mid-sequence.
const cut = font.fit("ünïx", 3);
const cut = font.fit("ünïx", font.fixed(3), Mono{});
try testing.expect(cut.elided);
try testing.expectEqualStrings("ün", "ünïx"[0..cut.end]);
}
@@ -478,32 +521,20 @@ test "malformed utf-8 in a title yields one glyph per bad byte" {
try testing.expectEqual(@as(?u21, null), it.next());
}
test "text measuring and cell fitting agree" {
const scale = 2;
// Whatever cellsForWidth says fits must actually fit.
var width: i32 = 0;
while (width < 200) : (width += 1) {
const cells = font.cellsForWidth(width, scale);
try testing.expect(font.textWidth(cells, scale) <= width);
try testing.expect(font.textWidth(cells + 1, scale) > width);
}
test "control characters in a title draw as spaces" {
var it: font.Iterator = .{ .bytes = "a\nb\tc\x7f" };
try testing.expectEqual(@as(?u21, 'a'), it.next());
try testing.expectEqual(@as(?u21, ' '), it.next());
try testing.expectEqual(@as(?u21, 'b'), it.next());
try testing.expectEqual(@as(?u21, ' '), it.next());
try testing.expectEqual(@as(?u21, 'c'), it.next());
try testing.expectEqual(@as(?u21, ' '), it.next());
try testing.expectEqual(@as(?u21, null), it.next());
}
test "printable ascii all have glyphs, and unknown codepoints fall back" {
// The space is blank, and nothing else printable is.
var c: u8 = 0x21;
while (c <= 0x7e) : (c += 1) {
const rows = font.glyph(c);
var lit = false;
for (rows) |r| lit = lit or r != 0;
try testing.expect(lit);
// Nothing may spill outside the five column cell.
for (rows) |r| try testing.expectEqual(@as(u8, 0), r & ~@as(u8, 0b11111));
}
for (font.glyph(' ')) |r| try testing.expectEqual(@as(u8, 0), r);
// Control characters stay blank; other codepoints get the fallback box.
for (font.glyph('\n')) |r| try testing.expectEqual(@as(u8, 0), r);
try testing.expect(!std.mem.eql(u8, font.glyph('?'), font.glyph('あ')));
try testing.expectEqualSlices(u8, font.glyph(0xfffd), font.glyph('あ'));
test "fixed point widths round up to whole pixels" {
try testing.expectEqual(@as(i32, 0), font.pixels(0));
try testing.expectEqual(@as(i32, 1), font.pixels(1));
try testing.expectEqual(@as(i32, 1), font.pixels(font.fixed(1)));
try testing.expectEqual(@as(i32, 2), font.pixels(font.fixed(1) + 1));
}