mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-20 18:56:08 +03:00
font/shaper: do not break on merged emoji if cursor is directly on it
This commit is contained in:
@ -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,
|
||||||
|
@ -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.
|
||||||
|
Reference in New Issue
Block a user