font/shaper: do not break on merged emoji if cursor is directly on it

This commit is contained in:
Mitchell Hashimoto
2023-07-18 16:38:02 -07:00
parent 4b062dc45c
commit 4137c6cf69
2 changed files with 83 additions and 26 deletions

View File

@ -702,6 +702,56 @@ test "shape cursor boundary" {
} }
} }
test "shape cursor boundary and colored emoji" {
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("👍🏼");
// 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 on emoji does not split it
{
// 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, 1), count);
}
{
// 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, 1), count);
}
}
const TestShaper = struct { const TestShaper = struct {
alloc: Allocator, alloc: Allocator,
shaper: Shaper, shaper: Shaper,

View File

@ -68,32 +68,6 @@ 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 we're a spacer, then we ignore it
if (cell.attrs.wide_spacer_tail) continue; if (cell.attrs.wide_spacer_tail) continue;
@ -122,6 +96,39 @@ pub const RunIterator = struct {
break :p null; break :p null;
} else null; } else null;
// 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.
//
// We do not break a cell that is exactly the grapheme. If there
// are cells following that contain joiners, we allow those to
// break. This creates an effect where hovering over an emoji
// such as a skin-tone emoji is fine, but hovering over the
// joiners will show the joiners allowing you to modify the
// emoji.
if (!cell.attrs.grapheme) {
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
// exactly one iteration. Why exactly one? Because we may
// start at our cursor but do many if our cursor is exactly
// on an emoji.
if (self.i == cursor_x and 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.
}
}
// Determine the font for this cell. We'll use fallbacks // Determine the font for this cell. We'll use fallbacks
// manually here to try replacement chars and then a space // manually here to try replacement chars and then a space
// for unknown glyphs. // for unknown glyphs.