mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-15 08:16:13 +03:00
font/shaper: split runs at selection boundaries
This commit is contained in:
@ -43,12 +43,22 @@ pub const Shaper = struct {
|
|||||||
/// Returns an iterator that returns one text run at a time for the
|
/// 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
|
/// given terminal row. Note that text runs are are only valid one at a time
|
||||||
/// for a Shaper struct since they share state.
|
/// 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(
|
pub fn runIterator(
|
||||||
self: *Shaper,
|
self: *Shaper,
|
||||||
group: *GroupCache,
|
group: *GroupCache,
|
||||||
row: terminal.Screen.Row,
|
row: terminal.Screen.Row,
|
||||||
|
selection: ?terminal.Selection,
|
||||||
) font.shape.RunIterator {
|
) 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
|
/// Shape the given text run. The text run must be the immediately previous
|
||||||
@ -136,7 +146,7 @@ test "run iterator" {
|
|||||||
|
|
||||||
// Get our run iterator
|
// Get our run iterator
|
||||||
var shaper = testdata.shaper;
|
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;
|
var count: usize = 0;
|
||||||
while (try it.next(alloc)) |_| count += 1;
|
while (try it.next(alloc)) |_| count += 1;
|
||||||
try testing.expectEqual(@as(usize, 1), count);
|
try testing.expectEqual(@as(usize, 1), count);
|
||||||
@ -149,7 +159,7 @@ test "run iterator" {
|
|||||||
try screen.testWriteString("ABCD EFG");
|
try screen.testWriteString("ABCD EFG");
|
||||||
|
|
||||||
var shaper = testdata.shaper;
|
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;
|
var count: usize = 0;
|
||||||
while (try it.next(alloc)) |_| count += 1;
|
while (try it.next(alloc)) |_| count += 1;
|
||||||
try testing.expectEqual(@as(usize, 1), count);
|
try testing.expectEqual(@as(usize, 1), count);
|
||||||
@ -163,7 +173,7 @@ test "run iterator" {
|
|||||||
|
|
||||||
// Get our run iterator
|
// Get our run iterator
|
||||||
var shaper = testdata.shaper;
|
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;
|
var count: usize = 0;
|
||||||
while (try it.next(alloc)) |_| {
|
while (try it.next(alloc)) |_| {
|
||||||
count += 1;
|
count += 1;
|
||||||
@ -197,7 +207,7 @@ test "run iterator: empty cells with background set" {
|
|||||||
|
|
||||||
// Get our run iterator
|
// Get our run iterator
|
||||||
var shaper = testdata.shaper;
|
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;
|
var count: usize = 0;
|
||||||
while (try it.next(alloc)) |run| {
|
while (try it.next(alloc)) |run| {
|
||||||
count += 1;
|
count += 1;
|
||||||
@ -211,6 +221,7 @@ test "run iterator: empty cells with background set" {
|
|||||||
try testing.expectEqual(@as(usize, 1), count);
|
try testing.expectEqual(@as(usize, 1), count);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test "shape" {
|
test "shape" {
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
const alloc = testing.allocator;
|
const alloc = testing.allocator;
|
||||||
@ -231,7 +242,7 @@ test "shape" {
|
|||||||
|
|
||||||
// Get our run iterator
|
// Get our run iterator
|
||||||
var shaper = testdata.shaper;
|
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;
|
var count: usize = 0;
|
||||||
while (try it.next(alloc)) |run| {
|
while (try it.next(alloc)) |run| {
|
||||||
count += 1;
|
count += 1;
|
||||||
@ -254,7 +265,7 @@ test "shape inconsolata ligs" {
|
|||||||
try screen.testWriteString(">=");
|
try screen.testWriteString(">=");
|
||||||
|
|
||||||
var shaper = testdata.shaper;
|
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;
|
var count: usize = 0;
|
||||||
while (try it.next(alloc)) |run| {
|
while (try it.next(alloc)) |run| {
|
||||||
count += 1;
|
count += 1;
|
||||||
@ -271,7 +282,7 @@ test "shape inconsolata ligs" {
|
|||||||
try screen.testWriteString("===");
|
try screen.testWriteString("===");
|
||||||
|
|
||||||
var shaper = testdata.shaper;
|
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;
|
var count: usize = 0;
|
||||||
while (try it.next(alloc)) |run| {
|
while (try it.next(alloc)) |run| {
|
||||||
count += 1;
|
count += 1;
|
||||||
@ -296,7 +307,7 @@ test "shape emoji width" {
|
|||||||
try screen.testWriteString("👍");
|
try screen.testWriteString("👍");
|
||||||
|
|
||||||
var shaper = testdata.shaper;
|
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;
|
var count: usize = 0;
|
||||||
while (try it.next(alloc)) |run| {
|
while (try it.next(alloc)) |run| {
|
||||||
count += 1;
|
count += 1;
|
||||||
@ -330,7 +341,7 @@ test "shape emoji width long" {
|
|||||||
|
|
||||||
// Get our run iterator
|
// Get our run iterator
|
||||||
var shaper = testdata.shaper;
|
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;
|
var count: usize = 0;
|
||||||
while (try it.next(alloc)) |run| {
|
while (try it.next(alloc)) |run| {
|
||||||
count += 1;
|
count += 1;
|
||||||
@ -361,7 +372,7 @@ test "shape variation selector VS15" {
|
|||||||
|
|
||||||
// Get our run iterator
|
// Get our run iterator
|
||||||
var shaper = testdata.shaper;
|
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;
|
var count: usize = 0;
|
||||||
while (try it.next(alloc)) |run| {
|
while (try it.next(alloc)) |run| {
|
||||||
count += 1;
|
count += 1;
|
||||||
@ -392,7 +403,7 @@ test "shape variation selector VS16" {
|
|||||||
|
|
||||||
// Get our run iterator
|
// Get our run iterator
|
||||||
var shaper = testdata.shaper;
|
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;
|
var count: usize = 0;
|
||||||
while (try it.next(alloc)) |run| {
|
while (try it.next(alloc)) |run| {
|
||||||
count += 1;
|
count += 1;
|
||||||
@ -420,7 +431,7 @@ test "shape with empty cells in between" {
|
|||||||
|
|
||||||
// Get our run iterator
|
// Get our run iterator
|
||||||
var shaper = testdata.shaper;
|
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;
|
var count: usize = 0;
|
||||||
while (try it.next(alloc)) |run| {
|
while (try it.next(alloc)) |run| {
|
||||||
count += 1;
|
count += 1;
|
||||||
@ -452,7 +463,7 @@ test "shape Chinese characters" {
|
|||||||
|
|
||||||
// Get our run iterator
|
// Get our run iterator
|
||||||
var shaper = testdata.shaper;
|
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;
|
var count: usize = 0;
|
||||||
while (try it.next(alloc)) |run| {
|
while (try it.next(alloc)) |run| {
|
||||||
count += 1;
|
count += 1;
|
||||||
@ -493,7 +504,7 @@ test "shape box glyphs" {
|
|||||||
|
|
||||||
// Get our run iterator
|
// Get our run iterator
|
||||||
var shaper = testdata.shaper;
|
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;
|
var count: usize = 0;
|
||||||
while (try it.next(alloc)) |run| {
|
while (try it.next(alloc)) |run| {
|
||||||
count += 1;
|
count += 1;
|
||||||
@ -508,6 +519,99 @@ test "shape box glyphs" {
|
|||||||
try testing.expectEqual(@as(usize, 1), count);
|
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 {
|
const TestShaper = struct {
|
||||||
alloc: Allocator,
|
alloc: Allocator,
|
||||||
shaper: Shaper,
|
shaper: Shaper,
|
||||||
|
@ -28,6 +28,7 @@ pub const RunIterator = struct {
|
|||||||
hooks: font.Shaper.RunIteratorHook,
|
hooks: font.Shaper.RunIteratorHook,
|
||||||
group: *font.GroupCache,
|
group: *font.GroupCache,
|
||||||
row: terminal.Screen.Row,
|
row: terminal.Screen.Row,
|
||||||
|
selection: ?terminal.Selection = null,
|
||||||
i: usize = 0,
|
i: usize = 0,
|
||||||
|
|
||||||
pub fn next(self: *RunIterator, alloc: Allocator) !?TextRun {
|
pub fn next(self: *RunIterator, alloc: Allocator) !?TextRun {
|
||||||
@ -56,6 +57,16 @@ pub const RunIterator = struct {
|
|||||||
const cluster = j;
|
const cluster = j;
|
||||||
const cell = self.row.getCell(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 we're a spacer, then we ignore it
|
||||||
if (cell.attrs.wide_spacer_tail) continue;
|
if (cell.attrs.wide_spacer_tail) continue;
|
||||||
|
|
||||||
|
@ -57,8 +57,14 @@ pub const Shaper = struct {
|
|||||||
self: *Shaper,
|
self: *Shaper,
|
||||||
group: *font.GroupCache,
|
group: *font.GroupCache,
|
||||||
row: terminal.Screen.Row,
|
row: terminal.Screen.Row,
|
||||||
|
selection: ?terminal.Selection,
|
||||||
) font.shape.RunIterator {
|
) 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
|
/// Shape the given text run. The text run must be the immediately
|
||||||
|
@ -847,8 +847,9 @@ pub fn rebuildCells(
|
|||||||
defer y += 1;
|
defer y += 1;
|
||||||
|
|
||||||
// Our selection value is only non-null if this selection happens
|
// Our selection value is only non-null if this selection happens
|
||||||
// to contain this row. If the selection changes for any reason,
|
// to contain this row. This selection value will be set to only be
|
||||||
// then we invalidate the cache.
|
// 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: {
|
const selection = sel: {
|
||||||
if (term_selection) |sel| {
|
if (term_selection) |sel| {
|
||||||
const screen_point = (terminal.point.Viewport{
|
const screen_point = (terminal.point.Viewport{
|
||||||
@ -856,8 +857,10 @@ pub fn rebuildCells(
|
|||||||
.y = y,
|
.y = y,
|
||||||
}).toScreen(screen);
|
}).toScreen(screen);
|
||||||
|
|
||||||
// If we are selected, we our colors are just inverted fg/bg
|
// If we are selected, we our colors are just inverted fg/bg.
|
||||||
if (sel.containsRow(screen_point)) break :sel sel;
|
if (sel.containedRow(screen, screen_point)) |row_sel| {
|
||||||
|
break :sel row_sel;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
break :sel null;
|
break :sel null;
|
||||||
@ -901,7 +904,7 @@ pub fn rebuildCells(
|
|||||||
const start = self.cells.items.len;
|
const start = self.cells.items.len;
|
||||||
|
|
||||||
// Split our row into runs and shape each one.
|
// 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| {
|
while (try iter.next(self.alloc)) |run| {
|
||||||
for (try self.font_shaper.shape(run)) |shaper_cell| {
|
for (try self.font_shaper.shape(run)) |shaper_cell| {
|
||||||
if (self.updateCell(
|
if (self.updateCell(
|
||||||
|
@ -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).
|
/// 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 {
|
fn order(self: Selection) Order {
|
||||||
if (self.start.y < self.end.y) return .forward;
|
if (self.start.y < self.end.y) return .forward;
|
||||||
|
Reference in New Issue
Block a user