From beb961fb809ce0bb7dbb8c4ae243b19502742ed0 Mon Sep 17 00:00:00 2001 From: Daniel Patterson Date: Sun, 26 Jan 2025 00:30:18 +0000 Subject: [PATCH] Introduce `font-shaping-break` config option --- src/config.zig | 1 + src/config/Config.zig | 31 +++++ src/font/shaper/coretext.zig | 231 ++++++++++++++++++++++++++------- src/font/shaper/harfbuzz.zig | 226 +++++++++++++++++++++++++------- src/font/shaper/noop.zig | 3 + src/font/shaper/run.zig | 61 +++++---- src/font/shaper/web_canvas.zig | 3 + 7 files changed, 436 insertions(+), 120 deletions(-) diff --git a/src/config.zig b/src/config.zig index 7f390fb08..ac38eb89c 100644 --- a/src/config.zig +++ b/src/config.zig @@ -20,6 +20,7 @@ pub const ConfirmCloseSurface = Config.ConfirmCloseSurface; pub const CopyOnSelect = Config.CopyOnSelect; pub const CustomShaderAnimation = Config.CustomShaderAnimation; pub const FontSyntheticStyle = Config.FontSyntheticStyle; +pub const FontShapingBreak = Config.FontShapingBreak; pub const FontStyle = Config.FontStyle; pub const FreetypeLoadFlags = Config.FreetypeLoadFlags; pub const Keybinds = Config.Keybinds; diff --git a/src/config/Config.zig b/src/config/Config.zig index 14ab5219d..ef8f48ee9 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -270,6 +270,32 @@ pub const compatibility = std.StaticStringMap( /// This is currently only supported on macOS. @"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. /// /// 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, }; +/// See "font-shaping-break" for documentation +pub const FontShapingBreak = packed struct { + cursor: bool = true, +}; + /// See "link" for documentation. pub const RepeatableLink = struct { const Self = @This(); diff --git a/src/font/shaper/coretext.zig b/src/font/shaper/coretext.zig index 8e2c45c69..654af02d9 100644 --- a/src/font/shaper/coretext.zig +++ b/src/font/shaper/coretext.zig @@ -7,6 +7,7 @@ const trace = @import("tracy").trace; const font = @import("../main.zig"); const os = @import("../../os/main.zig"); const terminal = @import("../../terminal/main.zig"); +const config = @import("../../config.zig"); const Feature = font.shape.Feature; const FeatureList = font.shape.FeatureList; const default_features = font.shape.default_features; @@ -293,6 +294,7 @@ pub const Shaper = struct { row: terminal.Pin, selection: ?terminal.Selection, cursor_x: ?usize, + break_config: config.FontShapingBreak, ) font.shape.RunIterator { return .{ .hooks = .{ .shaper = self }, @@ -301,6 +303,7 @@ pub const Shaper = struct { .row = row, .selection = selection, .cursor_x = cursor_x, + .break_config = break_config, }; } @@ -600,6 +603,7 @@ test "run iterator" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |_| count += 1; @@ -619,6 +623,7 @@ test "run iterator" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |_| count += 1; @@ -639,6 +644,7 @@ test "run iterator" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |_| count += 1; @@ -660,6 +666,7 @@ test "run iterator" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; 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 } }).?, null, null, + .{}, ); { const run = (try it.next(alloc)).?; @@ -743,6 +751,7 @@ test "shape" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -778,6 +787,7 @@ test "shape nerd fonts" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -806,6 +816,7 @@ test "shape inconsolata ligs" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -831,6 +842,7 @@ test "shape inconsolata ligs" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -864,6 +876,7 @@ test "shape monaspace ligs" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -898,6 +911,7 @@ test "shape left-replaced lig in last run" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -932,6 +946,7 @@ test "shape left-replaced lig in early run" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); const run = (try it.next(alloc)).?; @@ -963,6 +978,7 @@ test "shape U+3C9 with JB Mono" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var run_count: usize = 0; @@ -996,6 +1012,7 @@ test "shape emoji width" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1046,6 +1063,7 @@ test "shape emoji width long" { screen.pages.pin(.{ .screen = .{ .y = 1 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1082,6 +1100,7 @@ test "shape variation selector VS15" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1117,6 +1136,7 @@ test "shape variation selector VS16" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1149,6 +1169,7 @@ test "shape with empty cells in between" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1187,6 +1208,7 @@ test "shape Chinese characters" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1227,6 +1249,7 @@ test "shape box glyphs" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1267,6 +1290,7 @@ test "shape selection boundary" { false, ), null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1290,6 +1314,7 @@ test "shape selection boundary" { false, ), null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1313,6 +1338,7 @@ test "shape selection boundary" { false, ), null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1336,6 +1362,7 @@ test "shape selection boundary" { false, ), null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1359,6 +1386,7 @@ test "shape selection boundary" { false, ), null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1391,6 +1419,7 @@ test "shape cursor boundary" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1400,61 +1429,126 @@ test "shape cursor boundary" { 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.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - 0, - ); - var count: usize = 0; - while (try it.next(alloc)) |run| { - count += 1; - _ = try shaper.shape(run); + // Cursor at index 0 is two runs + { + // Get our run iterator + var shaper = &testdata.shaper; + var it = shaper.runIterator( + testdata.grid, + &screen, + screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + null, + 0, + .{ .cursor = true }, + ); + 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 - var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - 1, - ); - var count: usize = 0; - while (try it.next(alloc)) |run| { - count += 1; - _ = try shaper.shape(run); + // Cursor at index 1 is three runs + { + // Get our run iterator + var shaper = &testdata.shaper; + var it = shaper.runIterator( + testdata.grid, + &screen, + screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + null, + 1, + .{ .cursor = true }, + ); + 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 - var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - 9, - ); - var count: usize = 0; - while (try it.next(alloc)) |run| { - count += 1; - _ = try shaper.shape(run); + // Cursor at last col is two runs + { + // Get our run iterator + var shaper = &testdata.shaper; + var it = shaper.runIterator( + testdata.grid, + &screen, + screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + null, + 9, + .{ .cursor = true }, + ); + 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 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1499,6 +1594,25 @@ test "shape cursor boundary and colored emoji" { screen.pages.pin(.{ .screen = .{ .y = 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.grid, + &screen, + screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + null, + 0, + .{ .cursor = false }, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1516,6 +1630,25 @@ test "shape cursor boundary and colored emoji" { screen.pages.pin(.{ .screen = .{ .y = 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); + } + { + // 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| { @@ -1546,6 +1679,7 @@ test "shape cell attribute change" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1570,6 +1704,7 @@ test "shape cell attribute change" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1595,6 +1730,7 @@ test "shape cell attribute change" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1620,6 +1756,7 @@ test "shape cell attribute change" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1644,6 +1781,7 @@ test "shape cell attribute change" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1684,6 +1822,7 @@ test "shape high plane sprite font codepoint" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); // We should get one run const run = (try it.next(alloc)).?; diff --git a/src/font/shaper/harfbuzz.zig b/src/font/shaper/harfbuzz.zig index 361cbbe93..56654e88f 100644 --- a/src/font/shaper/harfbuzz.zig +++ b/src/font/shaper/harfbuzz.zig @@ -4,6 +4,7 @@ const Allocator = std.mem.Allocator; const harfbuzz = @import("harfbuzz"); const font = @import("../main.zig"); const terminal = @import("../../terminal/main.zig"); +const config = @import("../../config.zig"); const Feature = font.shape.Feature; const FeatureList = font.shape.FeatureList; const default_features = font.shape.default_features; @@ -94,6 +95,7 @@ pub const Shaper = struct { row: terminal.Pin, selection: ?terminal.Selection, cursor_x: ?usize, + break_config: config.FontShapingBreak, ) font.shape.RunIterator { return .{ .hooks = .{ .shaper = self }, @@ -102,6 +104,7 @@ pub const Shaper = struct { .row = row, .selection = selection, .cursor_x = cursor_x, + .break_config = break_config, }; } @@ -231,6 +234,7 @@ test "run iterator" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |_| count += 1; @@ -250,6 +254,7 @@ test "run iterator" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |_| count += 1; @@ -270,6 +275,7 @@ test "run iterator" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |_| { @@ -322,6 +328,7 @@ test "run iterator: empty cells with background set" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); { const run = (try it.next(alloc)).?; @@ -359,6 +366,7 @@ test "shape" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -388,6 +396,7 @@ test "shape inconsolata ligs" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -413,6 +422,7 @@ test "shape inconsolata ligs" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -446,6 +456,7 @@ test "shape monaspace ligs" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -482,6 +493,7 @@ test "shape arabic forced LTR" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -519,6 +531,7 @@ test "shape emoji width" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -571,6 +584,7 @@ test "shape emoji width long" { screen.pages.pin(.{ .screen = .{ .y = 1 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -609,6 +623,7 @@ test "shape variation selector VS15" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -646,6 +661,7 @@ test "shape variation selector VS16" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -680,6 +696,7 @@ test "shape with empty cells in between" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -718,6 +735,7 @@ test "shape Chinese characters" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -758,6 +776,7 @@ test "shape box glyphs" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -799,6 +818,7 @@ test "shape selection boundary" { false, ), null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -822,6 +842,7 @@ test "shape selection boundary" { false, ), null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -845,6 +866,7 @@ test "shape selection boundary" { false, ), null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -868,6 +890,7 @@ test "shape selection boundary" { false, ), null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -891,6 +914,7 @@ test "shape selection boundary" { false, ), null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -923,6 +947,7 @@ test "shape cursor boundary" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -932,61 +957,126 @@ test "shape cursor boundary" { 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.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - 0, - ); - var count: usize = 0; - while (try it.next(alloc)) |run| { - count += 1; - _ = try shaper.shape(run); + // Cursor at index 0 is two runs + { + // Get our run iterator + var shaper = &testdata.shaper; + var it = shaper.runIterator( + testdata.grid, + &screen, + screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + null, + 0, + .{ .cursor = true }, + ); + 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 - var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - 1, - ); - var count: usize = 0; - while (try it.next(alloc)) |run| { - count += 1; - _ = try shaper.shape(run); + // Cursor at index 1 is three runs + { + // Get our run iterator + var shaper = &testdata.shaper; + var it = shaper.runIterator( + testdata.grid, + &screen, + screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + null, + 1, + .{ .cursor = true }, + ); + 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 - var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - 9, - ); - var count: usize = 0; - while (try it.next(alloc)) |run| { - count += 1; - _ = try shaper.shape(run); + // Cursor at last col is two runs + { + // Get our run iterator + var shaper = &testdata.shaper; + var it = shaper.runIterator( + testdata.grid, + &screen, + screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + null, + 9, + .{ .cursor = true }, + ); + 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 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1031,6 +1122,25 @@ test "shape cursor boundary and colored emoji" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, 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; while (try it.next(alloc)) |run| { @@ -1048,6 +1158,25 @@ test "shape cursor boundary and colored emoji" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, 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; while (try it.next(alloc)) |run| { @@ -1078,6 +1207,7 @@ test "shape cell attribute change" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1102,6 +1232,7 @@ test "shape cell attribute change" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1127,6 +1258,7 @@ test "shape cell attribute change" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1152,6 +1284,7 @@ test "shape cell attribute change" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1176,6 +1309,7 @@ test "shape cell attribute change" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { diff --git a/src/font/shaper/noop.zig b/src/font/shaper/noop.zig index f8988f4ee..1041954e6 100644 --- a/src/font/shaper/noop.zig +++ b/src/font/shaper/noop.zig @@ -3,6 +3,7 @@ const assert = std.debug.assert; const Allocator = std.mem.Allocator; const trace = @import("tracy").trace; const font = @import("../main.zig"); +const config = @import("../../config.zig"); const Face = font.Face; const Collection = font.Collection; const DeferredFace = font.DeferredFace; @@ -75,6 +76,7 @@ pub const Shaper = struct { row: terminal.Pin, selection: ?terminal.Selection, cursor_x: ?usize, + break_config: config.FontShapingBreak, ) font.shape.RunIterator { return .{ .hooks = .{ .shaper = self }, @@ -83,6 +85,7 @@ pub const Shaper = struct { .row = row, .selection = selection, .cursor_x = cursor_x, + .break_config = break_config, }; } diff --git a/src/font/shaper/run.zig b/src/font/shaper/run.zig index 18ddd4b56..a6ff79e39 100644 --- a/src/font/shaper/run.zig +++ b/src/font/shaper/run.zig @@ -6,6 +6,8 @@ const shape = @import("../shape.zig"); const terminal = @import("../../terminal/main.zig"); const autoHash = std.hash.autoHash; 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 /// until the next run is created. A text run never goes across multiple @@ -40,6 +42,7 @@ pub const RunIterator = struct { row: terminal.Pin, selection: ?terminal.Selection = null, cursor_x: ?usize = null, + break_config: configpkg.FontShapingBreak, i: usize = 0, pub fn next(self: *RunIterator, alloc: Allocator) !?TextRun { @@ -175,36 +178,38 @@ pub const RunIterator = struct { break :emoji 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.hasGrapheme()) { - 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; + if (self.break_config.cursor) { + // 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.hasGrapheme()) { + 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; + // 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. } - - // After: after the cursor. We don't need to do anything - // special, we just let the run complete. } } diff --git a/src/font/shaper/web_canvas.zig b/src/font/shaper/web_canvas.zig index f38ab885a..95e220b84 100644 --- a/src/font/shaper/web_canvas.zig +++ b/src/font/shaper/web_canvas.zig @@ -4,6 +4,7 @@ const Allocator = std.mem.Allocator; const ziglyph = @import("ziglyph"); const font = @import("../main.zig"); const terminal = @import("../../terminal/main.zig"); +const config = @import("../../config.zig"); const log = std.log.scoped(.font_shaper); @@ -65,6 +66,7 @@ pub const Shaper = struct { row: terminal.Screen.Row, selection: ?terminal.Selection, cursor_x: ?usize, + break_config: config.FontShapingBreak, ) font.shape.RunIterator { return .{ .hooks = .{ .shaper = self }, @@ -72,6 +74,7 @@ pub const Shaper = struct { .row = row, .selection = selection, .cursor_x = cursor_x, + .break_config = break_config, }; }