From 0d8bc2b4330e04932a938f69e881be9158151d25 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 | 11 ++ src/font/shaper/coretext.zig | 219 +++++++++++++++++++++++++++------ src/font/shaper/harfbuzz.zig | 214 ++++++++++++++++++++++++++------ src/font/shaper/noop.zig | 3 + src/font/shaper/run.zig | 63 +++++----- src/font/shaper/web_canvas.zig | 3 + src/renderer/Metal.zig | 3 + src/renderer/OpenGL.zig | 3 + 9 files changed, 411 insertions(+), 109 deletions(-) diff --git a/src/config.zig b/src/config.zig index 75dbaae02b..640cf1d052 100644 --- a/src/config.zig +++ b/src/config.zig @@ -18,6 +18,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 8396561692..2c45ea5364 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -257,6 +257,12 @@ pub const renamed = std.StaticStringMap([]const u8).initComptime(&.{ /// This is currently only supported on macOS. @"font-thicken-strength": u8 = 255, +/// Where to break font shaping runs. +/// +/// Currently only one option, "cursor", which is enabled by default and breaks +/// text runs under the cursor. +@"font-shaping-break": FontShapingBreak = .{}, + /// What color space to use when performing alpha blending. /// /// This affects how text looks for different background/foreground color pairs. @@ -5588,6 +5594,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 e084a68c95..49ccbbe227 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| { @@ -1036,6 +1053,7 @@ test "shape emoji width long" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1072,6 +1090,7 @@ test "shape variation selector VS15" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1107,6 +1126,7 @@ test "shape variation selector VS16" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1139,6 +1159,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| { @@ -1177,6 +1198,7 @@ test "shape Chinese characters" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1217,6 +1239,7 @@ test "shape box glyphs" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1257,6 +1280,7 @@ test "shape selection boundary" { false, ), null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1280,6 +1304,7 @@ test "shape selection boundary" { false, ), null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1303,6 +1328,7 @@ test "shape selection boundary" { false, ), null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1326,6 +1352,7 @@ test "shape selection boundary" { false, ), null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1349,6 +1376,7 @@ test "shape selection boundary" { false, ), null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1381,6 +1409,7 @@ test "shape cursor boundary" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1390,26 +1419,142 @@ 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 + { + // 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); + } + } + { + // 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); + } + } +} + +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; @@ -1418,17 +1563,18 @@ test "shape cursor boundary" { &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, - 1, + null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; _ = try shaper.shape(run); } - try testing.expectEqual(@as(usize, 3), count); + try testing.expectEqual(@as(usize, 1), count); } - // Cursor at last col is two runs + // Cursor on emoji does not split it { // Get our run iterator var shaper = &testdata.shaper; @@ -1437,30 +1583,16 @@ test "shape cursor boundary" { &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, - 9, + 0, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; _ = try shaper.shape(run); } - try testing.expectEqual(@as(usize, 2), count); + try testing.expectEqual(@as(usize, 1), count); } -} - -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; @@ -1469,7 +1601,8 @@ test "shape cursor boundary and colored emoji" { &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, - null, + 0, + .{ .cursor = false }, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1478,8 +1611,6 @@ test "shape cursor boundary and colored emoji" { } try testing.expectEqual(@as(usize, 1), count); } - - // Cursor on emoji does not split it { // Get our run iterator var shaper = &testdata.shaper; @@ -1488,7 +1619,8 @@ test "shape cursor boundary and colored emoji" { &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, - 0, + 1, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1506,6 +1638,7 @@ test "shape cursor boundary and colored emoji" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, 1, + .{ .cursor = false }, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1536,6 +1669,7 @@ test "shape cell attribute change" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1560,6 +1694,7 @@ test "shape cell attribute change" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1585,6 +1720,7 @@ test "shape cell attribute change" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1610,6 +1746,7 @@ test "shape cell attribute change" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1634,6 +1771,7 @@ test "shape cell attribute change" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1674,6 +1812,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 97292b9b0d..f379eb6d11 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| { @@ -561,6 +574,7 @@ test "shape emoji width long" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -599,6 +613,7 @@ test "shape variation selector VS15" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -636,6 +651,7 @@ test "shape variation selector VS16" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -670,6 +686,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| { @@ -708,6 +725,7 @@ test "shape Chinese characters" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -748,6 +766,7 @@ test "shape box glyphs" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -789,6 +808,7 @@ test "shape selection boundary" { false, ), null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -812,6 +832,7 @@ test "shape selection boundary" { false, ), null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -835,6 +856,7 @@ test "shape selection boundary" { false, ), null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -858,6 +880,7 @@ test "shape selection boundary" { false, ), null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -881,6 +904,7 @@ test "shape selection boundary" { false, ), null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -913,6 +937,7 @@ test "shape cursor boundary" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -922,26 +947,142 @@ 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 + { + // 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); + } + } + { + // 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); + } + } +} + +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; @@ -950,17 +1091,18 @@ test "shape cursor boundary" { &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, - 1, + null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; _ = try shaper.shape(run); } - try testing.expectEqual(@as(usize, 3), count); + try testing.expectEqual(@as(usize, 1), count); } - // Cursor at last col is two runs + // Cursor on emoji does not split it { // Get our run iterator var shaper = &testdata.shaper; @@ -969,30 +1111,16 @@ test "shape cursor boundary" { &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, - 9, + 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); + try testing.expectEqual(@as(usize, 1), count); } -} - -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; @@ -1001,7 +1129,8 @@ test "shape cursor boundary and colored emoji" { &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, - null, + 0, + .{ .cursor = false }, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1010,8 +1139,6 @@ test "shape cursor boundary and colored emoji" { } try testing.expectEqual(@as(usize, 1), count); } - - // Cursor on emoji does not split it { // Get our run iterator var shaper = &testdata.shaper; @@ -1020,7 +1147,8 @@ test "shape cursor boundary and colored emoji" { &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, - 0, + 1, + .{ .cursor = true }, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1038,6 +1166,7 @@ test "shape cursor boundary and colored emoji" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, 1, + .{ .cursor = false }, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1068,6 +1197,7 @@ test "shape cell attribute change" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1092,6 +1222,7 @@ test "shape cell attribute change" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1117,6 +1248,7 @@ test "shape cell attribute change" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1142,6 +1274,7 @@ test "shape cell attribute change" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1166,6 +1299,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 f8988f4ee9..1041954e6f 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 22d19979eb..69a36b88fc 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; - - // 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; + 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; + } + + // 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 f38ab885ab..95e220b843 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, }; } diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 52a5437c66..26584b2790 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -374,6 +374,7 @@ pub const DerivedConfig = struct { font_thicken_strength: u8, font_features: std.ArrayListUnmanaged([:0]const u8), font_styles: font.CodepointResolver.StyleStatus, + font_shaping_break: configpkg.FontShapingBreak, cursor_color: ?terminal.color.RGB, cursor_invert: bool, cursor_opacity: f64, @@ -427,6 +428,7 @@ pub const DerivedConfig = struct { .font_thicken_strength = config.@"font-thicken-strength", .font_features = font_features.list, .font_styles = font_styles, + .font_shaping_break = config.@"font-shaping-break", .cursor_color = if (!cursor_invert and config.@"cursor-color" != null) config.@"cursor-color".?.toTerminalRGB() @@ -2493,6 +2495,7 @@ fn rebuildCells( row, row_selection, if (shape_cursor) screen.cursor.x else null, + self.config.font_shaping_break, ); var shaper_run: ?font.shape.TextRun = try run_iter.next(self.alloc); var shaper_cells: ?[]const font.shape.Cell = null; diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 3e674c7155..4160ed8bc4 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -275,6 +275,7 @@ pub const DerivedConfig = struct { font_thicken_strength: u8, font_features: std.ArrayListUnmanaged([:0]const u8), font_styles: font.CodepointResolver.StyleStatus, + font_shaping_break: configpkg.FontShapingBreak, cursor_color: ?terminal.color.RGB, cursor_invert: bool, cursor_text: ?terminal.color.RGB, @@ -325,6 +326,7 @@ pub const DerivedConfig = struct { .font_thicken_strength = config.@"font-thicken-strength", .font_features = font_features.list, .font_styles = font_styles, + .font_shaping_break = config.@"font-shaping-break", .cursor_color = if (!cursor_invert and config.@"cursor-color" != null) config.@"cursor-color".?.toTerminalRGB() @@ -1349,6 +1351,7 @@ pub fn rebuildCells( row, row_selection, if (shape_cursor) screen.cursor.x else null, + self.config.font_shaping_break, ); var shaper_run: ?font.shape.TextRun = try run_iter.next(self.alloc); var shaper_cells: ?[]const font.shape.Cell = null;