diff --git a/src/font/shaper/harfbuzz.zig b/src/font/shaper/harfbuzz.zig index e10b86edb..ad9b08396 100644 --- a/src/font/shaper/harfbuzz.zig +++ b/src/font/shaper/harfbuzz.zig @@ -81,12 +81,14 @@ pub const Shaper = struct { group: *GroupCache, row: terminal.Screen.Row, selection: ?terminal.Selection, + cursor_x: ?usize, ) font.shape.RunIterator { return .{ .hooks = .{ .shaper = self }, .group = group, .row = row, .selection = selection, + .cursor_x = cursor_x, }; } @@ -169,7 +171,7 @@ test "run iterator" { // Get our run iterator var shaper = testdata.shaper; - var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null); + var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null, null); var count: usize = 0; while (try it.next(alloc)) |_| count += 1; try testing.expectEqual(@as(usize, 1), count); @@ -182,7 +184,7 @@ test "run iterator" { try screen.testWriteString("ABCD EFG"); var shaper = testdata.shaper; - var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null); + var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null, null); var count: usize = 0; while (try it.next(alloc)) |_| count += 1; try testing.expectEqual(@as(usize, 1), count); @@ -196,7 +198,7 @@ test "run iterator" { // Get our run iterator var shaper = testdata.shaper; - var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null); + var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null, null); var count: usize = 0; while (try it.next(alloc)) |_| { count += 1; @@ -230,7 +232,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 }), null); + var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null, null); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -265,7 +267,7 @@ test "shape" { // Get our run iterator var shaper = testdata.shaper; - var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null); + var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null, null); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -288,7 +290,7 @@ test "shape inconsolata ligs" { try screen.testWriteString(">="); var shaper = testdata.shaper; - var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null); + var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null, null); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -305,7 +307,7 @@ test "shape inconsolata ligs" { try screen.testWriteString("==="); var shaper = testdata.shaper; - var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null); + var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null, null); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -330,7 +332,7 @@ test "shape emoji width" { try screen.testWriteString("👍"); var shaper = testdata.shaper; - var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null); + var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null, null); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -364,7 +366,7 @@ test "shape emoji width long" { // Get our run iterator var shaper = testdata.shaper; - var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null); + var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null, null); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -395,7 +397,7 @@ test "shape variation selector VS15" { // Get our run iterator var shaper = testdata.shaper; - var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null); + var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null, null); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -426,7 +428,7 @@ test "shape variation selector VS16" { // Get our run iterator var shaper = testdata.shaper; - var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null); + var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null, null); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -454,7 +456,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 }), null); + var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null, null); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -486,7 +488,7 @@ test "shape Chinese characters" { // Get our run iterator var shaper = testdata.shaper; - var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null); + var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null, null); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -527,7 +529,7 @@ test "shape box glyphs" { // Get our run iterator var shaper = testdata.shaper; - var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null); + var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null, null); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -561,7 +563,7 @@ test "shape selection boundary" { var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), .{ .start = .{ .x = 0, .y = 0 }, .end = .{ .x = screen.cols - 1, .y = 0 }, - }); + }, null); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -577,7 +579,7 @@ test "shape selection boundary" { var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), .{ .start = .{ .x = 2, .y = 0 }, .end = .{ .x = screen.cols - 1, .y = 0 }, - }); + }, null); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -593,7 +595,7 @@ test "shape selection boundary" { var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), .{ .start = .{ .x = 0, .y = 0 }, .end = .{ .x = 3, .y = 0 }, - }); + }, null); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -609,7 +611,7 @@ test "shape selection boundary" { var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), .{ .start = .{ .x = 1, .y = 0 }, .end = .{ .x = 3, .y = 0 }, - }); + }, null); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -625,7 +627,7 @@ test "shape selection boundary" { var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), .{ .start = .{ .x = 1, .y = 0 }, .end = .{ .x = 1, .y = 0 }, - }); + }, null); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -635,6 +637,71 @@ test "shape selection boundary" { } } +test "shape cursor 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"); + + // No cursor is full line + { + // Get our run iterator + var shaper = testdata.shaper; + var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 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); + } + + // Cursor at index 0 is two runs + { + // Get our run iterator + var shaper = testdata.shaper; + var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null, 0); + var count: usize = 0; + while (try it.next(alloc)) |run| { + count += 1; + _ = try shaper.shape(run); + } + try testing.expectEqual(@as(usize, 2), count); + } + + // Cursor at index 1 is three runs + { + // Get our run iterator + var shaper = testdata.shaper; + var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null, 1); + var count: usize = 0; + while (try it.next(alloc)) |run| { + count += 1; + _ = try shaper.shape(run); + } + try testing.expectEqual(@as(usize, 3), count); + } + + // Cursor at last col is two runs + { + // Get our run iterator + var shaper = testdata.shaper; + var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null, 9); + var count: usize = 0; + while (try it.next(alloc)) |run| { + count += 1; + _ = try shaper.shape(run); + } + try testing.expectEqual(@as(usize, 2), count); + } +} + const TestShaper = struct { alloc: Allocator, shaper: Shaper, diff --git a/src/font/shaper/run.zig b/src/font/shaper/run.zig index 7d9cbac66..bd6dc9383 100644 --- a/src/font/shaper/run.zig +++ b/src/font/shaper/run.zig @@ -29,6 +29,7 @@ pub const RunIterator = struct { group: *font.GroupCache, row: terminal.Screen.Row, selection: ?terminal.Selection = null, + cursor_x: ?usize = null, i: usize = 0, pub fn next(self: *RunIterator, alloc: Allocator) !?TextRun { @@ -67,6 +68,32 @@ pub const RunIterator = struct { } } + // If our cursor is on this line then we break the run around the + // cursor. This means that any row with a cursor has at least + // three breaks: before, exactly the cursor, and after. + if (self.cursor_x) |cursor_x| { + // Exactly: self.i is the cursor and we iterated once. This + // means that we started exactly at the cursor and did at + // least one (probably exactly one) iteration. That is + // exactly one character. + if (self.i == cursor_x and j > self.i) { + assert(j == self.i + 1); + break; + } + + // Before: up to and not including the cursor. This means + // that we started before the cursor (self.i < cursor_x) + // and j is now at the cursor meaning we haven't yet processed + // the cursor. + if (self.i < cursor_x and j == cursor_x) { + assert(j > 0); + break; + } + + // After: after the cursor. We don't need to do anything + // special, we just let the run complete. + } + // 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 991929488..4e46578e1 100644 --- a/src/font/shaper/web_canvas.zig +++ b/src/font/shaper/web_canvas.zig @@ -60,12 +60,14 @@ pub const Shaper = struct { group: *font.GroupCache, row: terminal.Screen.Row, selection: ?terminal.Selection, + cursor_x: ?usize, ) font.shape.RunIterator { return .{ .hooks = .{ .shaper = self }, .group = group, .row = row, .selection = selection, + .cursor_x = cursor_x, }; } @@ -289,7 +291,7 @@ pub const Wasm = struct { while (rowIter.next()) |row| { defer y += 1; - var iter = self.runIterator(group, row, null); + var iter = self.runIterator(group, row, null, null); while (try iter.next(alloc)) |run| { const cells = try self.shape(run); log.info("y={} run={d} shape={any} idx={}", .{ diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index d936ac244..6a7561ecb 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -871,15 +871,31 @@ fn rebuildCells( while (rowIter.next()) |row| { defer y += 1; - // If this is the row with our cursor, then we may have to modify - // the cell with the cursor. - const start_i: usize = self.cells.items.len; - defer if (draw_cursor and + // True if this is the row with our cursor. There are a lot of conditions + // here because the reasons we need to know this are primarily to invert. + // + // - If we aren't drawing the cursor (draw_cursor), then we don't need + // to change our rendering. + // - If the cursor is not visible, then we don't need to change rendering. + // - If the cursor style is not a box, then we don't need to change + // rendering because it'll never fully overlap a glyph. + // - If the viewport is not at the bottom, then we don't need to + // change rendering because the cursor is not visible. + // (NOTE: this may not be fully correct, we may be scrolled + // slightly up and the cursor may be visible) + // - If this y doesn't match our cursor y then we don't need to + // change rendering. + // + const cursor_row = draw_cursor and self.cursor_visible and self.cursor_style == .box and screen.viewportIsBottom() and - y == screen.cursor.y) - { + y == screen.cursor.y; + + // If this is the row with our cursor, then we may have to modify + // the cell with the cursor. + const start_i: usize = self.cells.items.len; + defer if (cursor_row) { for (self.cells.items[start_i..]) |cell| { if (cell.grid_pos[0] == @as(f32, @floatFromInt(screen.cursor.x)) and cell.mode == .fg) @@ -907,7 +923,12 @@ fn rebuildCells( }; // Split our row into runs and shape each one. - var iter = self.font_shaper.runIterator(self.font_group, row, row_selection); + var iter = self.font_shaper.runIterator( + self.font_group, + row, + row_selection, + if (cursor_row) screen.cursor.x else null, + ); while (try iter.next(self.alloc)) |run| { for (try self.font_shaper.shape(run)) |shaper_cell| { if (self.updateCell( diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 81bdb7fdc..235a34841 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -903,15 +903,17 @@ pub fn rebuildCells( break :sel null; }; - // If this is the row with our cursor, then we may have to modify - // the cell with the cursor. - const start_i: usize = self.cells.items.len; - defer if (draw_cursor and + // See Metal.zig + const cursor_row = draw_cursor and self.cursor_visible and self.cursor_style == .box and screen.viewportIsBottom() and - y == screen.cursor.y) - { + y == screen.cursor.y; + + // If this is the row with our cursor, then we may have to modify + // the cell with the cursor. + const start_i: usize = self.cells.items.len; + defer if (cursor_row) { for (self.cells.items[start_i..]) |cell| { if (cell.grid_col == screen.cursor.x and cell.mode == .fg) @@ -942,7 +944,12 @@ 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, selection); + var iter = self.font_shaper.runIterator( + self.font_group, + row, + selection, + if (cursor_row) screen.cursor.x else null, + ); while (try iter.next(self.alloc)) |run| { for (try self.font_shaper.shape(run)) |shaper_cell| { if (self.updateCell(