diff --git a/src/font/DeferredFace.zig b/src/font/DeferredFace.zig index 8a5767518..de4293ba1 100644 --- a/src/font/DeferredFace.zig +++ b/src/font/DeferredFace.zig @@ -83,9 +83,13 @@ pub const WebCanvas = struct { pub fn deinit(self: *DeferredFace) void { switch (options.backend) { .fontconfig_freetype => if (self.fc) |*fc| fc.deinit(), - .coretext, .coretext_freetype, .coretext_harfbuzz => if (self.ct) |*ct| ct.deinit(), .freetype => {}, .web_canvas => if (self.wc) |*wc| wc.deinit(), + .coretext, + .coretext_freetype, + .coretext_harfbuzz, + .coretext_noshape, + => if (self.ct) |*ct| ct.deinit(), } self.* = undefined; } @@ -98,7 +102,11 @@ pub fn familyName(self: DeferredFace, buf: []u8) ![]const u8 { .fontconfig_freetype => if (self.fc) |fc| return (try fc.pattern.get(.family, 0)).string, - .coretext, .coretext_freetype, .coretext_harfbuzz => if (self.ct) |ct| { + .coretext, + .coretext_freetype, + .coretext_harfbuzz, + .coretext_noshape, + => if (self.ct) |ct| { const family_name = ct.font.copyAttribute(.family_name); return family_name.cstringPtr(.utf8) orelse unsupported: { break :unsupported family_name.cstring(buf, .utf8) orelse @@ -121,7 +129,11 @@ pub fn name(self: DeferredFace, buf: []u8) ![]const u8 { .fontconfig_freetype => if (self.fc) |fc| return (try fc.pattern.get(.fullname, 0)).string, - .coretext, .coretext_freetype, .coretext_harfbuzz => if (self.ct) |ct| { + .coretext, + .coretext_freetype, + .coretext_harfbuzz, + .coretext_noshape, + => if (self.ct) |ct| { const display_name = ct.font.copyDisplayName(); return display_name.cstringPtr(.utf8) orelse unsupported: { // "NULL if the internal storage of theString does not allow @@ -147,7 +159,7 @@ pub fn load( ) !Face { return switch (options.backend) { .fontconfig_freetype => try self.loadFontconfig(lib, opts), - .coretext, .coretext_harfbuzz => try self.loadCoreText(lib, opts), + .coretext, .coretext_harfbuzz, .coretext_noshape => try self.loadCoreText(lib, opts), .coretext_freetype => try self.loadCoreTextFreetype(lib, opts), .web_canvas => try self.loadWebCanvas(opts), @@ -262,7 +274,11 @@ pub fn hasCodepoint(self: DeferredFace, cp: u32, p: ?Presentation) bool { } }, - .coretext, .coretext_freetype, .coretext_harfbuzz => { + .coretext, + .coretext_freetype, + .coretext_harfbuzz, + .coretext_noshape, + => { // If we are using coretext, we check the loaded CT font. if (self.ct) |ct| { if (p) |desired_p| { diff --git a/src/font/discovery.zig b/src/font/discovery.zig index b21d374f5..0259200c9 100644 --- a/src/font/discovery.zig +++ b/src/font/discovery.zig @@ -14,8 +14,12 @@ const log = std.log.scoped(.discovery); pub const Discover = switch (options.backend) { .freetype => void, // no discovery .fontconfig_freetype => Fontconfig, - .coretext, .coretext_freetype, .coretext_harfbuzz => CoreText, .web_canvas => void, // no discovery + .coretext, + .coretext_freetype, + .coretext_harfbuzz, + .coretext_noshape, + => CoreText, }; /// Descriptor is used to search for fonts. The only required field diff --git a/src/font/face.zig b/src/font/face.zig index 6d5f80169..815629b44 100644 --- a/src/font/face.zig +++ b/src/font/face.zig @@ -13,7 +13,11 @@ pub const Face = switch (options.backend) { .coretext_freetype, => freetype.Face, - .coretext, .coretext_harfbuzz => coretext.Face, + .coretext, + .coretext_harfbuzz, + .coretext_noshape, + => coretext.Face, + .web_canvas => web_canvas.Face, }; diff --git a/src/font/library.zig b/src/font/library.zig index 8f72c4fb8..57e11e64a 100644 --- a/src/font/library.zig +++ b/src/font/library.zig @@ -16,6 +16,7 @@ pub const Library = switch (options.backend) { // Some backends such as CT and Canvas don't have a "library" .coretext, .coretext_harfbuzz, + .coretext_noshape, .web_canvas, => NoopLibrary, }; diff --git a/src/font/main.zig b/src/font/main.zig index 72c3aa9cf..a287d9a06 100644 --- a/src/font/main.zig +++ b/src/font/main.zig @@ -61,6 +61,9 @@ pub const Backend = enum { /// CoreText for font discovery and rendering, HarfBuzz for shaping coretext_harfbuzz, + /// CoreText for font discovery and rendering, no shaping. + coretext_noshape, + /// Use the browser font system and the Canvas API (wasm). This limits /// the available fonts to browser fonts (anything Canvas natively /// supports). @@ -97,6 +100,7 @@ pub const Backend = enum { .coretext, .coretext_harfbuzz, + .coretext_noshape, .web_canvas, => false, }; @@ -107,6 +111,7 @@ pub const Backend = enum { .coretext, .coretext_freetype, .coretext_harfbuzz, + .coretext_noshape, => true, .freetype, @@ -124,6 +129,7 @@ pub const Backend = enum { .coretext, .coretext_freetype, .coretext_harfbuzz, + .coretext_noshape, .web_canvas, => false, }; @@ -138,6 +144,7 @@ pub const Backend = enum { => true, .coretext, + .coretext_noshape, .web_canvas, => false, }; diff --git a/src/font/shape.zig b/src/font/shape.zig index 1e5dd1a9f..e633c15c9 100644 --- a/src/font/shape.zig +++ b/src/font/shape.zig @@ -1,5 +1,6 @@ const builtin = @import("builtin"); const options = @import("main.zig").options; +pub const noop = @import("shaper/noop.zig"); pub const harfbuzz = @import("shaper/harfbuzz.zig"); pub const coretext = @import("shaper/coretext.zig"); pub const web_canvas = @import("shaper/web_canvas.zig"); @@ -19,6 +20,8 @@ pub const Shaper = switch (options.backend) { // font faces. .coretext => coretext.Shaper, + .coretext_noshape => noop.Shaper, + .web_canvas => web_canvas.Shaper, }; @@ -61,4 +64,7 @@ pub const Options = struct { test { _ = Cache; _ = Shaper; + + // Always test noop + _ = noop; } diff --git a/src/font/shaper/noop.zig b/src/font/shaper/noop.zig new file mode 100644 index 000000000..310b5cf40 --- /dev/null +++ b/src/font/shaper/noop.zig @@ -0,0 +1,143 @@ +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const trace = @import("tracy").trace; +const font = @import("../main.zig"); +const Face = font.Face; +const Collection = font.Collection; +const DeferredFace = font.DeferredFace; +const Group = font.Group; +const GroupCache = font.GroupCache; +const Library = font.Library; +const SharedGrid = font.SharedGrid; +const Style = font.Style; +const Presentation = font.Presentation; +const terminal = @import("../../terminal/main.zig"); + +const log = std.log.scoped(.font_shaper); + +/// Shaper that doesn't do any shaping. Each individual codepoint is mapped +/// directly to the detected text run font's glyph index. +pub const Shaper = struct { + /// The allocated used for the feature list and cell buf. + alloc: Allocator, + + /// The string used for shaping the current run. + run_state: RunState, + + /// The shared memory used for shaping results. + cell_buf: CellBuf, + + const CellBuf = std.ArrayListUnmanaged(font.shape.Cell); + const CodepointList = std.ArrayListUnmanaged(Codepoint); + const Codepoint = struct { + codepoint: u32, + cluster: u32, + }; + + const RunState = struct { + codepoints: CodepointList = .{}, + + fn deinit(self: *RunState, alloc: Allocator) void { + self.codepoints.deinit(alloc); + } + + fn reset(self: *RunState) !void { + self.codepoints.clearRetainingCapacity(); + } + }; + + /// The cell_buf argument is the buffer to use for storing shaped results. + /// This should be at least the number of columns in the terminal. + pub fn init(alloc: Allocator, opts: font.shape.Options) !Shaper { + _ = opts; + + return Shaper{ + .alloc = alloc, + .cell_buf = .{}, + .run_state = .{}, + }; + } + + pub fn deinit(self: *Shaper) void { + self.cell_buf.deinit(self.alloc); + self.run_state.deinit(self.alloc); + } + + pub fn runIterator( + self: *Shaper, + grid: *SharedGrid, + screen: *const terminal.Screen, + row: terminal.Pin, + selection: ?terminal.Selection, + cursor_x: ?usize, + ) font.shape.RunIterator { + return .{ + .hooks = .{ .shaper = self }, + .grid = grid, + .screen = screen, + .row = row, + .selection = selection, + .cursor_x = cursor_x, + }; + } + + 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 + // can just return the codepoints as-is. + if (run.font_index.special() != null) { + self.cell_buf.clearRetainingCapacity(); + try self.cell_buf.ensureTotalCapacity(self.alloc, state.codepoints.items.len); + for (state.codepoints.items) |entry| { + self.cell_buf.appendAssumeCapacity(.{ + .x = @intCast(entry.cluster), + .glyph_index = @intCast(entry.codepoint), + }); + } + + return self.cell_buf.items; + } + + // Go through the run and map each codepoint to a glyph index. + self.cell_buf.clearRetainingCapacity(); + + // Note: this is digging into some internal details, we should maybe + // expose a public API for this. + const face = try run.grid.resolver.collection.getFace(run.font_index); + for (state.codepoints.items) |entry| { + const glyph_index = face.glyphIndex(entry.codepoint); + try self.cell_buf.append(self.alloc, .{ + .x = @intCast(entry.cluster), + .glyph_index = glyph_index, + }); + } + + return self.cell_buf.items; + } + + /// The hooks for RunIterator. + pub const RunIteratorHook = struct { + shaper: *Shaper, + + pub fn prepare(self: *RunIteratorHook) !void { + try self.shaper.run_state.reset(); + } + + pub fn addCodepoint(self: RunIteratorHook, cp: u32, cluster: u32) !void { + try self.shaper.run_state.codepoints.append(self.shaper.alloc, .{ + .codepoint = cp, + .cluster = cluster, + }); + } + + pub fn finalize(self: RunIteratorHook) !void { + _ = self; + } + }; +}; + +test { + @import("std").testing.refAllDecls(@This()); +}