font/coretext: handle two-byte utf16 followed by more chars

This commit is contained in:
Mitchell Hashimoto
2024-04-04 21:31:07 -07:00
parent 6ace9e9d19
commit eb4d21fcbf
3 changed files with 95 additions and 29 deletions

Binary file not shown.

View File

@ -33,7 +33,7 @@ pub const Shaper = struct {
alloc: Allocator, alloc: Allocator,
/// The string used for shaping the current run. /// The string used for shaping the current run.
codepoints: CodepointList = .{}, run_state: RunState,
/// The font features we want to use. The hardcoded features are always /// The font features we want to use. The hardcoded features are always
/// set first. /// set first.
@ -49,6 +49,28 @@ pub const Shaper = struct {
cluster: u32, cluster: u32,
}; };
const RunState = struct {
str: *macos.foundation.MutableString,
codepoints: CodepointList,
fn init() !RunState {
var str = try macos.foundation.MutableString.create(0);
errdefer str.release();
return .{ .str = str, .codepoints = .{} };
}
fn deinit(self: *RunState, alloc: Allocator) void {
self.codepoints.deinit(alloc);
self.str.release();
}
fn reset(self: *RunState) !void {
self.codepoints.clearRetainingCapacity();
self.str.release();
self.str = try macos.foundation.MutableString.create(0);
}
};
/// List of font features, parsed into the data structures used by /// List of font features, parsed into the data structures used by
/// the CoreText API. The CoreText API requires a pretty annoying wrapping /// the CoreText API. The CoreText API requires a pretty annoying wrapping
/// to setup font features: /// to setup font features:
@ -148,16 +170,20 @@ pub const Shaper = struct {
for (hardcoded_features) |name| try feats.append(name); for (hardcoded_features) |name| try feats.append(name);
for (opts.features) |name| try feats.append(name); for (opts.features) |name| try feats.append(name);
const run_state = try RunState.init();
errdefer run_state.deinit();
return Shaper{ return Shaper{
.alloc = alloc, .alloc = alloc,
.cell_buf = .{}, .cell_buf = .{},
.run_state = run_state,
.features = feats, .features = feats,
}; };
} }
pub fn deinit(self: *Shaper) void { pub fn deinit(self: *Shaper) void {
self.cell_buf.deinit(self.alloc); self.cell_buf.deinit(self.alloc);
self.codepoints.deinit(self.alloc); self.run_state.deinit(self.alloc);
self.features.deinit(); self.features.deinit();
} }
@ -180,12 +206,14 @@ pub const Shaper = struct {
} }
pub fn shape(self: *Shaper, run: font.shape.TextRun) ![]const font.shape.Cell { pub fn shape(self: *Shaper, run: font.shape.TextRun) ![]const font.shape.Cell {
const state = &self.run_state;
// Special fonts aren't shaped and their codepoint == glyph so we // Special fonts aren't shaped and their codepoint == glyph so we
// can just return the codepoints as-is. // can just return the codepoints as-is.
if (run.font_index.special() != null) { if (run.font_index.special() != null) {
self.cell_buf.clearRetainingCapacity(); self.cell_buf.clearRetainingCapacity();
try self.cell_buf.ensureTotalCapacity(self.alloc, self.codepoints.items.len); try self.cell_buf.ensureTotalCapacity(self.alloc, state.codepoints.items.len);
for (self.codepoints.items) |entry| { for (state.codepoints.items) |entry| {
self.cell_buf.appendAssumeCapacity(.{ self.cell_buf.appendAssumeCapacity(.{
.x = @intCast(entry.cluster), .x = @intCast(entry.cluster),
.glyph_index = @intCast(entry.codepoint), .glyph_index = @intCast(entry.codepoint),
@ -218,26 +246,6 @@ pub const Shaper = struct {
}; };
defer run_font.release(); defer run_font.release();
// Build up our string contents
const str = str: {
const str = try macos.foundation.MutableString.create(0);
errdefer str.release();
for (self.codepoints.items) |entry| {
var unichars: [2]u16 = undefined;
const pair = macos.foundation.stringGetSurrogatePairForLongCharacter(
entry.codepoint,
&unichars,
);
const len: usize = if (pair) 2 else 1;
str.appendCharacters(unichars[0..len]);
// log.warn("append codepoint={} unichar_len={}", .{ cp, len });
}
break :str str;
};
defer str.release();
// Get our font and use that get the attributes to set for the // Get our font and use that get the attributes to set for the
// attributed string so the whole string uses the same font. // attributed string so the whole string uses the same font.
const attr_dict = dict: { const attr_dict = dict: {
@ -249,7 +257,7 @@ pub const Shaper = struct {
// Create an attributed string from our string // Create an attributed string from our string
const attr_str = try macos.foundation.AttributedString.create( const attr_str = try macos.foundation.AttributedString.create(
str.string(), state.str.string(),
attr_dict, attr_dict,
); );
defer attr_str.release(); defer attr_str.release();
@ -282,7 +290,7 @@ pub const Shaper = struct {
for (glyphs, positions, advances, indices) |glyph, pos, advance, index| { for (glyphs, positions, advances, indices) |glyph, pos, advance, index| {
// Our cluster is also our cell X position. If the cluster changes // Our cluster is also our cell X position. If the cluster changes
// then we need to reset our current cell offsets. // then we need to reset our current cell offsets.
const cluster = self.codepoints.items[index].cluster; const cluster = state.codepoints.items[index].cluster;
if (cell_offset.cluster != cluster) cell_offset = .{ if (cell_offset.cluster != cluster) cell_offset = .{
.cluster = cluster, .cluster = cluster,
}; };
@ -320,14 +328,33 @@ pub const Shaper = struct {
shaper: *Shaper, shaper: *Shaper,
pub fn prepare(self: *RunIteratorHook) !void { pub fn prepare(self: *RunIteratorHook) !void {
self.shaper.codepoints.clearRetainingCapacity(); try self.shaper.run_state.reset();
} }
pub fn addCodepoint(self: RunIteratorHook, cp: u32, cluster: u32) !void { pub fn addCodepoint(self: RunIteratorHook, cp: u32, cluster: u32) !void {
try self.shaper.codepoints.append(self.shaper.alloc, .{ // Build our UTF-16 string for CoreText
var unichars: [2]u16 = undefined;
const pair = macos.foundation.stringGetSurrogatePairForLongCharacter(
cp,
&unichars,
);
const len: usize = if (pair) 2 else 1;
const state = &self.shaper.run_state;
state.str.appendCharacters(unichars[0..len]);
// Build our reverse lookup table for codepoints to clusters
try state.codepoints.append(self.shaper.alloc, .{
.codepoint = cp, .codepoint = cp,
.cluster = cluster, .cluster = cluster,
}); });
// If the UTF-16 codepoint is a pair then we need to insert
// a dummy entry so that the CTRunGetStringIndices() function
// maps correctly.
if (pair) try state.codepoints.append(self.shaper.alloc, .{
.codepoint = 0,
.cluster = cluster,
});
} }
pub fn finalize(self: RunIteratorHook) !void { pub fn finalize(self: RunIteratorHook) !void {
@ -493,7 +520,41 @@ test "shape" {
try testing.expectEqual(@as(usize, 1), count); try testing.expectEqual(@as(usize, 1), count);
} }
// TODO(coretext) test "shape nerd fonts" {
const testing = std.testing;
const alloc = testing.allocator;
var testdata = try testShaperWithFont(alloc, .nerd_font);
defer testdata.deinit();
var buf: [32]u8 = undefined;
var buf_idx: usize = 0;
buf_idx += try std.unicode.utf8Encode(' ', buf[buf_idx..]); // space
buf_idx += try std.unicode.utf8Encode(0xF024B, buf[buf_idx..]); // nf-md-folder
buf_idx += try std.unicode.utf8Encode(' ', buf[buf_idx..]); // space
// Make a screen with some data
var screen = try terminal.Screen.init(alloc, 10, 3, 0);
defer screen.deinit();
try screen.testWriteString(buf[0..buf_idx]);
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
_ = try shaper.shape(run);
}
try testing.expectEqual(@as(usize, 1), count);
}
test "shape inconsolata ligs" { test "shape inconsolata ligs" {
const testing = std.testing; const testing = std.testing;
const alloc = testing.allocator; const alloc = testing.allocator;
@ -1274,6 +1335,7 @@ const TestShaper = struct {
const TestFont = enum { const TestFont = enum {
inconsolata, inconsolata,
monaspace_neon, monaspace_neon,
nerd_font,
}; };
/// Helper to return a fully initialized shaper. /// Helper to return a fully initialized shaper.
@ -1287,6 +1349,7 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper {
const testFont = switch (font_req) { const testFont = switch (font_req) {
.inconsolata => @import("../test.zig").fontRegular, .inconsolata => @import("../test.zig").fontRegular,
.monaspace_neon => @import("../test.zig").fontMonaspaceNeon, .monaspace_neon => @import("../test.zig").fontMonaspaceNeon,
.nerd_font => @import("../test.zig").fontNerdFont,
}; };
var lib = try Library.init(); var lib = try Library.init();

View File

@ -12,6 +12,9 @@ pub const fontEmoji = @embedFile("res/NotoColorEmoji.ttf");
pub const fontEmojiText = @embedFile("res/NotoEmoji-Regular.ttf"); pub const fontEmojiText = @embedFile("res/NotoEmoji-Regular.ttf");
pub const fontVariable = @embedFile("res/Lilex-VF.ttf"); pub const fontVariable = @embedFile("res/Lilex-VF.ttf");
/// Font with nerd fonts embedded.
pub const fontNerdFont = @embedFile("res/JetBrainsMonoNerdFont-Regular.ttf");
/// Cozette is a unique font because it embeds some emoji characters /// Cozette is a unique font because it embeds some emoji characters
/// but has a text presentation. /// but has a text presentation.
pub const fontCozette = @embedFile("res/CozetteVector.ttf"); pub const fontCozette = @embedFile("res/CozetteVector.ttf");