From 2be4eb0da72f78658169fad944fac2ac482e4ea5 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 23 Mar 2023 10:24:22 -0700 Subject: [PATCH] font/shaper: split runs at selection boundaries --- src/font/shaper/harfbuzz.zig | 134 +++++++++++++++++++++++++++++---- src/font/shaper/run.zig | 11 +++ src/font/shaper/web_canvas.zig | 8 +- src/renderer/OpenGL.zig | 13 ++-- src/terminal/Selection.zig | 13 +++- 5 files changed, 157 insertions(+), 22 deletions(-) diff --git a/src/font/shaper/harfbuzz.zig b/src/font/shaper/harfbuzz.zig index 44b56b5bf..5a090b5db 100644 --- a/src/font/shaper/harfbuzz.zig +++ b/src/font/shaper/harfbuzz.zig @@ -43,12 +43,22 @@ pub const Shaper = struct { /// 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. + /// + /// The selection must be a row-only selection (height = 1). See + /// Selection.containedRow. The run iterator will ONLY look at X values + /// and assume the y value matches. pub fn runIterator( self: *Shaper, group: *GroupCache, row: terminal.Screen.Row, + selection: ?terminal.Selection, ) font.shape.RunIterator { - return .{ .hooks = .{ .shaper = self }, .group = group, .row = row }; + return .{ + .hooks = .{ .shaper = self }, + .group = group, + .row = row, + .selection = selection, + }; } /// Shape the given text run. The text run must be the immediately previous @@ -136,7 +146,7 @@ test "run iterator" { // Get our run iterator var shaper = testdata.shaper; - var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 })); + var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null); var count: usize = 0; while (try it.next(alloc)) |_| count += 1; try testing.expectEqual(@as(usize, 1), count); @@ -149,7 +159,7 @@ test "run iterator" { try screen.testWriteString("ABCD EFG"); var shaper = testdata.shaper; - var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 })); + var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null); var count: usize = 0; while (try it.next(alloc)) |_| count += 1; try testing.expectEqual(@as(usize, 1), count); @@ -163,7 +173,7 @@ test "run iterator" { // Get our run iterator var shaper = testdata.shaper; - var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 })); + var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null); var count: usize = 0; while (try it.next(alloc)) |_| { count += 1; @@ -197,7 +207,7 @@ test "run iterator: empty cells with background set" { // Get our run iterator var shaper = testdata.shaper; - var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 })); + var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -211,6 +221,7 @@ test "run iterator: empty cells with background set" { try testing.expectEqual(@as(usize, 1), count); } } + test "shape" { const testing = std.testing; const alloc = testing.allocator; @@ -231,7 +242,7 @@ test "shape" { // Get our run iterator var shaper = testdata.shaper; - var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 })); + var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -254,7 +265,7 @@ test "shape inconsolata ligs" { try screen.testWriteString(">="); var shaper = testdata.shaper; - var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 })); + var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -271,7 +282,7 @@ test "shape inconsolata ligs" { try screen.testWriteString("==="); var shaper = testdata.shaper; - var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 })); + var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -296,7 +307,7 @@ test "shape emoji width" { try screen.testWriteString("👍"); var shaper = testdata.shaper; - var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 })); + var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -330,7 +341,7 @@ test "shape emoji width long" { // Get our run iterator var shaper = testdata.shaper; - var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 })); + var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -361,7 +372,7 @@ test "shape variation selector VS15" { // Get our run iterator var shaper = testdata.shaper; - var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 })); + var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -392,7 +403,7 @@ test "shape variation selector VS16" { // Get our run iterator var shaper = testdata.shaper; - var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 })); + var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -420,7 +431,7 @@ test "shape with empty cells in between" { // Get our run iterator var shaper = testdata.shaper; - var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 })); + var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -452,7 +463,7 @@ test "shape Chinese characters" { // Get our run iterator var shaper = testdata.shaper; - var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 })); + var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -493,7 +504,7 @@ test "shape box glyphs" { // Get our run iterator var shaper = testdata.shaper; - var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 })); + var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -508,6 +519,99 @@ test "shape box glyphs" { try testing.expectEqual(@as(usize, 1), count); } +test "shape selection boundary" { + 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, 10, 0); + defer screen.deinit(); + try screen.testWriteString("a1b2c3d4e5"); + + // Full line selection + { + // Get our run iterator + var shaper = testdata.shaper; + var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), .{ + .start = .{ .x = 0, .y = 0 }, + .end = .{ .x = screen.cols - 1, .y = 0 }, + }); + var count: usize = 0; + while (try it.next(alloc)) |run| { + count += 1; + _ = try shaper.shape(run); + } + try testing.expectEqual(@as(usize, 1), count); + } + + // Offset x, goes to end of line selection + { + // Get our run iterator + var shaper = testdata.shaper; + var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), .{ + .start = .{ .x = 2, .y = 0 }, + .end = .{ .x = screen.cols - 1, .y = 0 }, + }); + var count: usize = 0; + while (try it.next(alloc)) |run| { + count += 1; + _ = try shaper.shape(run); + } + try testing.expectEqual(@as(usize, 2), count); + } + + // Offset x, starts at beginning of line + { + // Get our run iterator + var shaper = testdata.shaper; + var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), .{ + .start = .{ .x = 0, .y = 0 }, + .end = .{ .x = 3, .y = 0 }, + }); + var count: usize = 0; + while (try it.next(alloc)) |run| { + count += 1; + _ = try shaper.shape(run); + } + try testing.expectEqual(@as(usize, 2), count); + } + + // Selection only subset of line + { + // Get our run iterator + var shaper = testdata.shaper; + var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), .{ + .start = .{ .x = 1, .y = 0 }, + .end = .{ .x = 3, .y = 0 }, + }); + var count: usize = 0; + while (try it.next(alloc)) |run| { + count += 1; + _ = try shaper.shape(run); + } + try testing.expectEqual(@as(usize, 3), count); + } + + // Selection only one character + { + // Get our run iterator + var shaper = testdata.shaper; + var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), .{ + .start = .{ .x = 1, .y = 0 }, + .end = .{ .x = 1, .y = 0 }, + }); + var count: usize = 0; + while (try it.next(alloc)) |run| { + count += 1; + _ = try shaper.shape(run); + } + try testing.expectEqual(@as(usize, 3), count); + } +} + const TestShaper = struct { alloc: Allocator, shaper: Shaper, diff --git a/src/font/shaper/run.zig b/src/font/shaper/run.zig index c7f802986..e9bc61771 100644 --- a/src/font/shaper/run.zig +++ b/src/font/shaper/run.zig @@ -28,6 +28,7 @@ pub const RunIterator = struct { hooks: font.Shaper.RunIteratorHook, group: *font.GroupCache, row: terminal.Screen.Row, + selection: ?terminal.Selection = null, i: usize = 0, pub fn next(self: *RunIterator, alloc: Allocator) !?TextRun { @@ -56,6 +57,16 @@ pub const RunIterator = struct { const cluster = j; const cell = self.row.getCell(j); + // If we have a selection and we're at a boundary point, then + // we break the run here. + if (self.selection) |unordered_sel| { + if (j > self.i) { + const sel = unordered_sel.ordered(.forward); + if (sel.start.x > 0 and j == sel.start.x) break; + if (sel.end.x > 0 and j == sel.end.x + 1) break; + } + } + // If we're a spacer, then we ignore it if (cell.attrs.wide_spacer_tail) continue; diff --git a/src/font/shaper/web_canvas.zig b/src/font/shaper/web_canvas.zig index c80558c10..dba9a18e7 100644 --- a/src/font/shaper/web_canvas.zig +++ b/src/font/shaper/web_canvas.zig @@ -57,8 +57,14 @@ pub const Shaper = struct { self: *Shaper, group: *font.GroupCache, row: terminal.Screen.Row, + selection: ?terminal.Selection, ) font.shape.RunIterator { - return .{ .hooks = .{ .shaper = self }, .group = group, .row = row }; + return .{ + .hooks = .{ .shaper = self }, + .group = group, + .row = row, + .selection = selection, + }; } /// Shape the given text run. The text run must be the immediately diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 9db367c94..489c92de1 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -847,8 +847,9 @@ pub fn rebuildCells( defer y += 1; // Our selection value is only non-null if this selection happens - // to contain this row. If the selection changes for any reason, - // then we invalidate the cache. + // to contain this row. This selection value will be set to only be + // the selection that contains this row. This way, if the selection + // changes but not for this line, we don't invalidate the cache. const selection = sel: { if (term_selection) |sel| { const screen_point = (terminal.point.Viewport{ @@ -856,8 +857,10 @@ pub fn rebuildCells( .y = y, }).toScreen(screen); - // If we are selected, we our colors are just inverted fg/bg - if (sel.containsRow(screen_point)) break :sel sel; + // If we are selected, we our colors are just inverted fg/bg. + if (sel.containedRow(screen, screen_point)) |row_sel| { + break :sel row_sel; + } } break :sel null; @@ -901,7 +904,7 @@ pub fn rebuildCells( const start = self.cells.items.len; // Split our row into runs and shape each one. - var iter = self.font_shaper.runIterator(self.font_group, row); + var iter = self.font_shaper.runIterator(self.font_group, row, selection); while (try iter.next(self.alloc)) |run| { for (try self.font_shaper.shape(run)) |shaper_cell| { if (self.updateCell( diff --git a/src/terminal/Selection.zig b/src/terminal/Selection.zig index add7a4d06..851c69f93 100644 --- a/src/terminal/Selection.zig +++ b/src/terminal/Selection.zig @@ -146,8 +146,19 @@ pub fn bottomRight(self: Selection) ScreenPoint { }; } +/// Returns the selection in the given order. +pub fn ordered(self: Selection, desired: Order) Selection { + if (self.order() == desired) return self; + const tl = self.topLeft(); + const br = self.bottomRight(); + return switch (desired) { + .forward => .{ .start = tl, .end = br }, + .reverse => .{ .start = br, .end = tl }, + }; +} + /// The order of the selection (whether it is selecting forward or back). -const Order = enum { forward, reverse }; +pub const Order = enum { forward, reverse }; fn order(self: Selection) Order { if (self.start.y < self.end.y) return .forward;