diff --git a/src/font/main.zig b/src/font/main.zig index 9a6a2b16f..8787be47a 100644 --- a/src/font/main.zig +++ b/src/font/main.zig @@ -10,7 +10,8 @@ pub const Face = face.Face; pub const Group = @import("Group.zig"); pub const GroupCache = @import("GroupCache.zig"); pub const Glyph = @import("Glyph.zig"); -pub const Shaper = @import("Shaper.zig"); +pub const shape = @import("shape.zig"); +pub const Shaper = shape.Shaper; pub const sprite = @import("sprite.zig"); pub const Sprite = sprite.Sprite; pub const Descriptor = discovery.Descriptor; diff --git a/src/font/shape.zig b/src/font/shape.zig new file mode 100644 index 000000000..d48688ce5 --- /dev/null +++ b/src/font/shape.zig @@ -0,0 +1,14 @@ +const builtin = @import("builtin"); +const options = @import("main.zig").options; +const harfbuzz = @import("shaper/harfbuzz.zig"); + +/// Shaper implementation for our compile options. +pub const Shaper = switch (options.backend) { + .freetype, + .fontconfig_freetype, + .coretext_freetype, + .coretext, + => harfbuzz.Shaper, + + .web_canvas => harfbuzz.Shaper, +}; diff --git a/src/font/Shaper.zig b/src/font/shaper/harfbuzz.zig similarity index 63% rename from src/font/Shaper.zig rename to src/font/shaper/harfbuzz.zig index 5b04e495b..d51e4da44 100644 --- a/src/font/Shaper.zig +++ b/src/font/shaper/harfbuzz.zig @@ -1,230 +1,230 @@ -//! This struct handles text shaping. -const Shaper = @This(); - const std = @import("std"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; const harfbuzz = @import("harfbuzz"); const trace = @import("tracy").trace; -const font = @import("main.zig"); -const Face = @import("main.zig").Face; -const DeferredFace = @import("main.zig").DeferredFace; -const Group = @import("main.zig").Group; -const GroupCache = @import("main.zig").GroupCache; -const Library = @import("main.zig").Library; -const Style = @import("main.zig").Style; -const Presentation = @import("main.zig").Presentation; -const terminal = @import("../terminal/main.zig"); +const font = @import("../main.zig"); +const Face = font.Face; +const DeferredFace = font.DeferredFace; +const Group = font.Group; +const GroupCache = font.GroupCache; +const Library = font.Library; +const Style = font.Style; +const Presentation = font.Presentation; +const terminal = @import("../../terminal/main.zig"); const log = std.log.scoped(.font_shaper); -/// The buffer used for text shaping. We reuse it across multiple shaping -/// calls to prevent allocations. -hb_buf: harfbuzz.Buffer, +/// Shaper that uses Harfbuzz. +pub const Shaper = struct { + /// The buffer used for text shaping. We reuse it across multiple shaping + /// calls to prevent allocations. + hb_buf: harfbuzz.Buffer, -/// The shared memory used for shaping results. -cell_buf: []Cell, + /// The shared memory used for shaping results. + cell_buf: []Cell, -/// 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(cell_buf: []Cell) !Shaper { - return Shaper{ - .hb_buf = try harfbuzz.Buffer.create(), - .cell_buf = cell_buf, - }; -} - -pub fn deinit(self: *Shaper) void { - self.hb_buf.destroy(); -} - -/// Returns an iterator that returns one text run at a time for the -/// given terminal row. Note that text runs are are only valid one at a time -/// for a Shaper struct since they share state. -pub fn runIterator(self: *Shaper, group: *GroupCache, row: terminal.Screen.Row) RunIterator { - return .{ .shaper = self, .group = group, .row = row }; -} - -/// Shape the given text run. The text run must be the immediately previous -/// text run that was iterated since the text run does share state with the -/// Shaper struct. -/// -/// The return value is only valid until the next shape call is called. -/// -/// If there is not enough space in the cell buffer, an error is returned. -pub fn shape(self: *Shaper, run: TextRun) ![]Cell { - const tracy = trace(@src()); - defer tracy.end(); - - // We only do shaping if the font is not a special-case. For special-case - // fonts, the codepoint == glyph_index so we don't need to run any shaping. - if (run.font_index.special() == null) { - // TODO: we do not want to hardcode these - const hb_feats = &[_]harfbuzz.Feature{ - harfbuzz.Feature.fromString("dlig").?, - harfbuzz.Feature.fromString("liga").?, + /// 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(cell_buf: []Cell) !Shaper { + return Shaper{ + .hb_buf = try harfbuzz.Buffer.create(), + .cell_buf = cell_buf, }; - - const face = try run.group.group.faceFromIndex(run.font_index); - harfbuzz.shape(face.hb_font, self.hb_buf, hb_feats); } - // If our buffer is empty, we short-circuit the rest of the work - // return nothing. - if (self.hb_buf.getLength() == 0) return self.cell_buf[0..0]; - const info = self.hb_buf.getGlyphInfos(); - const pos = self.hb_buf.getGlyphPositions() orelse return error.HarfbuzzFailed; - - // This is perhaps not true somewhere, but we currently assume it is true. - // If it isn't true, I'd like to catch it and learn more. - assert(info.len == pos.len); - - // Convert all our info/pos to cells and set it. - if (info.len > self.cell_buf.len) return error.OutOfMemory; - //log.warn("info={} pos={} run={}", .{ info.len, pos.len, run }); - - for (info) |v, i| { - self.cell_buf[i] = .{ - .x = @intCast(u16, v.cluster), - .glyph_index = v.codepoint, - }; - - //log.warn("i={} info={} pos={} cell={}", .{ i, v, pos[i], self.cell_buf[i] }); + pub fn deinit(self: *Shaper) void { + self.hb_buf.destroy(); } - return self.cell_buf[0..info.len]; -} + /// Returns an iterator that returns one text run at a time for the + /// given terminal row. Note that text runs are are only valid one at a time + /// for a Shaper struct since they share state. + pub fn runIterator(self: *Shaper, group: *GroupCache, row: terminal.Screen.Row) RunIterator { + return .{ .shaper = self, .group = group, .row = row }; + } -pub const Cell = struct { - /// The column that this cell occupies. Since a set of shaper cells is - /// always on the same line, only the X is stored. It is expected the - /// caller has access to the original screen cell. - x: u16, - - /// The glyph index for this cell. The font index to use alongside - /// this cell is available in the text run. - glyph_index: u32, -}; - -/// A single text run. A text run is only valid for one Shaper and -/// until the next run is created. -pub const TextRun = struct { - /// The offset in the row where this run started - offset: u16, - - /// The total number of cells produced by this run. - cells: u16, - - /// The font group that built this run. - group: *GroupCache, - - /// The font index to use for the glyphs of this run. - font_index: Group.FontIndex, -}; - -pub const RunIterator = struct { - shaper: *Shaper, - group: *GroupCache, - row: terminal.Screen.Row, - i: usize = 0, - - pub fn next(self: *RunIterator, alloc: Allocator) !?TextRun { + /// Shape the given text run. The text run must be the immediately previous + /// text run that was iterated since the text run does share state with the + /// Shaper struct. + /// + /// The return value is only valid until the next shape call is called. + /// + /// If there is not enough space in the cell buffer, an error is returned. + pub fn shape(self: *Shaper, run: TextRun) ![]Cell { const tracy = trace(@src()); defer tracy.end(); - // Trim the right side of a row that might be empty - const max: usize = max: { - var j: usize = self.row.lenCells(); - while (j > 0) : (j -= 1) if (!self.row.getCell(j - 1).empty()) break; - break :max j; - }; + // We only do shaping if the font is not a special-case. For special-case + // fonts, the codepoint == glyph_index so we don't need to run any shaping. + if (run.font_index.special() == null) { + // TODO: we do not want to hardcode these + const hb_feats = &[_]harfbuzz.Feature{ + harfbuzz.Feature.fromString("dlig").?, + harfbuzz.Feature.fromString("liga").?, + }; - // We're over at the max - if (self.i >= max) return null; - - // Track the font for our curent run - var current_font: Group.FontIndex = .{}; - - // Reset the buffer for our current run - self.shaper.hb_buf.reset(); - self.shaper.hb_buf.setContentType(.unicode); - - // Go through cell by cell and accumulate while we build our run. - var j: usize = self.i; - while (j < max) : (j += 1) { - const cluster = j; - const cell = self.row.getCell(j); - - // If we're a spacer, then we ignore it - if (cell.attrs.wide_spacer_tail) continue; - - const style: Style = if (cell.attrs.bold) - .bold - else - .regular; - - // Determine the presentation format for this glyph. - const presentation: ?Presentation = if (cell.attrs.grapheme) p: { - // We only check the FIRST codepoint because I believe the - // presentation format must be directly adjacent to the codepoint. - var it = self.row.codepointIterator(j); - if (it.next()) |cp| { - if (cp == 0xFE0E) break :p Presentation.text; - if (cp == 0xFE0F) break :p Presentation.emoji; - } - - break :p null; - } else null; - - // Determine the font for this cell. We'll use fallbacks - // manually here to try replacement chars and then a space - // for unknown glyphs. - const font_idx_opt = (try self.group.indexForCodepoint( - alloc, - if (cell.empty() or cell.char == 0) ' ' else cell.char, - style, - presentation, - )) orelse (try self.group.indexForCodepoint( - alloc, - 0xFFFD, - style, - .text, - )) orelse - try self.group.indexForCodepoint(alloc, ' ', style, .text); - const font_idx = font_idx_opt.?; - //log.warn("char={x} idx={}", .{ cell.char, font_idx }); - if (j == self.i) current_font = font_idx; - - // If our fonts are not equal, then we're done with our run. - if (font_idx.int() != current_font.int()) break; - - // Continue with our run - self.shaper.hb_buf.add(cell.char, @intCast(u32, cluster)); - - // If this cell is part of a grapheme cluster, add all the grapheme - // data points. - if (cell.attrs.grapheme) { - var it = self.row.codepointIterator(j); - while (it.next()) |cp| { - if (cp == 0xFE0E or cp == 0xFE0F) continue; - self.shaper.hb_buf.add(cp, @intCast(u32, cluster)); - } - } + const face = try run.group.group.faceFromIndex(run.font_index); + harfbuzz.shape(face.hb_font, self.hb_buf, hb_feats); } - // Finalize our buffer - self.shaper.hb_buf.guessSegmentProperties(); + // If our buffer is empty, we short-circuit the rest of the work + // return nothing. + if (self.hb_buf.getLength() == 0) return self.cell_buf[0..0]; + const info = self.hb_buf.getGlyphInfos(); + const pos = self.hb_buf.getGlyphPositions() orelse return error.HarfbuzzFailed; - // Move our cursor. Must defer since we use self.i below. - defer self.i = j; + // This is perhaps not true somewhere, but we currently assume it is true. + // If it isn't true, I'd like to catch it and learn more. + assert(info.len == pos.len); - return TextRun{ - .offset = @intCast(u16, self.i), - .cells = @intCast(u16, j - self.i), - .group = self.group, - .font_index = current_font, - }; + // Convert all our info/pos to cells and set it. + if (info.len > self.cell_buf.len) return error.OutOfMemory; + //log.warn("info={} pos={} run={}", .{ info.len, pos.len, run }); + + for (info) |v, i| { + self.cell_buf[i] = .{ + .x = @intCast(u16, v.cluster), + .glyph_index = v.codepoint, + }; + + //log.warn("i={} info={} pos={} cell={}", .{ i, v, pos[i], self.cell_buf[i] }); + } + + return self.cell_buf[0..info.len]; } + + pub const Cell = struct { + /// The column that this cell occupies. Since a set of shaper cells is + /// always on the same line, only the X is stored. It is expected the + /// caller has access to the original screen cell. + x: u16, + + /// The glyph index for this cell. The font index to use alongside + /// this cell is available in the text run. + glyph_index: u32, + }; + + /// A single text run. A text run is only valid for one Shaper and + /// until the next run is created. + pub const TextRun = struct { + /// The offset in the row where this run started + offset: u16, + + /// The total number of cells produced by this run. + cells: u16, + + /// The font group that built this run. + group: *GroupCache, + + /// The font index to use for the glyphs of this run. + font_index: Group.FontIndex, + }; + + pub const RunIterator = struct { + shaper: *Shaper, + group: *GroupCache, + row: terminal.Screen.Row, + i: usize = 0, + + pub fn next(self: *RunIterator, alloc: Allocator) !?TextRun { + const tracy = trace(@src()); + defer tracy.end(); + + // Trim the right side of a row that might be empty + const max: usize = max: { + var j: usize = self.row.lenCells(); + while (j > 0) : (j -= 1) if (!self.row.getCell(j - 1).empty()) break; + break :max j; + }; + + // We're over at the max + if (self.i >= max) return null; + + // Track the font for our curent run + var current_font: Group.FontIndex = .{}; + + // Reset the buffer for our current run + self.shaper.hb_buf.reset(); + self.shaper.hb_buf.setContentType(.unicode); + + // Go through cell by cell and accumulate while we build our run. + var j: usize = self.i; + while (j < max) : (j += 1) { + const cluster = j; + const cell = self.row.getCell(j); + + // If we're a spacer, then we ignore it + if (cell.attrs.wide_spacer_tail) continue; + + const style: Style = if (cell.attrs.bold) + .bold + else + .regular; + + // Determine the presentation format for this glyph. + const presentation: ?Presentation = if (cell.attrs.grapheme) p: { + // We only check the FIRST codepoint because I believe the + // presentation format must be directly adjacent to the codepoint. + var it = self.row.codepointIterator(j); + if (it.next()) |cp| { + if (cp == 0xFE0E) break :p Presentation.text; + if (cp == 0xFE0F) break :p Presentation.emoji; + } + + break :p null; + } else null; + + // Determine the font for this cell. We'll use fallbacks + // manually here to try replacement chars and then a space + // for unknown glyphs. + const font_idx_opt = (try self.group.indexForCodepoint( + alloc, + if (cell.empty() or cell.char == 0) ' ' else cell.char, + style, + presentation, + )) orelse (try self.group.indexForCodepoint( + alloc, + 0xFFFD, + style, + .text, + )) orelse + try self.group.indexForCodepoint(alloc, ' ', style, .text); + const font_idx = font_idx_opt.?; + //log.warn("char={x} idx={}", .{ cell.char, font_idx }); + if (j == self.i) current_font = font_idx; + + // If our fonts are not equal, then we're done with our run. + if (font_idx.int() != current_font.int()) break; + + // Continue with our run + self.shaper.hb_buf.add(cell.char, @intCast(u32, cluster)); + + // If this cell is part of a grapheme cluster, add all the grapheme + // data points. + if (cell.attrs.grapheme) { + var it = self.row.codepointIterator(j); + while (it.next()) |cp| { + if (cp == 0xFE0E or cp == 0xFE0F) continue; + self.shaper.hb_buf.add(cp, @intCast(u32, cluster)); + } + } + } + + // Finalize our buffer + self.shaper.hb_buf.guessSegmentProperties(); + + // Move our cursor. Must defer since we use self.i below. + defer self.i = j; + + return TextRun{ + .offset = @intCast(u16, self.i), + .cells = @intCast(u16, j - self.i), + .group = self.group, + .font_index = current_font, + }; + } + }; }; test "run iterator" { @@ -619,7 +619,7 @@ const TestShaper = struct { shaper: Shaper, cache: *GroupCache, lib: Library, - cell_buf: []Cell, + cell_buf: []Shaper.Cell, pub fn deinit(self: *TestShaper) void { self.shaper.deinit(); @@ -632,9 +632,9 @@ const TestShaper = struct { /// Helper to return a fully initialized shaper. fn testShaper(alloc: Allocator) !TestShaper { - const testFont = @import("test.zig").fontRegular; - const testEmoji = @import("test.zig").fontEmoji; - const testEmojiText = @import("test.zig").fontEmojiText; + const testFont = @import("../test.zig").fontRegular; + const testEmoji = @import("../test.zig").fontEmoji; + const testEmojiText = @import("../test.zig").fontEmojiText; var lib = try Library.init(); errdefer lib.deinit(); @@ -653,10 +653,10 @@ fn testShaper(alloc: Allocator) !TestShaper { try cache_ptr.group.addFace(alloc, .regular, DeferredFace.initLoaded(try Face.init(lib, testEmoji, .{ .points = 12 }))); try cache_ptr.group.addFace(alloc, .regular, DeferredFace.initLoaded(try Face.init(lib, testEmojiText, .{ .points = 12 }))); - var cell_buf = try alloc.alloc(Cell, 80); + var cell_buf = try alloc.alloc(Shaper.Cell, 80); errdefer alloc.free(cell_buf); - var shaper = try init(cell_buf); + var shaper = try Shaper.init(cell_buf); errdefer shaper.deinit(); return TestShaper{