Introduce `font-shaping-break` config option

pull/5374/head
Daniel Patterson 2025-01-26 00:30:18 +00:00 committed by Mitchell Hashimoto
parent 2592286988
commit beb961fb80
7 changed files with 436 additions and 120 deletions

View File

@ -20,6 +20,7 @@ pub const ConfirmCloseSurface = Config.ConfirmCloseSurface;
pub const CopyOnSelect = Config.CopyOnSelect; pub const CopyOnSelect = Config.CopyOnSelect;
pub const CustomShaderAnimation = Config.CustomShaderAnimation; pub const CustomShaderAnimation = Config.CustomShaderAnimation;
pub const FontSyntheticStyle = Config.FontSyntheticStyle; pub const FontSyntheticStyle = Config.FontSyntheticStyle;
pub const FontShapingBreak = Config.FontShapingBreak;
pub const FontStyle = Config.FontStyle; pub const FontStyle = Config.FontStyle;
pub const FreetypeLoadFlags = Config.FreetypeLoadFlags; pub const FreetypeLoadFlags = Config.FreetypeLoadFlags;
pub const Keybinds = Config.Keybinds; pub const Keybinds = Config.Keybinds;

View File

@ -270,6 +270,32 @@ pub const compatibility = std.StaticStringMap(
/// This is currently only supported on macOS. /// This is currently only supported on macOS.
@"font-thicken-strength": u8 = 255, @"font-thicken-strength": u8 = 255,
/// Locations to break font shaping into multiple runs.
///
/// A "run" is a contiguous segment of text that is shaped together. "Shaping"
/// is the process of converting text (codepoints) into glyphs (renderable
/// characters). This is how ligatures are formed, among other things.
/// For example, if a coding font turns "!=" into a single glyph, then it
/// must see "!" and "=" next to each other in a single run. When a run
/// is broken, the text is shaped separately. To continue our example, if
/// "!" is at the end of one run and "=" is at the start of the next run,
/// then the ligature will not be formed.
///
/// Ghostty breaks runs at certain points to improve readability or usability.
/// For example, Ghostty by default will break runs under the cursor so that
/// text editing can see the individual characters rather than a ligature.
/// This configuration lets you configure this behavior.
///
/// Combine values with a comma to set multiple options. Prefix an
/// option with "no-" to disable it. Enabling and disabling options
/// can be done at the same time.
///
/// Available options:
///
/// * `cursor` - Break runs under the cursor.
///
@"font-shaping-break": FontShapingBreak = .{},
/// What color space to use when performing alpha blending. /// What color space to use when performing alpha blending.
/// ///
/// This affects the appearance of text and of any images with transparency. /// This affects the appearance of text and of any images with transparency.
@ -6214,6 +6240,11 @@ pub const FontSyntheticStyle = packed struct {
@"bold-italic": bool = true, @"bold-italic": bool = true,
}; };
/// See "font-shaping-break" for documentation
pub const FontShapingBreak = packed struct {
cursor: bool = true,
};
/// See "link" for documentation. /// See "link" for documentation.
pub const RepeatableLink = struct { pub const RepeatableLink = struct {
const Self = @This(); const Self = @This();

View File

@ -7,6 +7,7 @@ const trace = @import("tracy").trace;
const font = @import("../main.zig"); const font = @import("../main.zig");
const os = @import("../../os/main.zig"); const os = @import("../../os/main.zig");
const terminal = @import("../../terminal/main.zig"); const terminal = @import("../../terminal/main.zig");
const config = @import("../../config.zig");
const Feature = font.shape.Feature; const Feature = font.shape.Feature;
const FeatureList = font.shape.FeatureList; const FeatureList = font.shape.FeatureList;
const default_features = font.shape.default_features; const default_features = font.shape.default_features;
@ -293,6 +294,7 @@ pub const Shaper = struct {
row: terminal.Pin, row: terminal.Pin,
selection: ?terminal.Selection, selection: ?terminal.Selection,
cursor_x: ?usize, cursor_x: ?usize,
break_config: config.FontShapingBreak,
) font.shape.RunIterator { ) font.shape.RunIterator {
return .{ return .{
.hooks = .{ .shaper = self }, .hooks = .{ .shaper = self },
@ -301,6 +303,7 @@ pub const Shaper = struct {
.row = row, .row = row,
.selection = selection, .selection = selection,
.cursor_x = cursor_x, .cursor_x = cursor_x,
.break_config = break_config,
}; };
} }
@ -600,6 +603,7 @@ test "run iterator" {
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, null,
null, null,
.{},
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |_| count += 1; while (try it.next(alloc)) |_| count += 1;
@ -619,6 +623,7 @@ test "run iterator" {
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, null,
null, null,
.{},
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |_| count += 1; while (try it.next(alloc)) |_| count += 1;
@ -639,6 +644,7 @@ test "run iterator" {
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, null,
null, null,
.{},
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |_| count += 1; while (try it.next(alloc)) |_| count += 1;
@ -660,6 +666,7 @@ test "run iterator" {
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, null,
null, null,
.{},
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |_| count += 1; while (try it.next(alloc)) |_| count += 1;
@ -707,6 +714,7 @@ test "run iterator: empty cells with background set" {
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, null,
null, null,
.{},
); );
{ {
const run = (try it.next(alloc)).?; const run = (try it.next(alloc)).?;
@ -743,6 +751,7 @@ test "shape" {
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, null,
null, null,
.{},
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
@ -778,6 +787,7 @@ test "shape nerd fonts" {
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, null,
null, null,
.{},
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
@ -806,6 +816,7 @@ test "shape inconsolata ligs" {
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, null,
null, null,
.{},
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
@ -831,6 +842,7 @@ test "shape inconsolata ligs" {
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, null,
null, null,
.{},
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
@ -864,6 +876,7 @@ test "shape monaspace ligs" {
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, null,
null, null,
.{},
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
@ -898,6 +911,7 @@ test "shape left-replaced lig in last run" {
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, null,
null, null,
.{},
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
@ -932,6 +946,7 @@ test "shape left-replaced lig in early run" {
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, null,
null, null,
.{},
); );
const run = (try it.next(alloc)).?; const run = (try it.next(alloc)).?;
@ -963,6 +978,7 @@ test "shape U+3C9 with JB Mono" {
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, null,
null, null,
.{},
); );
var run_count: usize = 0; var run_count: usize = 0;
@ -996,6 +1012,7 @@ test "shape emoji width" {
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, null,
null, null,
.{},
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
@ -1046,6 +1063,7 @@ test "shape emoji width long" {
screen.pages.pin(.{ .screen = .{ .y = 1 } }).?, screen.pages.pin(.{ .screen = .{ .y = 1 } }).?,
null, null,
null, null,
.{},
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
@ -1082,6 +1100,7 @@ test "shape variation selector VS15" {
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, null,
null, null,
.{},
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
@ -1117,6 +1136,7 @@ test "shape variation selector VS16" {
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, null,
null, null,
.{},
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
@ -1149,6 +1169,7 @@ test "shape with empty cells in between" {
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, null,
null, null,
.{},
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
@ -1187,6 +1208,7 @@ test "shape Chinese characters" {
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, null,
null, null,
.{},
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
@ -1227,6 +1249,7 @@ test "shape box glyphs" {
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, null,
null, null,
.{},
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
@ -1267,6 +1290,7 @@ test "shape selection boundary" {
false, false,
), ),
null, null,
.{},
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
@ -1290,6 +1314,7 @@ test "shape selection boundary" {
false, false,
), ),
null, null,
.{},
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
@ -1313,6 +1338,7 @@ test "shape selection boundary" {
false, false,
), ),
null, null,
.{},
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
@ -1336,6 +1362,7 @@ test "shape selection boundary" {
false, false,
), ),
null, null,
.{},
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
@ -1359,6 +1386,7 @@ test "shape selection boundary" {
false, false,
), ),
null, null,
.{},
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
@ -1391,6 +1419,7 @@ test "shape cursor boundary" {
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, null,
null, null,
.{},
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
@ -1400,61 +1429,126 @@ test "shape cursor boundary" {
try testing.expectEqual(@as(usize, 1), count); try testing.expectEqual(@as(usize, 1), count);
} }
// Cursor at index 0 is two runs
{ {
// Get our run iterator // Cursor at index 0 is two runs
var shaper = &testdata.shaper; {
var it = shaper.runIterator( // Get our run iterator
testdata.grid, var shaper = &testdata.shaper;
&screen, var it = shaper.runIterator(
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, testdata.grid,
null, &screen,
0, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
); null,
var count: usize = 0; 0,
while (try it.next(alloc)) |run| { .{ .cursor = true },
count += 1; );
_ = try shaper.shape(run); var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
_ = try shaper.shape(run);
}
try testing.expectEqual(@as(usize, 2), count);
}
// And without cursor splitting remains one
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
0,
.{ .cursor = false },
);
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
_ = try shaper.shape(run);
}
try testing.expectEqual(@as(usize, 1), count);
} }
try testing.expectEqual(@as(usize, 2), count);
} }
// Cursor at index 1 is three runs
{ {
// Get our run iterator // Cursor at index 1 is three runs
var shaper = &testdata.shaper; {
var it = shaper.runIterator( // Get our run iterator
testdata.grid, var shaper = &testdata.shaper;
&screen, var it = shaper.runIterator(
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, testdata.grid,
null, &screen,
1, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
); null,
var count: usize = 0; 1,
while (try it.next(alloc)) |run| { .{ .cursor = true },
count += 1; );
_ = try shaper.shape(run); var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
_ = try shaper.shape(run);
}
try testing.expectEqual(@as(usize, 3), count);
}
// And without cursor splitting remains one
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
1,
.{ .cursor = false },
);
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
_ = try shaper.shape(run);
}
try testing.expectEqual(@as(usize, 1), count);
} }
try testing.expectEqual(@as(usize, 3), count);
} }
// Cursor at last col is two runs
{ {
// Get our run iterator // Cursor at last col is two runs
var shaper = &testdata.shaper; {
var it = shaper.runIterator( // Get our run iterator
testdata.grid, var shaper = &testdata.shaper;
&screen, var it = shaper.runIterator(
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, testdata.grid,
null, &screen,
9, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
); null,
var count: usize = 0; 9,
while (try it.next(alloc)) |run| { .{ .cursor = true },
count += 1; );
_ = try shaper.shape(run); var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
_ = try shaper.shape(run);
}
try testing.expectEqual(@as(usize, 2), count);
}
// And without cursor splitting remains one
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
9,
.{ .cursor = false },
);
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
_ = try shaper.shape(run);
}
try testing.expectEqual(@as(usize, 1), count);
} }
try testing.expectEqual(@as(usize, 2), count);
} }
} }
@ -1480,6 +1574,7 @@ test "shape cursor boundary and colored emoji" {
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, null,
null, null,
.{},
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
@ -1499,6 +1594,25 @@ test "shape cursor boundary and colored emoji" {
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, null,
0, 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.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
0,
.{ .cursor = false },
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
@ -1516,6 +1630,25 @@ test "shape cursor boundary and colored emoji" {
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, null,
1, 1,
.{},
);
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.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
1,
.{ .cursor = false },
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
@ -1546,6 +1679,7 @@ test "shape cell attribute change" {
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, null,
null, null,
.{},
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
@ -1570,6 +1704,7 @@ test "shape cell attribute change" {
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, null,
null, null,
.{},
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
@ -1595,6 +1730,7 @@ test "shape cell attribute change" {
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, null,
null, null,
.{},
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
@ -1620,6 +1756,7 @@ test "shape cell attribute change" {
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, null,
null, null,
.{},
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
@ -1644,6 +1781,7 @@ test "shape cell attribute change" {
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, null,
null, null,
.{},
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
@ -1684,6 +1822,7 @@ test "shape high plane sprite font codepoint" {
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, null,
null, null,
.{},
); );
// We should get one run // We should get one run
const run = (try it.next(alloc)).?; const run = (try it.next(alloc)).?;

View File

@ -4,6 +4,7 @@ const Allocator = std.mem.Allocator;
const harfbuzz = @import("harfbuzz"); const harfbuzz = @import("harfbuzz");
const font = @import("../main.zig"); const font = @import("../main.zig");
const terminal = @import("../../terminal/main.zig"); const terminal = @import("../../terminal/main.zig");
const config = @import("../../config.zig");
const Feature = font.shape.Feature; const Feature = font.shape.Feature;
const FeatureList = font.shape.FeatureList; const FeatureList = font.shape.FeatureList;
const default_features = font.shape.default_features; const default_features = font.shape.default_features;
@ -94,6 +95,7 @@ pub const Shaper = struct {
row: terminal.Pin, row: terminal.Pin,
selection: ?terminal.Selection, selection: ?terminal.Selection,
cursor_x: ?usize, cursor_x: ?usize,
break_config: config.FontShapingBreak,
) font.shape.RunIterator { ) font.shape.RunIterator {
return .{ return .{
.hooks = .{ .shaper = self }, .hooks = .{ .shaper = self },
@ -102,6 +104,7 @@ pub const Shaper = struct {
.row = row, .row = row,
.selection = selection, .selection = selection,
.cursor_x = cursor_x, .cursor_x = cursor_x,
.break_config = break_config,
}; };
} }
@ -231,6 +234,7 @@ test "run iterator" {
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, null,
null, null,
.{},
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |_| count += 1; while (try it.next(alloc)) |_| count += 1;
@ -250,6 +254,7 @@ test "run iterator" {
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, null,
null, null,
.{},
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |_| count += 1; while (try it.next(alloc)) |_| count += 1;
@ -270,6 +275,7 @@ test "run iterator" {
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, null,
null, null,
.{},
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |_| { while (try it.next(alloc)) |_| {
@ -322,6 +328,7 @@ test "run iterator: empty cells with background set" {
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, null,
null, null,
.{},
); );
{ {
const run = (try it.next(alloc)).?; const run = (try it.next(alloc)).?;
@ -359,6 +366,7 @@ test "shape" {
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, null,
null, null,
.{},
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
@ -388,6 +396,7 @@ test "shape inconsolata ligs" {
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, null,
null, null,
.{},
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
@ -413,6 +422,7 @@ test "shape inconsolata ligs" {
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, null,
null, null,
.{},
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
@ -446,6 +456,7 @@ test "shape monaspace ligs" {
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, null,
null, null,
.{},
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
@ -482,6 +493,7 @@ test "shape arabic forced LTR" {
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, null,
null, null,
.{},
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
@ -519,6 +531,7 @@ test "shape emoji width" {
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, null,
null, null,
.{},
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
@ -571,6 +584,7 @@ test "shape emoji width long" {
screen.pages.pin(.{ .screen = .{ .y = 1 } }).?, screen.pages.pin(.{ .screen = .{ .y = 1 } }).?,
null, null,
null, null,
.{},
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
@ -609,6 +623,7 @@ test "shape variation selector VS15" {
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, null,
null, null,
.{},
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
@ -646,6 +661,7 @@ test "shape variation selector VS16" {
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, null,
null, null,
.{},
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
@ -680,6 +696,7 @@ test "shape with empty cells in between" {
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, null,
null, null,
.{},
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
@ -718,6 +735,7 @@ test "shape Chinese characters" {
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, null,
null, null,
.{},
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
@ -758,6 +776,7 @@ test "shape box glyphs" {
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, null,
null, null,
.{},
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
@ -799,6 +818,7 @@ test "shape selection boundary" {
false, false,
), ),
null, null,
.{},
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
@ -822,6 +842,7 @@ test "shape selection boundary" {
false, false,
), ),
null, null,
.{},
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
@ -845,6 +866,7 @@ test "shape selection boundary" {
false, false,
), ),
null, null,
.{},
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
@ -868,6 +890,7 @@ test "shape selection boundary" {
false, false,
), ),
null, null,
.{},
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
@ -891,6 +914,7 @@ test "shape selection boundary" {
false, false,
), ),
null, null,
.{},
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
@ -923,6 +947,7 @@ test "shape cursor boundary" {
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, null,
null, null,
.{},
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
@ -932,61 +957,126 @@ test "shape cursor boundary" {
try testing.expectEqual(@as(usize, 1), count); try testing.expectEqual(@as(usize, 1), count);
} }
// Cursor at index 0 is two runs
{ {
// Get our run iterator // Cursor at index 0 is two runs
var shaper = &testdata.shaper; {
var it = shaper.runIterator( // Get our run iterator
testdata.grid, var shaper = &testdata.shaper;
&screen, var it = shaper.runIterator(
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, testdata.grid,
null, &screen,
0, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
); null,
var count: usize = 0; 0,
while (try it.next(alloc)) |run| { .{ .cursor = true },
count += 1; );
_ = try shaper.shape(run); var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
_ = try shaper.shape(run);
}
try testing.expectEqual(@as(usize, 2), count);
}
// And without cursor splitting remains one
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
0,
.{ .cursor = false },
);
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
_ = try shaper.shape(run);
}
try testing.expectEqual(@as(usize, 1), count);
} }
try testing.expectEqual(@as(usize, 2), count);
} }
// Cursor at index 1 is three runs
{ {
// Get our run iterator // Cursor at index 1 is three runs
var shaper = &testdata.shaper; {
var it = shaper.runIterator( // Get our run iterator
testdata.grid, var shaper = &testdata.shaper;
&screen, var it = shaper.runIterator(
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, testdata.grid,
null, &screen,
1, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
); null,
var count: usize = 0; 1,
while (try it.next(alloc)) |run| { .{ .cursor = true },
count += 1; );
_ = try shaper.shape(run); var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
_ = try shaper.shape(run);
}
try testing.expectEqual(@as(usize, 3), count);
}
// And without cursor splitting remains one
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
1,
.{ .cursor = false },
);
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
_ = try shaper.shape(run);
}
try testing.expectEqual(@as(usize, 1), count);
} }
try testing.expectEqual(@as(usize, 3), count);
} }
// Cursor at last col is two runs
{ {
// Get our run iterator // Cursor at last col is two runs
var shaper = &testdata.shaper; {
var it = shaper.runIterator( // Get our run iterator
testdata.grid, var shaper = &testdata.shaper;
&screen, var it = shaper.runIterator(
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, testdata.grid,
null, &screen,
9, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
); null,
var count: usize = 0; 9,
while (try it.next(alloc)) |run| { .{ .cursor = true },
count += 1; );
_ = try shaper.shape(run); var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
_ = try shaper.shape(run);
}
try testing.expectEqual(@as(usize, 2), count);
}
// And without cursor splitting remains one
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
9,
.{ .cursor = false },
);
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
_ = try shaper.shape(run);
}
try testing.expectEqual(@as(usize, 1), count);
} }
try testing.expectEqual(@as(usize, 2), count);
} }
} }
@ -1012,6 +1102,7 @@ test "shape cursor boundary and colored emoji" {
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, null,
null, null,
.{},
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
@ -1031,6 +1122,25 @@ test "shape cursor boundary and colored emoji" {
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, null,
0, 0,
.{ .cursor = true },
);
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.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
0,
.{ .cursor = false },
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
@ -1048,6 +1158,25 @@ test "shape cursor boundary and colored emoji" {
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, null,
1, 1,
.{ .cursor = true },
);
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.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
1,
.{ .cursor = false },
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
@ -1078,6 +1207,7 @@ test "shape cell attribute change" {
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, null,
null, null,
.{},
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
@ -1102,6 +1232,7 @@ test "shape cell attribute change" {
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, null,
null, null,
.{},
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
@ -1127,6 +1258,7 @@ test "shape cell attribute change" {
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, null,
null, null,
.{},
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
@ -1152,6 +1284,7 @@ test "shape cell attribute change" {
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, null,
null, null,
.{},
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
@ -1176,6 +1309,7 @@ test "shape cell attribute change" {
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, null,
null, null,
.{},
); );
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {

View File

@ -3,6 +3,7 @@ const assert = std.debug.assert;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const trace = @import("tracy").trace; const trace = @import("tracy").trace;
const font = @import("../main.zig"); const font = @import("../main.zig");
const config = @import("../../config.zig");
const Face = font.Face; const Face = font.Face;
const Collection = font.Collection; const Collection = font.Collection;
const DeferredFace = font.DeferredFace; const DeferredFace = font.DeferredFace;
@ -75,6 +76,7 @@ pub const Shaper = struct {
row: terminal.Pin, row: terminal.Pin,
selection: ?terminal.Selection, selection: ?terminal.Selection,
cursor_x: ?usize, cursor_x: ?usize,
break_config: config.FontShapingBreak,
) font.shape.RunIterator { ) font.shape.RunIterator {
return .{ return .{
.hooks = .{ .shaper = self }, .hooks = .{ .shaper = self },
@ -83,6 +85,7 @@ pub const Shaper = struct {
.row = row, .row = row,
.selection = selection, .selection = selection,
.cursor_x = cursor_x, .cursor_x = cursor_x,
.break_config = break_config,
}; };
} }

View File

@ -6,6 +6,8 @@ const shape = @import("../shape.zig");
const terminal = @import("../../terminal/main.zig"); const terminal = @import("../../terminal/main.zig");
const autoHash = std.hash.autoHash; const autoHash = std.hash.autoHash;
const Hasher = std.hash.Wyhash; const Hasher = std.hash.Wyhash;
const configpkg = @import("../../config.zig");
const Config = configpkg.Config;
/// A single text run. A text run is only valid for one Shaper instance and /// A single text run. A text run is only valid for one Shaper instance and
/// until the next run is created. A text run never goes across multiple /// until the next run is created. A text run never goes across multiple
@ -40,6 +42,7 @@ pub const RunIterator = struct {
row: terminal.Pin, row: terminal.Pin,
selection: ?terminal.Selection = null, selection: ?terminal.Selection = null,
cursor_x: ?usize = null, cursor_x: ?usize = null,
break_config: configpkg.FontShapingBreak,
i: usize = 0, i: usize = 0,
pub fn next(self: *RunIterator, alloc: Allocator) !?TextRun { pub fn next(self: *RunIterator, alloc: Allocator) !?TextRun {
@ -175,36 +178,38 @@ pub const RunIterator = struct {
break :emoji null; break :emoji null;
}; };
// If our cursor is on this line then we break the run around the if (self.break_config.cursor) {
// cursor. This means that any row with a cursor has at least // If our cursor is on this line then we break the run around the
// three breaks: before, exactly the cursor, and after. // 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 // We do not break a cell that is exactly the grapheme. If there
// break. This creates an effect where hovering over an emoji // are cells following that contain joiners, we allow those to
// such as a skin-tone emoji is fine, but hovering over the // break. This creates an effect where hovering over an emoji
// joiners will show the joiners allowing you to modify the // such as a skin-tone emoji is fine, but hovering over the
// emoji. // joiners will show the joiners allowing you to modify the
if (!cell.hasGrapheme()) { // emoji.
if (self.cursor_x) |cursor_x| { if (!cell.hasGrapheme()) {
// Exactly: self.i is the cursor and we iterated once. This if (self.cursor_x) |cursor_x| {
// means that we started exactly at the cursor and did at // Exactly: self.i is the cursor and we iterated once. This
// exactly one iteration. Why exactly one? Because we may // means that we started exactly at the cursor and did at
// start at our cursor but do many if our cursor is exactly // exactly one iteration. Why exactly one? Because we may
// on an emoji. // start at our cursor but do many if our cursor is exactly
if (self.i == cursor_x and j == self.i + 1) break; // on an emoji.
if (self.i == cursor_x and j == self.i + 1) break;
// Before: up to and not including the cursor. This means // Before: up to and not including the cursor. This means
// that we started before the cursor (self.i < cursor_x) // that we started before the cursor (self.i < cursor_x)
// and j is now at the cursor meaning we haven't yet processed // and j is now at the cursor meaning we haven't yet processed
// the cursor. // the cursor.
if (self.i < cursor_x and j == cursor_x) { if (self.i < cursor_x and j == cursor_x) {
assert(j > 0); assert(j > 0);
break; break;
}
// After: after the cursor. We don't need to do anything
// special, we just let the run complete.
} }
// After: after the cursor. We don't need to do anything
// special, we just let the run complete.
} }
} }

View File

@ -4,6 +4,7 @@ const Allocator = std.mem.Allocator;
const ziglyph = @import("ziglyph"); const ziglyph = @import("ziglyph");
const font = @import("../main.zig"); const font = @import("../main.zig");
const terminal = @import("../../terminal/main.zig"); const terminal = @import("../../terminal/main.zig");
const config = @import("../../config.zig");
const log = std.log.scoped(.font_shaper); const log = std.log.scoped(.font_shaper);
@ -65,6 +66,7 @@ pub const Shaper = struct {
row: terminal.Screen.Row, row: terminal.Screen.Row,
selection: ?terminal.Selection, selection: ?terminal.Selection,
cursor_x: ?usize, cursor_x: ?usize,
break_config: config.FontShapingBreak,
) font.shape.RunIterator { ) font.shape.RunIterator {
return .{ return .{
.hooks = .{ .shaper = self }, .hooks = .{ .shaper = self },
@ -72,6 +74,7 @@ pub const Shaper = struct {
.row = row, .row = row,
.selection = selection, .selection = selection,
.cursor_x = cursor_x, .cursor_x = cursor_x,
.break_config = break_config,
}; };
} }