font/shaper: text runs should split around block cursors

Fixes #206
This commit is contained in:
Mitchell Hashimoto
2023-07-18 16:17:46 -07:00
parent 1aec2f2ca5
commit 4b062dc45c
5 changed files with 158 additions and 34 deletions

View File

@ -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,

View File

@ -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;

View File

@ -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={}", .{

View File

@ -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(

View File

@ -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(