mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 16:56:09 +03:00
font/coretext: handle two-byte utf16 followed by more chars
This commit is contained in:
BIN
src/font/res/JetBrainsMonoNerdFont-Regular.ttf
Normal file
BIN
src/font/res/JetBrainsMonoNerdFont-Regular.ttf
Normal file
Binary file not shown.
@ -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();
|
||||||
|
@ -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");
|
||||||
|
Reference in New Issue
Block a user