diff --git a/src/font/shape.zig b/src/font/shape.zig index dd0f3dcc5..e3634d68c 100644 --- a/src/font/shape.zig +++ b/src/font/shape.zig @@ -1,3 +1,4 @@ +const std = @import("std"); const builtin = @import("builtin"); const options = @import("main.zig").options; const run = @import("shaper/run.zig"); @@ -72,6 +73,12 @@ pub const RunOptions = struct { /// cached values may be updated during shaping. grid: *SharedGrid, + /// The cells for the row to shape. + cells: std.MultiArrayList(terminal.RenderState.Cell).Slice = .empty, + + /// The x boundaries of the selection in this row. + selection2: ?[2]u16 = null, + /// The terminal screen to shape. screen: *const terminal.Screen, diff --git a/src/font/shaper/coretext.zig b/src/font/shaper/coretext.zig index 953956eb9..41fa88758 100644 --- a/src/font/shaper/coretext.zig +++ b/src/font/shaper/coretext.zig @@ -625,17 +625,27 @@ test "run iterator" { defer testdata.deinit(); { - // Make a screen with some data - var screen = try terminal.Screen.init(alloc, .{ .cols = 5, .rows = 3, .max_scrollback = 0 }); - defer screen.deinit(); - try screen.testWriteString("ABCD"); + var t: terminal.Terminal = try .init(alloc, .{ + .cols = 5, + .rows = 3, + }); + defer t.deinit(alloc); + + var s = t.vtStream(); + defer s.deinit(); + try s.nextSlice("ABCD"); + + var state: terminal.RenderState = .empty; + defer state.deinit(alloc); + try state.update(alloc, &t); // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator(.{ .grid = testdata.grid, - .screen = &screen, - .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .cells = state.row_data.get(0).cells.slice(), + .screen = undefined, + .row = undefined, }); var count: usize = 0; while (try it.next(alloc)) |_| count += 1; @@ -644,15 +654,23 @@ test "run iterator" { // Spaces should be part of a run { - var screen = try terminal.Screen.init(alloc, .{ .cols = 10, .rows = 3, .max_scrollback = 0 }); - defer screen.deinit(); - try screen.testWriteString("ABCD EFG"); + var t = try terminal.Terminal.init(alloc, .{ .cols = 10, .rows = 3 }); + defer t.deinit(alloc); + + var s = t.vtStream(); + defer s.deinit(); + try s.nextSlice("ABCD EFG"); + + var state: terminal.RenderState = .empty; + defer state.deinit(alloc); + try state.update(alloc, &t); var shaper = &testdata.shaper; var it = shaper.runIterator(.{ .grid = testdata.grid, - .screen = &screen, - .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .cells = state.row_data.get(0).cells.slice(), + .screen = undefined, + .row = undefined, }); var count: usize = 0; while (try it.next(alloc)) |_| count += 1; @@ -661,16 +679,24 @@ test "run iterator" { { // Make a screen with some data - var screen = try terminal.Screen.init(alloc, .{ .cols = 5, .rows = 3, .max_scrollback = 0 }); - defer screen.deinit(); - try screen.testWriteString("A😃D"); + var t = try terminal.Terminal.init(alloc, .{ .cols = 5, .rows = 3 }); + defer t.deinit(alloc); + + var s = t.vtStream(); + defer s.deinit(); + try s.nextSlice("A😃D"); + + var state: terminal.RenderState = .empty; + defer state.deinit(alloc); + try state.update(alloc, &t); // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator(.{ .grid = testdata.grid, - .screen = &screen, - .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .cells = state.row_data.get(0).cells.slice(), + .screen = undefined, + .row = undefined, }); var count: usize = 0; while (try it.next(alloc)) |_| count += 1; @@ -680,16 +706,24 @@ test "run iterator" { // Bad ligatures for (&[_][]const u8{ "fl", "fi", "st" }) |bad| { // Make a screen with some data - var screen = try terminal.Screen.init(alloc, .{ .cols = 5, .rows = 3, .max_scrollback = 0 }); - defer screen.deinit(); - try screen.testWriteString(bad); + var t = try terminal.Terminal.init(alloc, .{ .cols = 5, .rows = 3 }); + defer t.deinit(alloc); + + var s = t.vtStream(); + defer s.deinit(); + try s.nextSlice(bad); + + var state: terminal.RenderState = .empty; + defer state.deinit(alloc); + try state.update(alloc, &t); // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator(.{ .grid = testdata.grid, - .screen = &screen, - .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .cells = state.row_data.get(0).cells.slice(), + .screen = undefined, + .row = undefined, }); var count: usize = 0; while (try it.next(alloc)) |_| count += 1; @@ -706,14 +740,18 @@ test "run iterator: empty cells with background set" { { // Make a screen with some data - var screen = try terminal.Screen.init(alloc, .{ .cols = 5, .rows = 3, .max_scrollback = 0 }); - defer screen.deinit(); - try screen.setAttribute(.{ .direct_color_bg = .{ .r = 0xFF, .g = 0, .b = 0 } }); - try screen.testWriteString("A"); + var t = try terminal.Terminal.init(alloc, .{ .cols = 5, .rows = 3 }); + defer t.deinit(alloc); + + var s = t.vtStream(); + defer s.deinit(); + // Set red background + try s.nextSlice("\x1b[48;2;255;0;0m"); + try s.nextSlice("A"); // Get our first row { - const list_cell = screen.pages.getCell(.{ .active = .{ .x = 1 } }).?; + const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = 1 } }).?; const cell = list_cell.cell; cell.* = .{ .content_tag = .bg_color_rgb, @@ -721,7 +759,7 @@ test "run iterator: empty cells with background set" { }; } { - const list_cell = screen.pages.getCell(.{ .active = .{ .x = 2 } }).?; + const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = 2 } }).?; const cell = list_cell.cell; cell.* = .{ .content_tag = .bg_color_rgb, @@ -729,12 +767,17 @@ test "run iterator: empty cells with background set" { }; } + var state: terminal.RenderState = .empty; + defer state.deinit(alloc); + try state.update(alloc, &t); + // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator(.{ .grid = testdata.grid, - .screen = &screen, - .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .cells = state.row_data.get(0).cells.slice(), + .screen = undefined, + .row = undefined, }); { const run = (try it.next(alloc)).?; @@ -759,16 +802,24 @@ test "shape" { buf_idx += try std.unicode.utf8Encode(0x1F3FD, buf[buf_idx..]); // Medium skin tone // Make a screen with some data - var screen = try terminal.Screen.init(alloc, .{ .cols = 10, .rows = 3, .max_scrollback = 0 }); - defer screen.deinit(); - try screen.testWriteString(buf[0..buf_idx]); + var t = try terminal.Terminal.init(alloc, .{ .cols = 10, .rows = 3 }); + defer t.deinit(alloc); + + var s = t.vtStream(); + defer s.deinit(); + try s.nextSlice(buf[0..buf_idx]); + + var state: terminal.RenderState = .empty; + defer state.deinit(alloc); + try state.update(alloc, &t); // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator(.{ .grid = testdata.grid, - .screen = &screen, - .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .cells = state.row_data.get(0).cells.slice(), + .screen = undefined, + .row = undefined, }); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -792,16 +843,24 @@ test "shape nerd fonts" { buf_idx += try std.unicode.utf8Encode(' ', buf[buf_idx..]); // space // Make a screen with some data - var screen = try terminal.Screen.init(alloc, .{ .cols = 10, .rows = 3, .max_scrollback = 0 }); - defer screen.deinit(); - try screen.testWriteString(buf[0..buf_idx]); + var t = try terminal.Terminal.init(alloc, .{ .cols = 10, .rows = 3 }); + defer t.deinit(alloc); + + var s = t.vtStream(); + defer s.deinit(); + try s.nextSlice(buf[0..buf_idx]); + + var state: terminal.RenderState = .empty; + defer state.deinit(alloc); + try state.update(alloc, &t); // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator(.{ .grid = testdata.grid, - .screen = &screen, - .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .cells = state.row_data.get(0).cells.slice(), + .screen = undefined, + .row = undefined, }); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -819,15 +878,23 @@ test "shape inconsolata ligs" { defer testdata.deinit(); { - var screen = try terminal.Screen.init(alloc, .{ .cols = 5, .rows = 3, .max_scrollback = 0 }); - defer screen.deinit(); - try screen.testWriteString(">="); + var t = try terminal.Terminal.init(alloc, .{ .cols = 5, .rows = 3 }); + defer t.deinit(alloc); + + var s = t.vtStream(); + defer s.deinit(); + try s.nextSlice(">="); + + var state: terminal.RenderState = .empty; + defer state.deinit(alloc); + try state.update(alloc, &t); var shaper = &testdata.shaper; var it = shaper.runIterator(.{ .grid = testdata.grid, - .screen = &screen, - .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .cells = state.row_data.get(0).cells.slice(), + .screen = undefined, + .row = undefined, }); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -842,15 +909,23 @@ test "shape inconsolata ligs" { } { - var screen = try terminal.Screen.init(alloc, .{ .cols = 5, .rows = 3, .max_scrollback = 0 }); - defer screen.deinit(); - try screen.testWriteString("==="); + var t = try terminal.Terminal.init(alloc, .{ .cols = 5, .rows = 3 }); + defer t.deinit(alloc); + + var s = t.vtStream(); + defer s.deinit(); + try s.nextSlice("==="); + + var state: terminal.RenderState = .empty; + defer state.deinit(alloc); + try state.update(alloc, &t); var shaper = &testdata.shaper; var it = shaper.runIterator(.{ .grid = testdata.grid, - .screen = &screen, - .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .cells = state.row_data.get(0).cells.slice(), + .screen = undefined, + .row = undefined, }); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -873,15 +948,23 @@ test "shape monaspace ligs" { defer testdata.deinit(); { - var screen = try terminal.Screen.init(alloc, .{ .cols = 5, .rows = 3, .max_scrollback = 0 }); - defer screen.deinit(); - try screen.testWriteString("==="); + var t = try terminal.Terminal.init(alloc, .{ .cols = 5, .rows = 3 }); + defer t.deinit(alloc); + + var s = t.vtStream(); + defer s.deinit(); + try s.nextSlice("==="); + + var state: terminal.RenderState = .empty; + defer state.deinit(alloc); + try state.update(alloc, &t); var shaper = &testdata.shaper; var it = shaper.runIterator(.{ .grid = testdata.grid, - .screen = &screen, - .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .cells = state.row_data.get(0).cells.slice(), + .screen = undefined, + .row = undefined, }); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -905,15 +988,23 @@ test "shape left-replaced lig in last run" { defer testdata.deinit(); { - var screen = try terminal.Screen.init(alloc, .{ .cols = 5, .rows = 3, .max_scrollback = 0 }); - defer screen.deinit(); - try screen.testWriteString("!=="); + var t = try terminal.Terminal.init(alloc, .{ .cols = 5, .rows = 3 }); + defer t.deinit(alloc); + + var s = t.vtStream(); + defer s.deinit(); + try s.nextSlice("!=="); + + var state: terminal.RenderState = .empty; + defer state.deinit(alloc); + try state.update(alloc, &t); var shaper = &testdata.shaper; var it = shaper.runIterator(.{ .grid = testdata.grid, - .screen = &screen, - .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .cells = state.row_data.get(0).cells.slice(), + .screen = undefined, + .row = undefined, }); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -937,15 +1028,23 @@ test "shape left-replaced lig in early run" { defer testdata.deinit(); { - var screen = try terminal.Screen.init(alloc, .{ .cols = 5, .rows = 3, .max_scrollback = 0 }); - defer screen.deinit(); - try screen.testWriteString("!==X"); + var t = try terminal.Terminal.init(alloc, .{ .cols = 5, .rows = 3 }); + defer t.deinit(alloc); + + var s = t.vtStream(); + defer s.deinit(); + try s.nextSlice("!==X"); + + var state: terminal.RenderState = .empty; + defer state.deinit(alloc); + try state.update(alloc, &t); var shaper = &testdata.shaper; var it = shaper.runIterator(.{ .grid = testdata.grid, - .screen = &screen, - .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .cells = state.row_data.get(0).cells.slice(), + .screen = undefined, + .row = undefined, }); const run = (try it.next(alloc)).?; @@ -966,15 +1065,23 @@ test "shape U+3C9 with JB Mono" { defer testdata.deinit(); { - var screen = try terminal.Screen.init(alloc, .{ .cols = 10, .rows = 3, .max_scrollback = 0 }); - defer screen.deinit(); - try screen.testWriteString("\u{03C9} foo"); + var t = try terminal.Terminal.init(alloc, .{ .cols = 10, .rows = 3 }); + defer t.deinit(alloc); + + var s = t.vtStream(); + defer s.deinit(); + try s.nextSlice("\u{03C9} foo"); + + var state: terminal.RenderState = .empty; + defer state.deinit(alloc); + try state.update(alloc, &t); var shaper = &testdata.shaper; var it = shaper.runIterator(.{ .grid = testdata.grid, - .screen = &screen, - .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .cells = state.row_data.get(0).cells.slice(), + .screen = undefined, + .row = undefined, }); var run_count: usize = 0; @@ -997,15 +1104,23 @@ test "shape emoji width" { defer testdata.deinit(); { - var screen = try terminal.Screen.init(alloc, .{ .cols = 5, .rows = 3, .max_scrollback = 0 }); - defer screen.deinit(); - try screen.testWriteString("👍"); + var t = try terminal.Terminal.init(alloc, .{ .cols = 5, .rows = 3 }); + defer t.deinit(alloc); + + var s = t.vtStream(); + defer s.deinit(); + try s.nextSlice("👍"); + + var state: terminal.RenderState = .empty; + defer state.deinit(alloc); + try state.update(alloc, &t); var shaper = &testdata.shaper; var it = shaper.runIterator(.{ .grid = testdata.grid, - .screen = &screen, - .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .cells = state.row_data.get(0).cells.slice(), + .screen = undefined, + .row = undefined, }); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1026,10 +1141,10 @@ test "shape emoji width long" { defer testdata.deinit(); // Make a screen and add a long emoji sequence to it. - var screen = try terminal.Screen.init(alloc, .{ .cols = 30, .rows = 3, .max_scrollback = 0 }); - defer screen.deinit(); + var t = try terminal.Terminal.init(alloc, .{ .cols = 30, .rows = 3 }); + defer t.deinit(alloc); - var page = screen.pages.pages.first.?.data; + var page = t.screens.active.pages.pages.first.?.data; var row = page.getRow(1); const cell = &row.cells.ptr(page.memory)[0]; cell.* = .{ @@ -1048,12 +1163,17 @@ test "shape emoji width long" { graphemes[0..], ); + var state: terminal.RenderState = .empty; + defer state.deinit(alloc); + try state.update(alloc, &t); + // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator(.{ .grid = testdata.grid, - .screen = &screen, - .row = screen.pages.pin(.{ .screen = .{ .y = 1 } }).?, + .cells = state.row_data.get(1).cells.slice(), + .screen = undefined, + .row = undefined, }); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1078,16 +1198,24 @@ test "shape variation selector VS15" { buf_idx += try std.unicode.utf8Encode(0xFE0E, buf[buf_idx..]); // ZWJ to force text // Make a screen with some data - var screen = try terminal.Screen.init(alloc, .{ .cols = 10, .rows = 3, .max_scrollback = 0 }); - defer screen.deinit(); - try screen.testWriteString(buf[0..buf_idx]); + var t = try terminal.Terminal.init(alloc, .{ .cols = 10, .rows = 3 }); + defer t.deinit(alloc); + + var s = t.vtStream(); + defer s.deinit(); + try s.nextSlice(buf[0..buf_idx]); + + var state: terminal.RenderState = .empty; + defer state.deinit(alloc); + try state.update(alloc, &t); // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator(.{ .grid = testdata.grid, - .screen = &screen, - .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .cells = state.row_data.get(0).cells.slice(), + .screen = undefined, + .row = undefined, }); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1111,16 +1239,24 @@ test "shape variation selector VS16" { buf_idx += try std.unicode.utf8Encode(0xFE0F, buf[buf_idx..]); // ZWJ to force color // Make a screen with some data - var screen = try terminal.Screen.init(alloc, .{ .cols = 10, .rows = 3, .max_scrollback = 0 }); - defer screen.deinit(); - try screen.testWriteString(buf[0..buf_idx]); + var t = try terminal.Terminal.init(alloc, .{ .cols = 10, .rows = 3 }); + defer t.deinit(alloc); + + var s = t.vtStream(); + defer s.deinit(); + try s.nextSlice(buf[0..buf_idx]); + + var state: terminal.RenderState = .empty; + defer state.deinit(alloc); + try state.update(alloc, &t); // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator(.{ .grid = testdata.grid, - .screen = &screen, - .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .cells = state.row_data.get(0).cells.slice(), + .screen = undefined, + .row = undefined, }); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1139,18 +1275,26 @@ test "shape with empty cells in between" { defer testdata.deinit(); // Make a screen with some data - var screen = try terminal.Screen.init(alloc, .{ .cols = 30, .rows = 3, .max_scrollback = 0 }); - defer screen.deinit(); - try screen.testWriteString("A"); - screen.cursorRight(5); - try screen.testWriteString("B"); + var t = try terminal.Terminal.init(alloc, .{ .cols = 30, .rows = 3 }); + defer t.deinit(alloc); + + var s = t.vtStream(); + defer s.deinit(); + try s.nextSlice("A"); + try s.nextSlice("\x1b[5C"); // 5 spaces forward + try s.nextSlice("B"); + + var state: terminal.RenderState = .empty; + defer state.deinit(alloc); + try state.update(alloc, &t); // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator(.{ .grid = testdata.grid, - .screen = &screen, - .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .cells = state.row_data.get(0).cells.slice(), + .screen = undefined, + .row = undefined, }); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1177,16 +1321,24 @@ test "shape Chinese characters" { buf_idx += try std.unicode.utf8Encode('a', buf[buf_idx..]); // Make a screen with some data - var screen = try terminal.Screen.init(alloc, .{ .cols = 30, .rows = 3, .max_scrollback = 0 }); - defer screen.deinit(); - try screen.testWriteString(buf[0..buf_idx]); + var t = try terminal.Terminal.init(alloc, .{ .cols = 30, .rows = 3 }); + defer t.deinit(alloc); + + var s = t.vtStream(); + defer s.deinit(); + try s.nextSlice(buf[0..buf_idx]); + + var state: terminal.RenderState = .empty; + defer state.deinit(alloc); + try state.update(alloc, &t); // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator(.{ .grid = testdata.grid, - .screen = &screen, - .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .cells = state.row_data.get(0).cells.slice(), + .screen = undefined, + .row = undefined, }); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1218,16 +1370,24 @@ test "shape Devanagari string" { defer testdata.deinit(); // Make a screen with some data - var screen = try terminal.Screen.init(alloc, .{ .cols = 30, .rows = 3, .max_scrollback = 0 }); - defer screen.deinit(); - try screen.testWriteString("अपार्टमेंट"); + var t = try terminal.Terminal.init(alloc, .{ .cols = 30, .rows = 3 }); + defer t.deinit(alloc); + + var s = t.vtStream(); + defer s.deinit(); + try s.nextSlice("अपार्टमेंट"); + + var state: terminal.RenderState = .empty; + defer state.deinit(alloc); + try state.update(alloc, &t); // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator(.{ .grid = testdata.grid, - .screen = &screen, - .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .cells = state.row_data.get(0).cells.slice(), + .screen = undefined, + .row = undefined, }); const run = try it.next(alloc); @@ -1260,16 +1420,24 @@ test "shape box glyphs" { buf_idx += try std.unicode.utf8Encode(0x2501, buf[buf_idx..]); // // Make a screen with some data - var screen = try terminal.Screen.init(alloc, .{ .cols = 10, .rows = 3, .max_scrollback = 0 }); - defer screen.deinit(); - try screen.testWriteString(buf[0..buf_idx]); + var t = try terminal.Terminal.init(alloc, .{ .cols = 10, .rows = 3 }); + defer t.deinit(alloc); + + var s = t.vtStream(); + defer s.deinit(); + try s.nextSlice(buf[0..buf_idx]); + + var state: terminal.RenderState = .empty; + defer state.deinit(alloc); + try state.update(alloc, &t); // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator(.{ .grid = testdata.grid, - .screen = &screen, - .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .cells = state.row_data.get(0).cells.slice(), + .screen = undefined, + .row = undefined, }); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1292,9 +1460,16 @@ test "shape selection boundary" { defer testdata.deinit(); // Make a screen with some data - var screen = try terminal.Screen.init(alloc, .{ .cols = 10, .rows = 3, .max_scrollback = 0 }); - defer screen.deinit(); - try screen.testWriteString("a1b2c3d4e5"); + var t = try terminal.Terminal.init(alloc, .{ .cols = 10, .rows = 3 }); + defer t.deinit(alloc); + + var s = t.vtStream(); + defer s.deinit(); + try s.nextSlice("a1b2c3d4e5"); + + var state: terminal.RenderState = .empty; + defer state.deinit(alloc); + try state.update(alloc, &t); // Full line selection { @@ -1302,13 +1477,10 @@ test "shape selection boundary" { var shaper = &testdata.shaper; var it = shaper.runIterator(.{ .grid = testdata.grid, - .screen = &screen, - .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - .selection = terminal.Selection.init( - screen.pages.pin(.{ .active = .{ .x = 0, .y = 0 } }).?, - screen.pages.pin(.{ .active = .{ .x = screen.pages.cols - 1, .y = 0 } }).?, - false, - ), + .cells = state.row_data.get(0).cells.slice(), + .screen = undefined, + .row = undefined, + .selection2 = .{ 0, @intCast(t.cols - 1) }, }); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1324,13 +1496,10 @@ test "shape selection boundary" { var shaper = &testdata.shaper; var it = shaper.runIterator(.{ .grid = testdata.grid, - .screen = &screen, - .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - .selection = terminal.Selection.init( - screen.pages.pin(.{ .active = .{ .x = 2, .y = 0 } }).?, - screen.pages.pin(.{ .active = .{ .x = screen.pages.cols - 1, .y = 0 } }).?, - false, - ), + .cells = state.row_data.get(0).cells.slice(), + .screen = undefined, + .row = undefined, + .selection2 = .{ 2, @intCast(t.cols - 1) }, }); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1346,13 +1515,10 @@ test "shape selection boundary" { var shaper = &testdata.shaper; var it = shaper.runIterator(.{ .grid = testdata.grid, - .screen = &screen, - .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - .selection = terminal.Selection.init( - screen.pages.pin(.{ .active = .{ .x = 0, .y = 0 } }).?, - screen.pages.pin(.{ .active = .{ .x = 3, .y = 0 } }).?, - false, - ), + .cells = state.row_data.get(0).cells.slice(), + .screen = undefined, + .row = undefined, + .selection2 = .{ 0, 3 }, }); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1368,13 +1534,10 @@ test "shape selection boundary" { var shaper = &testdata.shaper; var it = shaper.runIterator(.{ .grid = testdata.grid, - .screen = &screen, - .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - .selection = terminal.Selection.init( - screen.pages.pin(.{ .active = .{ .x = 1, .y = 0 } }).?, - screen.pages.pin(.{ .active = .{ .x = 3, .y = 0 } }).?, - false, - ), + .cells = state.row_data.get(0).cells.slice(), + .screen = undefined, + .row = undefined, + .selection2 = .{ 1, 3 }, }); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1390,13 +1553,10 @@ test "shape selection boundary" { var shaper = &testdata.shaper; var it = shaper.runIterator(.{ .grid = testdata.grid, - .screen = &screen, - .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - .selection = terminal.Selection.init( - screen.pages.pin(.{ .active = .{ .x = 1, .y = 0 } }).?, - screen.pages.pin(.{ .active = .{ .x = 1, .y = 0 } }).?, - false, - ), + .cells = state.row_data.get(0).cells.slice(), + .screen = undefined, + .row = undefined, + .selection2 = .{ 1, 1 }, }); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1415,9 +1575,16 @@ test "shape cursor boundary" { defer testdata.deinit(); // Make a screen with some data - var screen = try terminal.Screen.init(alloc, .{ .cols = 10, .rows = 3, .max_scrollback = 0 }); - defer screen.deinit(); - try screen.testWriteString("a1b2c3d4e5"); + var t = try terminal.Terminal.init(alloc, .{ .cols = 10, .rows = 3 }); + defer t.deinit(alloc); + + var s = t.vtStream(); + defer s.deinit(); + try s.nextSlice("a1b2c3d4e5"); + + var state: terminal.RenderState = .empty; + defer state.deinit(alloc); + try state.update(alloc, &t); // No cursor is full line { @@ -1425,8 +1592,9 @@ test "shape cursor boundary" { var shaper = &testdata.shaper; var it = shaper.runIterator(.{ .grid = testdata.grid, - .screen = &screen, - .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .cells = state.row_data.get(0).cells.slice(), + .screen = undefined, + .row = undefined, }); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1443,8 +1611,9 @@ test "shape cursor boundary" { var shaper = &testdata.shaper; var it = shaper.runIterator(.{ .grid = testdata.grid, - .screen = &screen, - .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .cells = state.row_data.get(0).cells.slice(), + .screen = undefined, + .row = undefined, .cursor_x = 0, }); var count: usize = 0; @@ -1460,8 +1629,9 @@ test "shape cursor boundary" { var shaper = &testdata.shaper; var it = shaper.runIterator(.{ .grid = testdata.grid, - .screen = &screen, - .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .cells = state.row_data.get(0).cells.slice(), + .screen = undefined, + .row = undefined, }); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1479,8 +1649,9 @@ test "shape cursor boundary" { var shaper = &testdata.shaper; var it = shaper.runIterator(.{ .grid = testdata.grid, - .screen = &screen, - .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .cells = state.row_data.get(0).cells.slice(), + .screen = undefined, + .row = undefined, .cursor_x = 1, }); var count: usize = 0; @@ -1496,8 +1667,9 @@ test "shape cursor boundary" { var shaper = &testdata.shaper; var it = shaper.runIterator(.{ .grid = testdata.grid, - .screen = &screen, - .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .cells = state.row_data.get(0).cells.slice(), + .screen = undefined, + .row = undefined, }); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1514,8 +1686,9 @@ test "shape cursor boundary" { var shaper = &testdata.shaper; var it = shaper.runIterator(.{ .grid = testdata.grid, - .screen = &screen, - .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .cells = state.row_data.get(0).cells.slice(), + .screen = undefined, + .row = undefined, .cursor_x = 9, }); var count: usize = 0; @@ -1531,8 +1704,9 @@ test "shape cursor boundary" { var shaper = &testdata.shaper; var it = shaper.runIterator(.{ .grid = testdata.grid, - .screen = &screen, - .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .cells = state.row_data.get(0).cells.slice(), + .screen = undefined, + .row = undefined, }); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1552,9 +1726,16 @@ test "shape cursor boundary and colored emoji" { defer testdata.deinit(); // Make a screen with some data - var screen = try terminal.Screen.init(alloc, .{ .cols = 3, .rows = 10, .max_scrollback = 0 }); - defer screen.deinit(); - try screen.testWriteString("👍🏼"); + var t = try terminal.Terminal.init(alloc, .{ .cols = 3, .rows = 10 }); + defer t.deinit(alloc); + + var s = t.vtStream(); + defer s.deinit(); + try s.nextSlice("👍🏼"); + + var state: terminal.RenderState = .empty; + defer state.deinit(alloc); + try state.update(alloc, &t); // No cursor is full line { @@ -1562,8 +1743,9 @@ test "shape cursor boundary and colored emoji" { var shaper = &testdata.shaper; var it = shaper.runIterator(.{ .grid = testdata.grid, - .screen = &screen, - .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .cells = state.row_data.get(0).cells.slice(), + .screen = undefined, + .row = undefined, }); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1579,8 +1761,9 @@ test "shape cursor boundary and colored emoji" { var shaper = &testdata.shaper; var it = shaper.runIterator(.{ .grid = testdata.grid, - .screen = &screen, - .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .cells = state.row_data.get(0).cells.slice(), + .screen = undefined, + .row = undefined, .cursor_x = 0, }); var count: usize = 0; @@ -1595,8 +1778,9 @@ test "shape cursor boundary and colored emoji" { var shaper = &testdata.shaper; var it = shaper.runIterator(.{ .grid = testdata.grid, - .screen = &screen, - .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .cells = state.row_data.get(0).cells.slice(), + .screen = undefined, + .row = undefined, }); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1610,8 +1794,9 @@ test "shape cursor boundary and colored emoji" { var shaper = &testdata.shaper; var it = shaper.runIterator(.{ .grid = testdata.grid, - .screen = &screen, - .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .cells = state.row_data.get(0).cells.slice(), + .screen = undefined, + .row = undefined, .cursor_x = 1, }); var count: usize = 0; @@ -1626,8 +1811,9 @@ test "shape cursor boundary and colored emoji" { var shaper = &testdata.shaper; var it = shaper.runIterator(.{ .grid = testdata.grid, - .screen = &screen, - .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .cells = state.row_data.get(0).cells.slice(), + .screen = undefined, + .row = undefined, }); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1647,15 +1833,23 @@ test "shape cell attribute change" { // Plain >= should shape into 1 run { - var screen = try terminal.Screen.init(alloc, .{ .cols = 10, .rows = 3, .max_scrollback = 0 }); - defer screen.deinit(); - try screen.testWriteString(">="); + var t = try terminal.Terminal.init(alloc, .{ .cols = 10, .rows = 3 }); + defer t.deinit(alloc); + + var s = t.vtStream(); + defer s.deinit(); + try s.nextSlice(">="); + + var state: terminal.RenderState = .empty; + defer state.deinit(alloc); + try state.update(alloc, &t); var shaper = &testdata.shaper; var it = shaper.runIterator(.{ .grid = testdata.grid, - .screen = &screen, - .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .cells = state.row_data.get(0).cells.slice(), + .screen = undefined, + .row = undefined, }); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1667,17 +1861,25 @@ test "shape cell attribute change" { // Bold vs regular should split { - var screen = try terminal.Screen.init(alloc, .{ .cols = 3, .rows = 10, .max_scrollback = 0 }); - defer screen.deinit(); - try screen.testWriteString(">"); - try screen.setAttribute(.{ .bold = {} }); - try screen.testWriteString("="); + var t = try terminal.Terminal.init(alloc, .{ .cols = 3, .rows = 10 }); + defer t.deinit(alloc); + + var s = t.vtStream(); + defer s.deinit(); + try s.nextSlice(">"); + try s.nextSlice("\x1b[1m"); // Bold + try s.nextSlice("="); + + var state: terminal.RenderState = .empty; + defer state.deinit(alloc); + try state.update(alloc, &t); var shaper = &testdata.shaper; var it = shaper.runIterator(.{ .grid = testdata.grid, - .screen = &screen, - .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .cells = state.row_data.get(0).cells.slice(), + .screen = undefined, + .row = undefined, }); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1689,18 +1891,28 @@ test "shape cell attribute change" { // Changing fg color should split { - var screen = try terminal.Screen.init(alloc, .{ .cols = 3, .rows = 10, .max_scrollback = 0 }); - defer screen.deinit(); - try screen.setAttribute(.{ .direct_color_fg = .{ .r = 1, .g = 2, .b = 3 } }); - try screen.testWriteString(">"); - try screen.setAttribute(.{ .direct_color_fg = .{ .r = 3, .g = 2, .b = 1 } }); - try screen.testWriteString("="); + var t = try terminal.Terminal.init(alloc, .{ .cols = 3, .rows = 10 }); + defer t.deinit(alloc); + + var s = t.vtStream(); + defer s.deinit(); + // RGB 1, 2, 3 + try s.nextSlice("\x1b[38;2;1;2;3m"); + try s.nextSlice(">"); + // RGB 3, 2, 1 + try s.nextSlice("\x1b[38;2;3;2;1m"); + try s.nextSlice("="); + + var state: terminal.RenderState = .empty; + defer state.deinit(alloc); + try state.update(alloc, &t); var shaper = &testdata.shaper; var it = shaper.runIterator(.{ .grid = testdata.grid, - .screen = &screen, - .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .cells = state.row_data.get(0).cells.slice(), + .screen = undefined, + .row = undefined, }); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1712,18 +1924,28 @@ test "shape cell attribute change" { // Changing bg color should NOT split { - var screen = try terminal.Screen.init(alloc, .{ .cols = 3, .rows = 10, .max_scrollback = 0 }); - defer screen.deinit(); - try screen.setAttribute(.{ .direct_color_bg = .{ .r = 1, .g = 2, .b = 3 } }); - try screen.testWriteString(">"); - try screen.setAttribute(.{ .direct_color_bg = .{ .r = 3, .g = 2, .b = 1 } }); - try screen.testWriteString("="); + var t = try terminal.Terminal.init(alloc, .{ .cols = 3, .rows = 10 }); + defer t.deinit(alloc); + + var s = t.vtStream(); + defer s.deinit(); + // RGB 1, 2, 3 bg + try s.nextSlice("\x1b[48;2;1;2;3m"); + try s.nextSlice(">"); + // RGB 3, 2, 1 bg + try s.nextSlice("\x1b[48;2;3;2;1m"); + try s.nextSlice("="); + + var state: terminal.RenderState = .empty; + defer state.deinit(alloc); + try state.update(alloc, &t); var shaper = &testdata.shaper; var it = shaper.runIterator(.{ .grid = testdata.grid, - .screen = &screen, - .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .cells = state.row_data.get(0).cells.slice(), + .screen = undefined, + .row = undefined, }); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1735,17 +1957,26 @@ test "shape cell attribute change" { // Same bg color should not split { - var screen = try terminal.Screen.init(alloc, .{ .cols = 3, .rows = 10, .max_scrollback = 0 }); - defer screen.deinit(); - try screen.setAttribute(.{ .direct_color_bg = .{ .r = 1, .g = 2, .b = 3 } }); - try screen.testWriteString(">"); - try screen.testWriteString("="); + var t = try terminal.Terminal.init(alloc, .{ .cols = 3, .rows = 10 }); + defer t.deinit(alloc); + + var s = t.vtStream(); + defer s.deinit(); + // RGB 1, 2, 3 bg + try s.nextSlice("\x1b[48;2;1;2;3m"); + try s.nextSlice(">"); + try s.nextSlice("="); + + var state: terminal.RenderState = .empty; + defer state.deinit(alloc); + try state.update(alloc, &t); var shaper = &testdata.shaper; var it = shaper.runIterator(.{ .grid = testdata.grid, - .screen = &screen, - .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .cells = state.row_data.get(0).cells.slice(), + .screen = undefined, + .row = undefined, }); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1773,17 +2004,24 @@ test "shape high plane sprite font codepoint" { var testdata = try testShaper(alloc); defer testdata.deinit(); - var screen = try terminal.Screen.init(alloc, .{ .cols = 10, .rows = 3, .max_scrollback = 0 }); - defer screen.deinit(); + var t = try terminal.Terminal.init(alloc, .{ .cols = 10, .rows = 3 }); + defer t.deinit(alloc); + var s = t.vtStream(); + defer s.deinit(); // U+1FB70: Vertical One Eighth Block-2 - try screen.testWriteString("\u{1FB70}"); + try s.nextSlice("\u{1FB70}"); + + var state: terminal.RenderState = .empty; + defer state.deinit(alloc); + try state.update(alloc, &t); var shaper = &testdata.shaper; var it = shaper.runIterator(.{ .grid = testdata.grid, - .screen = &screen, - .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .cells = state.row_data.get(0).cells.slice(), + .screen = undefined, + .row = undefined, }); // We should get one run const run = (try it.next(alloc)).?; diff --git a/src/font/shaper/run.zig b/src/font/shaper/run.zig index 79e4bfc18..a0080d1fc 100644 --- a/src/font/shaper/run.zig +++ b/src/font/shaper/run.zig @@ -45,6 +45,8 @@ pub const RunIterator = struct { i: usize = 0, pub fn next(self: *RunIterator, alloc: Allocator) !?TextRun { + if (self.opts.cells.len > 0) return try self.next2(alloc); + const cells = self.opts.row.cells(.all); // Trim the right side of a row that might be empty @@ -309,6 +311,265 @@ pub const RunIterator = struct { }; } + pub fn next2(self: *RunIterator, alloc: Allocator) !?TextRun { + const slice = &self.opts.cells; + const cells: []const terminal.page.Cell = slice.items(.raw); + const graphemes: []const []const u21 = slice.items(.grapheme); + const styles: []const terminal.Style = slice.items(.style); + + // Trim the right side of a row that might be empty + const max: usize = max: { + for (0..cells.len) |i| { + const rev_i = cells.len - i - 1; + if (!cells[rev_i].isEmpty()) break :max rev_i + 1; + } + + break :max 0; + }; + + // Invisible cells don't have any glyphs rendered, + // so we explicitly skip them in the shaping process. + while (self.i < max and + (cells[self.i].hasStyling() and + styles[self.i].flags.invisible)) self.i += 1; + + // We're over at the max + if (self.i >= max) return null; + + // Track the font for our current run + var current_font: font.Collection.Index = .{}; + + // Allow the hook to prepare + try self.hooks.prepare(); + + // Initialize our hash for this run. + var hasher = Hasher.init(0); + + // Let's get our style that we'll expect for the run. + const style: terminal.Style = if (cells[self.i].hasStyling()) styles[self.i] else .{}; + + // Go through cell by cell and accumulate while we build our run. + var j: usize = self.i; + while (j < max) : (j += 1) { + // Use relative cluster positions (offset from run start) to make + // the shaping cache position-independent. This ensures that runs + // with identical content but different starting positions in the + // row produce the same hash, enabling cache reuse. + const cluster = j - self.i; + const cell: *const terminal.page.Cell = &cells[j]; + + // If we have a selection and we're at a boundary point, then + // we break the run here. + if (self.opts.selection2) |bounds| { + if (j > self.i) { + if (bounds[0] > 0 and j == bounds[0]) break; + if (bounds[1] > 0 and j == bounds[1] + 1) break; + } + } + + // If we're a spacer, then we ignore it + switch (cell.wide) { + .narrow, .wide => {}, + .spacer_head, .spacer_tail => continue, + } + + // If our cell attributes are changing, then we split the run. + // This prevents a single glyph for ">=" to be rendered with + // one color when the two components have different styling. + if (j > self.i) style: { + const prev_cell = cells[j - 1]; + + // If the prev cell and this cell are both plain + // codepoints then we check if they are commonly "bad" + // ligatures and spit the run if they are. + if (prev_cell.content_tag == .codepoint and + cell.content_tag == .codepoint) + { + const prev_cp = prev_cell.codepoint(); + switch (prev_cp) { + // fl, fi + 'f' => { + const cp = cell.codepoint(); + if (cp == 'l' or cp == 'i') break; + }, + + // st + 's' => { + const cp = cell.codepoint(); + if (cp == 't') break; + }, + + else => {}, + } + } + + // If the style is exactly the change then fast path out. + if (prev_cell.style_id == cell.style_id) break :style; + + // The style is different. We allow differing background + // styles but any other change results in a new run. + const c1 = comparableStyle(style); + const c2 = comparableStyle(if (cell.hasStyling()) styles[j] else .{}); + if (!c1.eql(c2)) break; + } + + // Text runs break when font styles change so we need to get + // the proper style. + const font_style: font.Style = style: { + if (style.flags.bold) { + if (style.flags.italic) break :style .bold_italic; + break :style .bold; + } + + if (style.flags.italic) break :style .italic; + break :style .regular; + }; + + // Determine the presentation format for this glyph. + const presentation: ?font.Presentation = if (cell.hasGrapheme()) p: { + // We only check the FIRST codepoint because I believe the + // presentation format must be directly adjacent to the codepoint. + const cps = graphemes[j]; + assert(cps.len > 0); + if (cps[0] == 0xFE0E) break :p .text; + if (cps[0] == 0xFE0F) break :p .emoji; + break :p null; + } else emoji: { + // If we're not a grapheme, our individual char could be + // an emoji so we want to check if we expect emoji presentation. + // The font grid indexForCodepoint we use below will do this + // automatically. + 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.opts.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. + } + } + + // We need to find a font that supports this character. If + // there are additional zero-width codepoints (to form a single + // grapheme, i.e. combining characters), we need to find a font + // that supports all of them. + const font_info: struct { + idx: font.Collection.Index, + fallback: ?u32 = null, + } = font_info: { + // If we find a font that supports this entire grapheme + // then we use that. + if (try self.indexForCell2( + alloc, + cell, + graphemes[j], + font_style, + presentation, + )) |idx| break :font_info .{ .idx = idx }; + + // Otherwise we need a fallback character. Prefer the + // official replacement character. + if (try self.opts.grid.getIndex( + alloc, + 0xFFFD, // replacement char + font_style, + presentation, + )) |idx| break :font_info .{ .idx = idx, .fallback = 0xFFFD }; + + // Fallback to space + if (try self.opts.grid.getIndex( + alloc, + ' ', + font_style, + presentation, + )) |idx| break :font_info .{ .idx = idx, .fallback = ' ' }; + + // We can't render at all. This is a bug, we should always + // have a font that can render a space. + unreachable; + }; + + //log.warn("char={x} info={}", .{ cell.char, font_info }); + if (j == self.i) current_font = font_info.idx; + + // If our fonts are not equal, then we're done with our run. + if (font_info.idx != current_font) break; + + // If we're a fallback character, add that and continue; we + // don't want to add the entire grapheme. + if (font_info.fallback) |cp| { + try self.addCodepoint(&hasher, cp, @intCast(cluster)); + continue; + } + + // If we're a Kitty unicode placeholder then we add a blank. + if (cell.codepoint() == terminal.kitty.graphics.unicode.placeholder) { + try self.addCodepoint(&hasher, ' ', @intCast(cluster)); + continue; + } + + // Add all the codepoints for our grapheme + try self.addCodepoint( + &hasher, + if (cell.codepoint() == 0) ' ' else cell.codepoint(), + @intCast(cluster), + ); + if (cell.hasGrapheme()) { + for (graphemes[j]) |cp| { + // Do not send presentation modifiers + if (cp == 0xFE0E or cp == 0xFE0F) continue; + try self.addCodepoint(&hasher, cp, @intCast(cluster)); + } + } + } + + // Finalize our buffer + try self.hooks.finalize(); + + // Add our length to the hash as an additional mechanism to avoid collisions + autoHash(&hasher, j - self.i); + + // Add our font index + autoHash(&hasher, current_font); + + // Move our cursor. Must defer since we use self.i below. + defer self.i = j; + + return .{ + .hash = hasher.final(), + .offset = @intCast(self.i), + .cells = @intCast(j - self.i), + .grid = self.opts.grid, + .font_index = current_font, + }; + } + fn addCodepoint(self: *RunIterator, hasher: anytype, cp: u32, cluster: u32) !void { autoHash(hasher, cp); autoHash(hasher, cluster); @@ -324,7 +585,7 @@ pub const RunIterator = struct { fn indexForCell( self: *RunIterator, alloc: Allocator, - cell: *terminal.Cell, + cell: *const terminal.Cell, style: font.Style, presentation: ?font.Presentation, ) !?font.Collection.Index { @@ -396,6 +657,85 @@ pub const RunIterator = struct { return null; } + + fn indexForCell2( + self: *RunIterator, + alloc: Allocator, + cell: *const terminal.Cell, + graphemes: []const u21, + style: font.Style, + presentation: ?font.Presentation, + ) !?font.Collection.Index { + if (cell.isEmpty() or + cell.codepoint() == 0 or + cell.codepoint() == terminal.kitty.graphics.unicode.placeholder) + { + return try self.opts.grid.getIndex( + alloc, + ' ', + style, + presentation, + ); + } + + // Get the font index for the primary codepoint. + const primary_cp: u32 = cell.codepoint(); + const primary = try self.opts.grid.getIndex( + alloc, + primary_cp, + style, + presentation, + ) orelse return null; + + // Easy, and common: we aren't a multi-codepoint grapheme, so + // we just return whatever index for the cell codepoint. + if (!cell.hasGrapheme()) return primary; + + // If this is a grapheme, we need to find a font that supports + // all of the codepoints in the grapheme. + var candidates: std.ArrayList(font.Collection.Index) = try .initCapacity( + alloc, + graphemes.len + 1, + ); + defer candidates.deinit(alloc); + candidates.appendAssumeCapacity(primary); + + for (graphemes) |cp| { + // Ignore Emoji ZWJs + if (cp == 0xFE0E or cp == 0xFE0F or cp == 0x200D) continue; + + // Find a font that supports this codepoint. If none support this + // then the whole grapheme can't be rendered so we return null. + // + // We explicitly do not require the additional grapheme components + // to support the base presentation, since it is common for emoji + // fonts to support the base emoji with emoji presentation but not + // certain ZWJ-combined characters like the male and female signs. + const idx = try self.opts.grid.getIndex( + alloc, + cp, + style, + null, + ) orelse return null; + candidates.appendAssumeCapacity(idx); + } + + // We need to find a candidate that has ALL of our codepoints + for (candidates.items) |idx| { + if (!self.opts.grid.hasCodepoint(idx, primary_cp, presentation)) continue; + for (graphemes) |cp| { + // Ignore Emoji ZWJs + if (cp == 0xFE0E or cp == 0xFE0F or cp == 0x200D) continue; + if (!self.opts.grid.hasCodepoint(idx, cp, null)) break; + } else { + // If the while completed, then we have a candidate that + // supports all of our codepoints. + return idx; + } + } + + return null; + } }; /// Returns a style that when compared must be identical for a run to