From 0505018186102442e677a374c06335825e2a58e5 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 29 Aug 2022 16:39:48 -0700 Subject: [PATCH] Line segmentation into text runs --- pkg/harfbuzz/buffer.zig | 5 ++ src/font/Group.zig | 11 ++- src/font/Shaper.zig | 178 ++++++++++++++++++++++++++++++++++++++++ src/font/main.zig | 1 + src/terminal/Screen.zig | 2 +- 5 files changed, 193 insertions(+), 4 deletions(-) create mode 100644 src/font/Shaper.zig diff --git a/pkg/harfbuzz/buffer.zig b/pkg/harfbuzz/buffer.zig index f62833358..5b494579c 100644 --- a/pkg/harfbuzz/buffer.zig +++ b/pkg/harfbuzz/buffer.zig @@ -31,6 +31,11 @@ pub const Buffer = struct { c.hb_buffer_reset(self.handle); } + /// Returns the number of items in the buffer. + pub fn getLength(self: Buffer) u32 { + return c.hb_buffer_get_length(self.handle); + } + /// Sets the type of buffer contents. Buffers are either empty, contain /// characters (before shaping), or contain glyphs (the result of shaping). pub fn setContentType(self: Buffer, ct: ContentType) void { diff --git a/src/font/Group.zig b/src/font/Group.zig index a952660bf..6020002af 100644 --- a/src/font/Group.zig +++ b/src/font/Group.zig @@ -63,11 +63,16 @@ pub fn addFace(self: *Group, alloc: Allocator, style: Style, face: Face) !void { /// This represents a specific font in the group. pub const FontIndex = packed struct { /// The number of bits we use for the index. - const idx_bits = 8 - StyleArray.len; + const idx_bits = 8 - @typeInfo(@typeInfo(Style).Enum.tag_type).Int.bits; pub const IndexInt = @Type(.{ .Int = .{ .signedness = .unsigned, .bits = idx_bits } }); - style: Style, - idx: IndexInt, + style: Style = .regular, + idx: IndexInt = 0, + + /// Convert to int + pub fn int(self: FontIndex) u8 { + return @bitCast(u8, self); + } test { // We never want to take up more than a byte since font indexes are diff --git a/src/font/Shaper.zig b/src/font/Shaper.zig new file mode 100644 index 000000000..cd41ea6aa --- /dev/null +++ b/src/font/Shaper.zig @@ -0,0 +1,178 @@ +//! This struct handles text shaping. +const Shaper = @This(); + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const harfbuzz = @import("harfbuzz"); +const Atlas = @import("../Atlas.zig"); +const Face = @import("main.zig").Face; +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 terminal = @import("../terminal/main.zig"); + +/// The font group to use under the covers +group: *GroupCache, + +/// The buffer used for text shaping. We reuse it across multiple shaping +/// calls to prevent allocations. +hb_buf: harfbuzz.Buffer, + +pub fn init(group: *GroupCache) !Shaper { + return Shaper{ + .group = group, + .hb_buf = try harfbuzz.Buffer.create(), + }; +} + +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, row: terminal.Screen.Row) RunIterator { + return .{ .shaper = self, .row = row }; +} + +/// A single text run. A text run is only valid for one Shaper and +/// until the next run is created. +pub const TextRun = struct { + font_index: Group.FontIndex, +}; + +pub const RunIterator = struct { + shaper: *Shaper, + row: terminal.Screen.Row, + i: usize = 0, + + pub fn next(self: *RunIterator, alloc: Allocator) !?TextRun { + if (self.i >= self.row.len) 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 < self.row.len) : (j += 1) { + const cell = self.row[j]; + + // Ignore tailing wide spacers, this will get fixed up by the shaper + if (cell.empty() or cell.attrs.wide_spacer_tail) continue; + + const style: Style = if (cell.attrs.bold) + .bold + else + .regular; + + // Determine the font for this cell + const font_idx_opt = try self.shaper.group.indexForCodepoint(alloc, style, cell.char); + const font_idx = font_idx_opt.?; + 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, j - self.i)); + } + + // Finalize our buffer + self.shaper.hb_buf.guessSegmentProperties(); + + // Move our cursor + self.i = j; + + return TextRun{ .font_index = current_font }; + } +}; + +test "run iterator" { + const testing = std.testing; + const alloc = testing.allocator; + + var testdata = try testShaper(alloc); + defer testdata.deinit(); + + { + // Make a screen with some data + var screen = try terminal.Screen.init(alloc, 3, 5, 0); + defer screen.deinit(alloc); + screen.testWriteString("ABCD"); + + // Get our run iterator + var shaper = testdata.shaper; + var it = shaper.runIterator(screen.getRow(.{ .screen = 0 })); + var count: usize = 0; + while (try it.next(alloc)) |_| count += 1; + try testing.expectEqual(@as(usize, 1), count); + } + + { + // Make a screen with some data + var screen = try terminal.Screen.init(alloc, 3, 5, 0); + defer screen.deinit(alloc); + screen.testWriteString("A😃D"); + + // Get our run iterator + var shaper = testdata.shaper; + var it = shaper.runIterator(screen.getRow(.{ .screen = 0 })); + var count: usize = 0; + while (try it.next(alloc)) |_| { + count += 1; + + // All runs should be exactly length 1 + std.log.warn("YES", .{}); + try testing.expectEqual(@as(u32, 1), shaper.hb_buf.getLength()); + } + try testing.expectEqual(@as(usize, 3), count); + } +} + +const TestShaper = struct { + alloc: Allocator, + shaper: Shaper, + cache: *GroupCache, + lib: Library, + + pub fn deinit(self: *TestShaper) void { + self.shaper.deinit(); + self.cache.deinit(self.alloc); + self.alloc.destroy(self.cache); + self.lib.deinit(); + } +}; + +/// Helper to return a fully initialized shaper. +fn testShaper(alloc: Allocator) !TestShaper { + const testFont = @import("test.zig").fontRegular; + const testEmoji = @import("test.zig").fontEmoji; + + var lib = try Library.init(); + errdefer lib.deinit(); + + var cache_ptr = try alloc.create(GroupCache); + errdefer alloc.destroy(cache_ptr); + cache_ptr.* = try GroupCache.init(alloc, try Group.init(alloc)); + errdefer cache_ptr.*.deinit(alloc); + + // Setup group + try cache_ptr.group.addFace(alloc, .regular, try Face.init(lib, testFont, .{ .points = 12 })); + try cache_ptr.group.addFace(alloc, .regular, try Face.init(lib, testEmoji, .{ .points = 12 })); + + var shaper = try init(cache_ptr); + errdefer shaper.deinit(); + + return TestShaper{ + .alloc = alloc, + .shaper = shaper, + .cache = cache_ptr, + .lib = lib, + }; +} diff --git a/src/font/main.zig b/src/font/main.zig index ec3e8b73a..e4469eb2e 100644 --- a/src/font/main.zig +++ b/src/font/main.zig @@ -5,6 +5,7 @@ pub const Group = @import("Group.zig"); pub const GroupCache = @import("GroupCache.zig"); pub const Glyph = @import("Glyph.zig"); pub const Library = @import("Library.zig"); +pub const Shaper = @import("Shaper.zig"); /// The styles that a family can take. pub const Style = enum(u2) { diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index e2a704340..73466b685 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -1093,7 +1093,7 @@ pub fn testString(self: Screen, alloc: Allocator, tag: RowIndexTag) ![]const u8 /// Writes a basic string into the screen for testing. Newlines (\n) separate /// each row. If a line is longer than the available columns, soft-wrapping /// will occur. -fn testWriteString(self: *Screen, text: []const u8) void { +pub fn testWriteString(self: *Screen, text: []const u8) void { var y: usize = 0; var x: usize = 0;