From d29073d999e88f8a35cd3c368f97f6a3bfa8a0fc Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 24 Jul 2024 11:27:02 -0700 Subject: [PATCH 01/36] terminal/kitty: add graphics diacritics file --- src/terminal/kitty/graphics.zig | 5 + src/terminal/kitty/graphics_diacritics.zig | 329 +++++++++++++++++++++ 2 files changed, 334 insertions(+) create mode 100644 src/terminal/kitty/graphics_diacritics.zig diff --git a/src/terminal/kitty/graphics.zig b/src/terminal/kitty/graphics.zig index cfc45adbc..0fa52fab0 100644 --- a/src/terminal/kitty/graphics.zig +++ b/src/terminal/kitty/graphics.zig @@ -20,3 +20,8 @@ pub usingnamespace @import("graphics_command.zig"); pub usingnamespace @import("graphics_exec.zig"); pub usingnamespace @import("graphics_image.zig"); pub usingnamespace @import("graphics_storage.zig"); +pub const diacritics = @import("graphics_diacritics.zig"); + +test { + @import("std").testing.refAllDecls(@This()); +} diff --git a/src/terminal/kitty/graphics_diacritics.zig b/src/terminal/kitty/graphics_diacritics.zig new file mode 100644 index 000000000..db587f407 --- /dev/null +++ b/src/terminal/kitty/graphics_diacritics.zig @@ -0,0 +1,329 @@ +const std = @import("std"); +const assert = std.debug.assert; +const testing = std.testing; + +/// Get the row/col index for a diacritic codepoint. +pub fn get(cp: u21) ?usize { + return std.sort.binarySearch(u21, cp, diacritics, {}, (struct { + fn order(context: void, lhs: u21, rhs: u21) std.math.Order { + _ = context; + return std.math.order(lhs, rhs); + } + }).order); +} + +/// These are the diacritics used with the Kitty graphics protocol +/// Unicode placement feature to specify the row/column for placement. +/// The index into the array determines the value. +/// +/// This is derived from: +/// https://sw.kovidgoyal.net/kitty/_downloads/f0a0de9ec8d9ff4456206db8e0814937/rowcolumn-diacritics.txt +const diacritics: []const u21 = &.{ + 0x0305, + 0x030D, + 0x030E, + 0x0310, + 0x0312, + 0x033D, + 0x033E, + 0x033F, + 0x0346, + 0x034A, + 0x034B, + 0x034C, + 0x0350, + 0x0351, + 0x0352, + 0x0357, + 0x035B, + 0x0363, + 0x0364, + 0x0365, + 0x0366, + 0x0367, + 0x0368, + 0x0369, + 0x036A, + 0x036B, + 0x036C, + 0x036D, + 0x036E, + 0x036F, + 0x0483, + 0x0484, + 0x0485, + 0x0486, + 0x0487, + 0x0592, + 0x0593, + 0x0594, + 0x0595, + 0x0597, + 0x0598, + 0x0599, + 0x059C, + 0x059D, + 0x059E, + 0x059F, + 0x05A0, + 0x05A1, + 0x05A8, + 0x05A9, + 0x05AB, + 0x05AC, + 0x05AF, + 0x05C4, + 0x0610, + 0x0611, + 0x0612, + 0x0613, + 0x0614, + 0x0615, + 0x0616, + 0x0617, + 0x0657, + 0x0658, + 0x0659, + 0x065A, + 0x065B, + 0x065D, + 0x065E, + 0x06D6, + 0x06D7, + 0x06D8, + 0x06D9, + 0x06DA, + 0x06DB, + 0x06DC, + 0x06DF, + 0x06E0, + 0x06E1, + 0x06E2, + 0x06E4, + 0x06E7, + 0x06E8, + 0x06EB, + 0x06EC, + 0x0730, + 0x0732, + 0x0733, + 0x0735, + 0x0736, + 0x073A, + 0x073D, + 0x073F, + 0x0740, + 0x0741, + 0x0743, + 0x0745, + 0x0747, + 0x0749, + 0x074A, + 0x07EB, + 0x07EC, + 0x07ED, + 0x07EE, + 0x07EF, + 0x07F0, + 0x07F1, + 0x07F3, + 0x0816, + 0x0817, + 0x0818, + 0x0819, + 0x081B, + 0x081C, + 0x081D, + 0x081E, + 0x081F, + 0x0820, + 0x0821, + 0x0822, + 0x0823, + 0x0825, + 0x0826, + 0x0827, + 0x0829, + 0x082A, + 0x082B, + 0x082C, + 0x082D, + 0x0951, + 0x0953, + 0x0954, + 0x0F82, + 0x0F83, + 0x0F86, + 0x0F87, + 0x135D, + 0x135E, + 0x135F, + 0x17DD, + 0x193A, + 0x1A17, + 0x1A75, + 0x1A76, + 0x1A77, + 0x1A78, + 0x1A79, + 0x1A7A, + 0x1A7B, + 0x1A7C, + 0x1B6B, + 0x1B6D, + 0x1B6E, + 0x1B6F, + 0x1B70, + 0x1B71, + 0x1B72, + 0x1B73, + 0x1CD0, + 0x1CD1, + 0x1CD2, + 0x1CDA, + 0x1CDB, + 0x1CE0, + 0x1DC0, + 0x1DC1, + 0x1DC3, + 0x1DC4, + 0x1DC5, + 0x1DC6, + 0x1DC7, + 0x1DC8, + 0x1DC9, + 0x1DCB, + 0x1DCC, + 0x1DD1, + 0x1DD2, + 0x1DD3, + 0x1DD4, + 0x1DD5, + 0x1DD6, + 0x1DD7, + 0x1DD8, + 0x1DD9, + 0x1DDA, + 0x1DDB, + 0x1DDC, + 0x1DDD, + 0x1DDE, + 0x1DDF, + 0x1DE0, + 0x1DE1, + 0x1DE2, + 0x1DE3, + 0x1DE4, + 0x1DE5, + 0x1DE6, + 0x1DFE, + 0x20D0, + 0x20D1, + 0x20D4, + 0x20D5, + 0x20D6, + 0x20D7, + 0x20DB, + 0x20DC, + 0x20E1, + 0x20E7, + 0x20E9, + 0x20F0, + 0x2CEF, + 0x2CF0, + 0x2CF1, + 0x2DE0, + 0x2DE1, + 0x2DE2, + 0x2DE3, + 0x2DE4, + 0x2DE5, + 0x2DE6, + 0x2DE7, + 0x2DE8, + 0x2DE9, + 0x2DEA, + 0x2DEB, + 0x2DEC, + 0x2DED, + 0x2DEE, + 0x2DEF, + 0x2DF0, + 0x2DF1, + 0x2DF2, + 0x2DF3, + 0x2DF4, + 0x2DF5, + 0x2DF6, + 0x2DF7, + 0x2DF8, + 0x2DF9, + 0x2DFA, + 0x2DFB, + 0x2DFC, + 0x2DFD, + 0x2DFE, + 0x2DFF, + 0xA66F, + 0xA67C, + 0xA67D, + 0xA6F0, + 0xA6F1, + 0xA8E0, + 0xA8E1, + 0xA8E2, + 0xA8E3, + 0xA8E4, + 0xA8E5, + 0xA8E6, + 0xA8E7, + 0xA8E8, + 0xA8E9, + 0xA8EA, + 0xA8EB, + 0xA8EC, + 0xA8ED, + 0xA8EE, + 0xA8EF, + 0xA8F0, + 0xA8F1, + 0xAAB0, + 0xAAB2, + 0xAAB3, + 0xAAB7, + 0xAAB8, + 0xAABE, + 0xAABF, + 0xAAC1, + 0xFE20, + 0xFE21, + 0xFE22, + 0xFE23, + 0xFE24, + 0xFE25, + 0xFE26, + 0x10A0F, + 0x10A38, + 0x1D185, + 0x1D186, + 0x1D187, + 0x1D188, + 0x1D189, + 0x1D1AA, + 0x1D1AB, + 0x1D1AC, + 0x1D1AD, + 0x1D242, + 0x1D243, + 0x1D244, +}; + +test { + // diacritics must be sorted since we use a binary search. + try testing.expect(std.sort.isSorted(u21, diacritics, {}, (struct { + fn lessThan(context: void, lhs: u21, rhs: u21) bool { + _ = context; + return lhs < rhs; + } + }).lessThan)); +} From a5c382633fe2d84c416996c52bc78e00401ff94c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 24 Jul 2024 11:38:56 -0700 Subject: [PATCH 02/36] terminal/kitty: placements support location enum (only pin for now) --- src/terminal/kitty/graphics_exec.zig | 34 +++++----- src/terminal/kitty/graphics_storage.zig | 84 ++++++++++++++----------- 2 files changed, 65 insertions(+), 53 deletions(-) diff --git a/src/terminal/kitty/graphics_exec.zig b/src/terminal/kitty/graphics_exec.zig index 0ea084795..1d873c0c2 100644 --- a/src/terminal/kitty/graphics_exec.zig +++ b/src/terminal/kitty/graphics_exec.zig @@ -196,7 +196,9 @@ fn display( // Add the placement const p: ImageStorage.Placement = .{ - .pin = placement_pin, + .location = .{ + .pin = placement_pin, + }, .x_offset = d.x_offset, .y_offset = d.y_offset, .source_x = d.x, @@ -218,21 +220,23 @@ fn display( return result; }; - // Cursor needs to move after placement - switch (d.cursor_movement) { - .none => {}, - .after => { - // We use terminal.index to properly handle scroll regions. - const size = p.gridSize(img, terminal); - for (0..size.rows) |_| terminal.index() catch |err| { - log.warn("failed to move cursor: {}", .{err}); - break; - }; + // Apply cursor movement setting. This only applies to pin placements. + switch (p.location) { + .pin => |pin| switch (d.cursor_movement) { + .none => {}, + .after => { + // We use terminal.index to properly handle scroll regions. + const size = p.gridSize(img, terminal); + for (0..size.rows) |_| terminal.index() catch |err| { + log.warn("failed to move cursor: {}", .{err}); + break; + }; - terminal.setCursorPos( - terminal.screen.cursor.y, - p.pin.x + size.cols + 1, - ); + terminal.setCursorPos( + terminal.screen.cursor.y, + pin.x + size.cols + 1, + ); + }, }, } diff --git a/src/terminal/kitty/graphics_storage.zig b/src/terminal/kitty/graphics_storage.zig index cf02ee73e..27be82b7c 100644 --- a/src/terminal/kitty/graphics_storage.zig +++ b/src/terminal/kitty/graphics_storage.zig @@ -576,8 +576,11 @@ pub const ImageStorage = struct { }; pub const Placement = struct { - /// The tracked pin for this placement. - pin: *PageList.Pin, + /// The location where this placement should be drawn. + location: union(enum) { + /// Exactly placed on a screen pin. + pin: *PageList.Pin, + }, /// Offset of the x/y from the top-left of the cell. x_offset: u32 = 0, @@ -600,7 +603,9 @@ pub const ImageStorage = struct { self: *const Placement, s: *terminal.Screen, ) void { - s.pages.untrackPin(self.pin); + switch (self.location) { + .pin => |p| s.pages.untrackPin(p), + } } /// Returns the size in grid cells that this placement takes up. @@ -648,9 +653,12 @@ pub const ImageStorage = struct { image: Image, t: *const terminal.Terminal, ) Rect { - const grid_size = self.gridSize(image, t); + assert(self.location == .pin); - var br = switch (self.pin.downOverflow(grid_size.rows - 1)) { + const grid_size = self.gridSize(image, t); + const pin = self.location.pin; + + var br = switch (pin.downOverflow(grid_size.rows - 1)) { .offset => |v| v, .overflow => |v| v.end, }; @@ -658,12 +666,12 @@ pub const ImageStorage = struct { // We need to sub one here because the x value is // one width already. So if the image is width "1" // then we add zero to X because X itelf is width 1. - self.pin.x + (grid_size.cols - 1), + pin.x + (grid_size.cols - 1), t.cols - 1, ); return .{ - .top_left = self.pin.*, + .top_left = pin.*, .bottom_right = br, }; } @@ -692,8 +700,8 @@ test "storage: add placement with zero placement id" { defer s.deinit(alloc, &t.screen); try s.addImage(alloc, .{ .id = 1, .width = 50, .height = 50 }); try s.addImage(alloc, .{ .id = 2, .width = 25, .height = 25 }); - try s.addPlacement(alloc, 1, 0, .{ .pin = try trackPin(&t, .{ .x = 25, .y = 25 }) }); - try s.addPlacement(alloc, 1, 0, .{ .pin = try trackPin(&t, .{ .x = 25, .y = 25 }) }); + try s.addPlacement(alloc, 1, 0, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 25, .y = 25 }) } }); + try s.addPlacement(alloc, 1, 0, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 25, .y = 25 }) } }); try testing.expectEqual(@as(usize, 2), s.placements.count()); try testing.expectEqual(@as(usize, 2), s.images.count()); @@ -721,8 +729,8 @@ test "storage: delete all placements and images" { try s.addImage(alloc, .{ .id = 1 }); try s.addImage(alloc, .{ .id = 2 }); try s.addImage(alloc, .{ .id = 3 }); - try s.addPlacement(alloc, 1, 1, .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) }); - try s.addPlacement(alloc, 2, 1, .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) }); + try s.addPlacement(alloc, 1, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } }); + try s.addPlacement(alloc, 2, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } }); s.dirty = false; s.delete(alloc, &t, .{ .all = true }); @@ -745,8 +753,8 @@ test "storage: delete all placements and images preserves limit" { try s.addImage(alloc, .{ .id = 1 }); try s.addImage(alloc, .{ .id = 2 }); try s.addImage(alloc, .{ .id = 3 }); - try s.addPlacement(alloc, 1, 1, .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) }); - try s.addPlacement(alloc, 2, 1, .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) }); + try s.addPlacement(alloc, 1, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } }); + try s.addPlacement(alloc, 2, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } }); s.dirty = false; s.delete(alloc, &t, .{ .all = true }); @@ -769,8 +777,8 @@ test "storage: delete all placements" { try s.addImage(alloc, .{ .id = 1 }); try s.addImage(alloc, .{ .id = 2 }); try s.addImage(alloc, .{ .id = 3 }); - try s.addPlacement(alloc, 1, 1, .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) }); - try s.addPlacement(alloc, 2, 1, .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) }); + try s.addPlacement(alloc, 1, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } }); + try s.addPlacement(alloc, 2, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } }); s.dirty = false; s.delete(alloc, &t, .{ .all = false }); @@ -792,8 +800,8 @@ test "storage: delete all placements by image id" { try s.addImage(alloc, .{ .id = 1 }); try s.addImage(alloc, .{ .id = 2 }); try s.addImage(alloc, .{ .id = 3 }); - try s.addPlacement(alloc, 1, 1, .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) }); - try s.addPlacement(alloc, 2, 1, .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) }); + try s.addPlacement(alloc, 1, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } }); + try s.addPlacement(alloc, 2, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } }); s.dirty = false; s.delete(alloc, &t, .{ .id = .{ .image_id = 2 } }); @@ -815,8 +823,8 @@ test "storage: delete all placements by image id and unused images" { try s.addImage(alloc, .{ .id = 1 }); try s.addImage(alloc, .{ .id = 2 }); try s.addImage(alloc, .{ .id = 3 }); - try s.addPlacement(alloc, 1, 1, .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) }); - try s.addPlacement(alloc, 2, 1, .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) }); + try s.addPlacement(alloc, 1, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } }); + try s.addPlacement(alloc, 2, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } }); s.dirty = false; s.delete(alloc, &t, .{ .id = .{ .delete = true, .image_id = 2 } }); @@ -838,9 +846,9 @@ test "storage: delete placement by specific id" { try s.addImage(alloc, .{ .id = 1 }); try s.addImage(alloc, .{ .id = 2 }); try s.addImage(alloc, .{ .id = 3 }); - try s.addPlacement(alloc, 1, 1, .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) }); - try s.addPlacement(alloc, 1, 2, .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) }); - try s.addPlacement(alloc, 2, 1, .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) }); + try s.addPlacement(alloc, 1, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } }); + try s.addPlacement(alloc, 1, 2, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } }); + try s.addPlacement(alloc, 2, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } }); s.dirty = false; s.delete(alloc, &t, .{ .id = .{ @@ -867,8 +875,8 @@ test "storage: delete intersecting cursor" { defer s.deinit(alloc, &t.screen); try s.addImage(alloc, .{ .id = 1, .width = 50, .height = 50 }); try s.addImage(alloc, .{ .id = 2, .width = 25, .height = 25 }); - try s.addPlacement(alloc, 1, 1, .{ .pin = try trackPin(&t, .{ .x = 0, .y = 0 }) }); - try s.addPlacement(alloc, 1, 2, .{ .pin = try trackPin(&t, .{ .x = 25, .y = 25 }) }); + try s.addPlacement(alloc, 1, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 0, .y = 0 }) } }); + try s.addPlacement(alloc, 1, 2, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 25, .y = 25 }) } }); t.screen.cursorAbsolute(12, 12); @@ -899,8 +907,8 @@ test "storage: delete intersecting cursor plus unused" { defer s.deinit(alloc, &t.screen); try s.addImage(alloc, .{ .id = 1, .width = 50, .height = 50 }); try s.addImage(alloc, .{ .id = 2, .width = 25, .height = 25 }); - try s.addPlacement(alloc, 1, 1, .{ .pin = try trackPin(&t, .{ .x = 0, .y = 0 }) }); - try s.addPlacement(alloc, 1, 2, .{ .pin = try trackPin(&t, .{ .x = 25, .y = 25 }) }); + try s.addPlacement(alloc, 1, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 0, .y = 0 }) } }); + try s.addPlacement(alloc, 1, 2, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 25, .y = 25 }) } }); t.screen.cursorAbsolute(12, 12); @@ -931,8 +939,8 @@ test "storage: delete intersecting cursor hits multiple" { defer s.deinit(alloc, &t.screen); try s.addImage(alloc, .{ .id = 1, .width = 50, .height = 50 }); try s.addImage(alloc, .{ .id = 2, .width = 25, .height = 25 }); - try s.addPlacement(alloc, 1, 1, .{ .pin = try trackPin(&t, .{ .x = 0, .y = 0 }) }); - try s.addPlacement(alloc, 1, 2, .{ .pin = try trackPin(&t, .{ .x = 25, .y = 25 }) }); + try s.addPlacement(alloc, 1, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 0, .y = 0 }) } }); + try s.addPlacement(alloc, 1, 2, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 25, .y = 25 }) } }); t.screen.cursorAbsolute(26, 26); @@ -957,8 +965,8 @@ test "storage: delete by column" { defer s.deinit(alloc, &t.screen); try s.addImage(alloc, .{ .id = 1, .width = 50, .height = 50 }); try s.addImage(alloc, .{ .id = 2, .width = 25, .height = 25 }); - try s.addPlacement(alloc, 1, 1, .{ .pin = try trackPin(&t, .{ .x = 0, .y = 0 }) }); - try s.addPlacement(alloc, 1, 2, .{ .pin = try trackPin(&t, .{ .x = 25, .y = 25 }) }); + try s.addPlacement(alloc, 1, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 0, .y = 0 }) } }); + try s.addPlacement(alloc, 1, 2, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 25, .y = 25 }) } }); s.dirty = false; s.delete(alloc, &t, .{ .column = .{ @@ -988,9 +996,9 @@ test "storage: delete by column 1x1" { var s: ImageStorage = .{}; defer s.deinit(alloc, &t.screen); try s.addImage(alloc, .{ .id = 1, .width = 1, .height = 1 }); - try s.addPlacement(alloc, 1, 1, .{ .pin = try trackPin(&t, .{ .x = 0, .y = 0 }) }); - try s.addPlacement(alloc, 1, 2, .{ .pin = try trackPin(&t, .{ .x = 1, .y = 0 }) }); - try s.addPlacement(alloc, 1, 3, .{ .pin = try trackPin(&t, .{ .x = 2, .y = 0 }) }); + try s.addPlacement(alloc, 1, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 0, .y = 0 }) } }); + try s.addPlacement(alloc, 1, 2, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 0 }) } }); + try s.addPlacement(alloc, 1, 3, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 2, .y = 0 }) } }); s.delete(alloc, &t, .{ .column = .{ .delete = false, @@ -1023,8 +1031,8 @@ test "storage: delete by row" { defer s.deinit(alloc, &t.screen); try s.addImage(alloc, .{ .id = 1, .width = 50, .height = 50 }); try s.addImage(alloc, .{ .id = 2, .width = 25, .height = 25 }); - try s.addPlacement(alloc, 1, 1, .{ .pin = try trackPin(&t, .{ .x = 0, .y = 0 }) }); - try s.addPlacement(alloc, 1, 2, .{ .pin = try trackPin(&t, .{ .x = 25, .y = 25 }) }); + try s.addPlacement(alloc, 1, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 0, .y = 0 }) } }); + try s.addPlacement(alloc, 1, 2, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 25, .y = 25 }) } }); s.dirty = false; s.delete(alloc, &t, .{ .row = .{ @@ -1054,9 +1062,9 @@ test "storage: delete by row 1x1" { var s: ImageStorage = .{}; defer s.deinit(alloc, &t.screen); try s.addImage(alloc, .{ .id = 1, .width = 1, .height = 1 }); - try s.addPlacement(alloc, 1, 1, .{ .pin = try trackPin(&t, .{ .y = 0 }) }); - try s.addPlacement(alloc, 1, 2, .{ .pin = try trackPin(&t, .{ .y = 1 }) }); - try s.addPlacement(alloc, 1, 3, .{ .pin = try trackPin(&t, .{ .y = 2 }) }); + try s.addPlacement(alloc, 1, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .y = 0 }) } }); + try s.addPlacement(alloc, 1, 2, .{ .location = .{ .pin = try trackPin(&t, .{ .y = 1 }) } }); + try s.addPlacement(alloc, 1, 3, .{ .location = .{ .pin = try trackPin(&t, .{ .y = 2 }) } }); s.delete(alloc, &t, .{ .row = .{ .delete = false, From 7d9e50353b6894b961b99282acef62f575720882 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 24 Jul 2024 11:49:42 -0700 Subject: [PATCH 03/36] terminal/kitty: add virtual placeholders placements --- src/terminal/kitty/graphics_exec.zig | 28 ++++++++++------- src/terminal/kitty/graphics_storage.zig | 41 +++++++++++++++++-------- 2 files changed, 46 insertions(+), 23 deletions(-) diff --git a/src/terminal/kitty/graphics_exec.zig b/src/terminal/kitty/graphics_exec.zig index 1d873c0c2..2859d3f0f 100644 --- a/src/terminal/kitty/graphics_exec.zig +++ b/src/terminal/kitty/graphics_exec.zig @@ -184,21 +184,26 @@ fn display( // Make sure our response has the image id in case we looked up by number result.id = img.id; - // Track a new pin for our cursor. The cursor is always tracked but we - // don't want this one to move with the cursor. - const placement_pin = terminal.screen.pages.trackPin( - terminal.screen.cursor.page_pin.*, - ) catch |err| { - log.warn("failed to create pin for Kitty graphics err={}", .{err}); - result.message = "EINVAL: failed to prepare terminal state"; - return result; + // Location where the placement will go. + const location: ImageStorage.Placement.Location = location: { + // Virtual placements are not tracked + if (d.virtual_placement) break :location .{ .virtual = {} }; + + // Track a new pin for our cursor. The cursor is always tracked but we + // don't want this one to move with the cursor. + const pin = terminal.screen.pages.trackPin( + terminal.screen.cursor.page_pin.*, + ) catch |err| { + log.warn("failed to create pin for Kitty graphics err={}", .{err}); + result.message = "EINVAL: failed to prepare terminal state"; + return result; + }; + break :location .{ .pin = pin }; }; // Add the placement const p: ImageStorage.Placement = .{ - .location = .{ - .pin = placement_pin, - }, + .location = location, .x_offset = d.x_offset, .y_offset = d.y_offset, .source_x = d.x, @@ -222,6 +227,7 @@ fn display( // Apply cursor movement setting. This only applies to pin placements. switch (p.location) { + .virtual => {}, .pin => |pin| switch (d.cursor_movement) { .none => {}, .after => { diff --git a/src/terminal/kitty/graphics_storage.zig b/src/terminal/kitty/graphics_storage.zig index 27be82b7c..82441a54e 100644 --- a/src/terminal/kitty/graphics_storage.zig +++ b/src/terminal/kitty/graphics_storage.zig @@ -218,6 +218,7 @@ pub const ImageStorage = struct { cmd: command.Delete, ) void { switch (cmd) { + // TODO: virtual placeholders must not be deleted according to spec .all => |delete_images| if (delete_images) { // We just reset our entire state. self.deinit(alloc, &t.screen); @@ -318,7 +319,7 @@ pub const ImageStorage = struct { var it = self.placements.iterator(); while (it.next()) |entry| { const img = self.imageById(entry.key_ptr.image_id) orelse continue; - const rect = entry.value_ptr.rect(img, t); + const rect = entry.value_ptr.rect(img, t) orelse continue; if (rect.top_left.x <= x and rect.bottom_right.x >= x) { entry.value_ptr.deinit(&t.screen); self.placements.removeByPtr(entry.key_ptr); @@ -345,7 +346,7 @@ pub const ImageStorage = struct { var it = self.placements.iterator(); while (it.next()) |entry| { const img = self.imageById(entry.key_ptr.image_id) orelse continue; - const rect = entry.value_ptr.rect(img, t); + const rect = entry.value_ptr.rect(img, t) orelse continue; // We need to copy our pin to ensure we are at least at // the top-left x. @@ -365,6 +366,14 @@ pub const ImageStorage = struct { .z => |v| { var it = self.placements.iterator(); while (it.next()) |entry| { + switch (entry.value_ptr.location) { + .pin => {}, + + // Virtual placeholders cannot delete by z according + // to the spec. + .virtual => continue, + } + if (entry.value_ptr.z == v.z) { const image_id = entry.key_ptr.image_id; entry.value_ptr.deinit(&t.screen); @@ -451,7 +460,7 @@ pub const ImageStorage = struct { var it = self.placements.iterator(); while (it.next()) |entry| { const img = self.imageById(entry.key_ptr.image_id) orelse continue; - const rect = entry.value_ptr.rect(img, t); + const rect = entry.value_ptr.rect(img, t) orelse continue; if (target_pin.isBetween(rect.top_left, rect.bottom_right)) { if (filter) |f| if (!f(filter_ctx, entry.value_ptr.*)) continue; entry.value_ptr.deinit(&t.screen); @@ -577,10 +586,7 @@ pub const ImageStorage = struct { pub const Placement = struct { /// The location where this placement should be drawn. - location: union(enum) { - /// Exactly placed on a screen pin. - pin: *PageList.Pin, - }, + location: Location, /// Offset of the x/y from the top-left of the cell. x_offset: u32 = 0, @@ -599,12 +605,21 @@ pub const ImageStorage = struct { /// The z-index for this placement. z: i32 = 0, + pub const Location = union(enum) { + /// Exactly placed on a screen pin. + pin: *PageList.Pin, + + /// Virtual placement (U=1) for unicode placeholders. + virtual: void, + }; + pub fn deinit( self: *const Placement, s: *terminal.Screen, ) void { switch (self.location) { .pin => |p| s.pages.untrackPin(p), + .virtual => {}, } } @@ -647,16 +662,18 @@ pub const ImageStorage = struct { } /// Returns a selection of the entire rectangle this placement - /// occupies within the screen. + /// occupies within the screen. This can return null if the placement + /// doesn't have an associated rect (i.e. a virtual placement). pub fn rect( self: Placement, image: Image, t: *const terminal.Terminal, - ) Rect { - assert(self.location == .pin); - + ) ?Rect { const grid_size = self.gridSize(image, t); - const pin = self.location.pin; + const pin = switch (self.location) { + .pin => |p| p, + .virtual => return null, + }; var br = switch (pin.downOverflow(grid_size.rows - 1)) { .offset => |v| v, From 763e7fab8abf4613c49214ddd993582dde99867f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 24 Jul 2024 11:52:49 -0700 Subject: [PATCH 04/36] renderer: skip virtual placements --- src/renderer/Metal.zig | 9 ++++++--- src/renderer/OpenGL.zig | 9 ++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index d6c2514cc..07c5956ec 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -1598,8 +1598,11 @@ fn prepKittyGraphics( continue; }; + // Get the rect for the placement. If this placement doesn't have + // a rect then its virtual or something so skip it. + const rect = p.rect(image, t) orelse continue; + // If the selection isn't within our viewport then skip it. - const rect = p.rect(image, t); if (bot.before(rect.top_left)) continue; if (rect.bottom_right.before(top)) continue; @@ -1656,7 +1659,7 @@ fn prepKittyGraphics( // Convert our screen point to a viewport point const viewport: terminal.point.Point = t.screen.pages.pointFromPin( .viewport, - p.pin.*, + rect.top_left, ) orelse .{ .viewport = .{} }; // Calculate the source rectangle @@ -1679,7 +1682,7 @@ fn prepKittyGraphics( if (image.width > 0 and image.height > 0) { try self.image_placements.append(self.alloc, .{ .image_id = kv.key_ptr.image_id, - .x = @intCast(p.pin.x), + .x = @intCast(rect.top_left.x), .y = @intCast(viewport.viewport.y), .z = p.z, .width = dest_width, diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 46acff1a9..fd9261874 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -785,8 +785,11 @@ fn prepKittyGraphics( continue; }; + // Get the rect for the placement. If this placement doesn't have + // a rect then its virtual or something so skip it. + const rect = p.rect(image, t) orelse continue; + // If the selection isn't within our viewport then skip it. - const rect = p.rect(image, t); if (bot.before(rect.top_left)) continue; if (rect.bottom_right.before(top)) continue; @@ -843,7 +846,7 @@ fn prepKittyGraphics( // Convert our screen point to a viewport point const viewport: terminal.point.Point = t.screen.pages.pointFromPin( .viewport, - p.pin.*, + rect.top_left, ) orelse .{ .viewport = .{} }; // Calculate the source rectangle @@ -866,7 +869,7 @@ fn prepKittyGraphics( if (image.width > 0 and image.height > 0) { try self.image_placements.append(self.alloc, .{ .image_id = kv.key_ptr.image_id, - .x = @intCast(p.pin.x), + .x = @intCast(rect.top_left.x), .y = @intCast(viewport.viewport.y), .z = p.z, .width = dest_width, From 2c0f9bfc28cf9e0e7e7a8040eae73d759b304cb4 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 24 Jul 2024 18:44:14 -0700 Subject: [PATCH 05/36] terminal: cell returns empty for Kitty placeholder So we don't render the replacement char --- src/terminal/Terminal.zig | 6 +++++- src/terminal/kitty/graphics.zig | 1 + src/terminal/kitty/graphics_diacritics.zig | 3 +++ src/terminal/page.zig | 22 ++++++++++++++++++++-- 4 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index c58377374..16b8a07b9 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -379,7 +379,11 @@ pub fn print(self: *Terminal, c: u21) !void { } } - log.debug("c={x} grapheme attach to left={}", .{ c, prev.left }); + log.debug("c={X} grapheme attach to left={} primary_cp={X}", .{ + c, + prev.left, + prev.cell.codepoint(), + }); self.screen.cursorMarkDirty(); try self.screen.appendGrapheme(prev.cell, c); return; diff --git a/src/terminal/kitty/graphics.zig b/src/terminal/kitty/graphics.zig index 0fa52fab0..14ab6babb 100644 --- a/src/terminal/kitty/graphics.zig +++ b/src/terminal/kitty/graphics.zig @@ -21,6 +21,7 @@ pub usingnamespace @import("graphics_exec.zig"); pub usingnamespace @import("graphics_image.zig"); pub usingnamespace @import("graphics_storage.zig"); pub const diacritics = @import("graphics_diacritics.zig"); +pub const placeholder = diacritics.placeholder; test { @import("std").testing.refAllDecls(@This()); diff --git a/src/terminal/kitty/graphics_diacritics.zig b/src/terminal/kitty/graphics_diacritics.zig index db587f407..92fb816eb 100644 --- a/src/terminal/kitty/graphics_diacritics.zig +++ b/src/terminal/kitty/graphics_diacritics.zig @@ -2,6 +2,9 @@ const std = @import("std"); const assert = std.debug.assert; const testing = std.testing; +/// Codepoint for the unicode placeholder character. +pub const placeholder: u21 = 0x10EEEE; + /// Get the row/col index for a diacritic codepoint. pub fn get(cp: u21) ?usize { return std.sort.binarySearch(u21, cp, diacritics, {}, (struct { diff --git a/src/terminal/page.zig b/src/terminal/page.zig index 677e3fb4a..457207782 100644 --- a/src/terminal/page.zig +++ b/src/terminal/page.zig @@ -8,6 +8,7 @@ const posix = std.posix; const fastmem = @import("../fastmem.zig"); const color = @import("color.zig"); const hyperlink = @import("hyperlink.zig"); +const kitty = @import("kitty.zig"); const sgr = @import("sgr.zig"); const style = @import("style.zig"); const size = @import("size.zig"); @@ -1673,11 +1674,18 @@ pub const Cell = packed struct(u64) { return @as(u64, @bitCast(self)) == 0; } + /// Returns true if this cell represents a cell with text to render. + /// + /// Cases this returns false: + /// - Cell text is blank + /// - Cell is styled but only with a background color and no text + /// - Cell has a unicode placeholder for Kitty graphics protocol pub fn hasText(self: Cell) bool { return switch (self.content_tag) { .codepoint, .codepoint_grapheme, - => self.content.codepoint != 0, + => self.content.codepoint != 0 and + self.content.codepoint != kitty.graphics.placeholder, .bg_color_palette, .bg_color_rgb, @@ -1709,7 +1717,8 @@ pub const Cell = packed struct(u64) { return self.style_id != style.default_id; } - /// Returns true if the cell has no text or styling. + /// Returns true if the cell has no text or styling. This also returns + /// true if the cell represents a Kitty graphics unicode placeholder. pub fn isEmpty(self: Cell) bool { return switch (self.content_tag) { // Textual cells are empty if they have no text and are narrow. @@ -2641,3 +2650,12 @@ test "Page verifyIntegrity zero cols" { page.verifyIntegrity(testing.allocator), ); } + +test "Cell isEmpty for kitty placeholder" { + var c: Cell = .{ + .content_tag = .codepoint_grapheme, + .content = .{ .codepoint = kitty.graphics.placeholder }, + }; + try testing.expectEqual(@as(u21, kitty.graphics.placeholder), c.codepoint()); + try testing.expect(c.isEmpty()); +} From 358b4ca896dd75c76b694c595947ae53988780e6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 24 Jul 2024 18:59:01 -0700 Subject: [PATCH 06/36] terminal/kitty: parse relative placement fields --- src/terminal/kitty/graphics_command.zig | 20 ++++++++++++++++++++ src/terminal/kitty/graphics_exec.zig | 9 ++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/terminal/kitty/graphics_command.zig b/src/terminal/kitty/graphics_command.zig index fe9a4520f..810a8df55 100644 --- a/src/terminal/kitty/graphics_command.zig +++ b/src/terminal/kitty/graphics_command.zig @@ -462,6 +462,10 @@ pub const Display = struct { rows: u32 = 0, // r cursor_movement: CursorMovement = .after, // C virtual_placement: bool = false, // U + parent_id: u32 = 0, // P + parent_placement_id: u32 = 0, // Q + horizontal_offset: u32 = 0, // H + vertical_offset: u32 = 0, // V z: i32 = 0, // z pub const CursorMovement = enum { @@ -537,6 +541,22 @@ pub const Display = struct { result.z = @bitCast(v); } + if (kv.get('P')) |v| { + result.parent_id = v; + } + + if (kv.get('Q')) |v| { + result.parent_placement_id = v; + } + + if (kv.get('H')) |v| { + result.horizontal_offset = v; + } + + if (kv.get('V')) |v| { + result.vertical_offset = v; + } + return result; } }; diff --git a/src/terminal/kitty/graphics_exec.zig b/src/terminal/kitty/graphics_exec.zig index 2859d3f0f..b8924d56f 100644 --- a/src/terminal/kitty/graphics_exec.zig +++ b/src/terminal/kitty/graphics_exec.zig @@ -187,7 +187,14 @@ fn display( // Location where the placement will go. const location: ImageStorage.Placement.Location = location: { // Virtual placements are not tracked - if (d.virtual_placement) break :location .{ .virtual = {} }; + if (d.virtual_placement) { + if (d.parent_id > 0) { + result.message = "EINVAL: virtual placement cannot refer to a parent"; + return result; + } + + break :location .{ .virtual = {} }; + } // Track a new pin for our cursor. The cursor is always tracked but we // don't want this one to move with the cursor. From f71afcab9595de7bc2d6c8011f84c141ac330fa0 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 24 Jul 2024 19:14:17 -0700 Subject: [PATCH 07/36] terminal/kitty: diacritic small tests --- src/terminal/kitty/graphics_diacritics.zig | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/terminal/kitty/graphics_diacritics.zig b/src/terminal/kitty/graphics_diacritics.zig index 92fb816eb..ec3e7d0ca 100644 --- a/src/terminal/kitty/graphics_diacritics.zig +++ b/src/terminal/kitty/graphics_diacritics.zig @@ -5,7 +5,7 @@ const testing = std.testing; /// Codepoint for the unicode placeholder character. pub const placeholder: u21 = 0x10EEEE; -/// Get the row/col index for a diacritic codepoint. +/// Get the row/col index for a diacritic codepoint. These are 0-indexed. pub fn get(cp: u21) ?usize { return std.sort.binarySearch(u21, cp, diacritics, {}, (struct { fn order(context: void, lhs: u21, rhs: u21) std.math.Order { @@ -321,7 +321,7 @@ const diacritics: []const u21 = &.{ 0x1D244, }; -test { +test "sorted" { // diacritics must be sorted since we use a binary search. try testing.expect(std.sort.isSorted(u21, diacritics, {}, (struct { fn lessThan(context: void, lhs: u21, rhs: u21) bool { @@ -330,3 +330,9 @@ test { } }).lessThan)); } + +test "diacritic" { + // Some spot checks based on Kitty behavior + try testing.expectEqual(30, get(0x483).?); + try testing.expectEqual(294, get(0x1d242).?); +} From bb1a9bf532cfec2f632b1b0b78129ee996b1224a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 24 Jul 2024 19:20:45 -0700 Subject: [PATCH 08/36] terminal/kitty: rename diacritics to unicode --- src/terminal/kitty/graphics.zig | 3 +-- .../{graphics_diacritics.zig => graphics_unicode.zig} | 9 ++++++--- src/terminal/page.zig | 6 +++--- 3 files changed, 10 insertions(+), 8 deletions(-) rename src/terminal/kitty/{graphics_diacritics.zig => graphics_unicode.zig} (94%) diff --git a/src/terminal/kitty/graphics.zig b/src/terminal/kitty/graphics.zig index 14ab6babb..22d102f53 100644 --- a/src/terminal/kitty/graphics.zig +++ b/src/terminal/kitty/graphics.zig @@ -20,8 +20,7 @@ pub usingnamespace @import("graphics_command.zig"); pub usingnamespace @import("graphics_exec.zig"); pub usingnamespace @import("graphics_image.zig"); pub usingnamespace @import("graphics_storage.zig"); -pub const diacritics = @import("graphics_diacritics.zig"); -pub const placeholder = diacritics.placeholder; +pub const unicode = @import("graphics_unicode.zig"); test { @import("std").testing.refAllDecls(@This()); diff --git a/src/terminal/kitty/graphics_diacritics.zig b/src/terminal/kitty/graphics_unicode.zig similarity index 94% rename from src/terminal/kitty/graphics_diacritics.zig rename to src/terminal/kitty/graphics_unicode.zig index ec3e7d0ca..d85d07067 100644 --- a/src/terminal/kitty/graphics_diacritics.zig +++ b/src/terminal/kitty/graphics_unicode.zig @@ -1,3 +1,6 @@ +//! This file contains various logic and data for working with the +//! Kitty graphics protocol unicode placeholder, virtual placement feature. + const std = @import("std"); const assert = std.debug.assert; const testing = std.testing; @@ -6,7 +9,7 @@ const testing = std.testing; pub const placeholder: u21 = 0x10EEEE; /// Get the row/col index for a diacritic codepoint. These are 0-indexed. -pub fn get(cp: u21) ?usize { +pub fn getIndex(cp: u21) ?usize { return std.sort.binarySearch(u21, cp, diacritics, {}, (struct { fn order(context: void, lhs: u21, rhs: u21) std.math.Order { _ = context; @@ -333,6 +336,6 @@ test "sorted" { test "diacritic" { // Some spot checks based on Kitty behavior - try testing.expectEqual(30, get(0x483).?); - try testing.expectEqual(294, get(0x1d242).?); + try testing.expectEqual(30, getIndex(0x483).?); + try testing.expectEqual(294, getIndex(0x1d242).?); } diff --git a/src/terminal/page.zig b/src/terminal/page.zig index 457207782..0ad599641 100644 --- a/src/terminal/page.zig +++ b/src/terminal/page.zig @@ -1685,7 +1685,7 @@ pub const Cell = packed struct(u64) { .codepoint, .codepoint_grapheme, => self.content.codepoint != 0 and - self.content.codepoint != kitty.graphics.placeholder, + self.content.codepoint != kitty.graphics.unicode.placeholder, .bg_color_palette, .bg_color_rgb, @@ -2654,8 +2654,8 @@ test "Page verifyIntegrity zero cols" { test "Cell isEmpty for kitty placeholder" { var c: Cell = .{ .content_tag = .codepoint_grapheme, - .content = .{ .codepoint = kitty.graphics.placeholder }, + .content = .{ .codepoint = kitty.graphics.unicode.placeholder }, }; - try testing.expectEqual(@as(u21, kitty.graphics.placeholder), c.codepoint()); + try testing.expectEqual(@as(u21, kitty.graphics.unicode.placeholder), c.codepoint()); try testing.expect(c.isEmpty()); } From deacb10fb1a6cdf093c0f4440ebca206c0fb5fc1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 25 Jul 2024 10:03:44 -0700 Subject: [PATCH 09/36] terminal: print must use codepoint() now to work with placeholders --- src/terminal/Terminal.zig | 4 ++-- src/terminal/page.zig | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 16b8a07b9..013325c3a 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -281,7 +281,7 @@ pub fn print(self: *Terminal, c: u21) !void { // column. Otherwise, we need to check if there is text to // figure out if we're attaching to the prev or current. if (self.screen.cursor.x != right_limit - 1) break :left 1; - break :left @intFromBool(!self.screen.cursor.page_cell.hasText()); + break :left @intFromBool(self.screen.cursor.page_cell.codepoint() == 0); }; // If the previous cell is a wide spacer tail, then we actually @@ -299,7 +299,7 @@ pub fn print(self: *Terminal, c: u21) !void { // If our cell has no content, then this is a new cell and // necessarily a grapheme break. - if (!prev.cell.hasText()) break :grapheme; + if (prev.cell.codepoint() == 0) break :grapheme; const grapheme_break = brk: { var state: unicode.GraphemeBreakState = .{}; diff --git a/src/terminal/page.zig b/src/terminal/page.zig index 0ad599641..396e976be 100644 --- a/src/terminal/page.zig +++ b/src/terminal/page.zig @@ -1161,7 +1161,7 @@ pub const Page = struct { pub fn appendGrapheme(self: *Page, row: *Row, cell: *Cell, cp: u21) Allocator.Error!void { defer self.assertIntegrity(); - if (comptime std.debug.runtime_safety) assert(cell.hasText()); + if (comptime std.debug.runtime_safety) assert(cell.codepoint() != 0); const cell_offset = getOffset(Cell, self.memory, cell); var map = self.grapheme_map.map(self.memory); From e656fe3b792d5f23fbd52f56333d251ed2c5d0b6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 25 Jul 2024 10:05:05 -0700 Subject: [PATCH 10/36] terminal/kitty: starting virtual placement iterator --- src/terminal/kitty/graphics_unicode.zig | 94 ++++++++++++++++++++++++- 1 file changed, 92 insertions(+), 2 deletions(-) diff --git a/src/terminal/kitty/graphics_unicode.zig b/src/terminal/kitty/graphics_unicode.zig index d85d07067..21f3657e5 100644 --- a/src/terminal/kitty/graphics_unicode.zig +++ b/src/terminal/kitty/graphics_unicode.zig @@ -4,10 +4,64 @@ const std = @import("std"); const assert = std.debug.assert; const testing = std.testing; +const terminal = @import("../main.zig"); /// Codepoint for the unicode placeholder character. pub const placeholder: u21 = 0x10EEEE; +/// Returns an iterator that iterates over all of the virtual placements +/// in the given pin. If `limit` is provided, the iterator will stop +/// when it reaches that pin (inclusive). If `limit` is not provided, +/// the iterator will continue until the end of the page list. +pub fn placementIterator( + pin: terminal.Pin, + limit: ?terminal.Pin, +) PlacementIterator { + var row_it = pin.rowIterator(.right_down, limit); + const row = row_it.next(); + return .{ .row_it = row_it, .row = row }; +} + +/// Iterator over unicode virtual placements. +pub const PlacementIterator = struct { + row_it: terminal.PageList.RowIterator, + row: ?terminal.Pin, + + pub fn next(self: *PlacementIterator) ?Placement { + while (self.row) |*row| { + // A row must have graphemes to possibly have virtual placements + // since virtual placements are done via diacritics. + if (row.rowAndCell().row.grapheme) { + // Iterate over our remaining cells and find one with a placeholder. + const cells = row.cells(.right); + for (cells, row.x..) |cell, x| { + if (cell.codepoint() != placeholder) continue; + + if (x == cells.len - 1) { + // We are at the end of this row so move to the next row + self.row = self.row_it.next(); + } else { + // We can move right to the next cell. + row.x = @intCast(x + 1); + } + + // TODO + return .{}; + } + } + + // We didn't find any placements. Move to the next row. + self.row = self.row_it.next(); + } + + return null; + } +}; + +/// A virtual placement in the terminal. This can represent more than +/// one cell if the cells combine to be a run. +pub const Placement = struct {}; + /// Get the row/col index for a diacritic codepoint. These are 0-indexed. pub fn getIndex(cp: u21) ?usize { return std.sort.binarySearch(u21, cp, diacritics, {}, (struct { @@ -324,7 +378,7 @@ const diacritics: []const u21 = &.{ 0x1D244, }; -test "sorted" { +test "unicode diacritic sorted" { // diacritics must be sorted since we use a binary search. try testing.expect(std.sort.isSorted(u21, diacritics, {}, (struct { fn lessThan(context: void, lhs: u21, rhs: u21) bool { @@ -334,8 +388,44 @@ test "sorted" { }).lessThan)); } -test "diacritic" { +test "unicode diacritic" { // Some spot checks based on Kitty behavior try testing.expectEqual(30, getIndex(0x483).?); try testing.expectEqual(294, getIndex(0x1d242).?); } + +test "unicode placement: none" { + const alloc = testing.allocator; + var t = try terminal.Terminal.init(alloc, .{ .rows = 5, .cols = 5 }); + defer t.deinit(alloc); + t.modes.set(.grapheme_cluster, true); + + // Single cell + try t.printString("hello\nworld\n1\n2"); + + // No placements + const pin = t.screen.pages.getTopLeft(.viewport); + var it = placementIterator(pin, null); + try testing.expect(it.next() == null); +} + +test "unicode placement: single" { + const alloc = testing.allocator; + var t = try terminal.Terminal.init(alloc, .{ .rows = 5, .cols = 5 }); + defer t.deinit(alloc); + t.modes.set(.grapheme_cluster, true); + + // Single cell + try t.printString("\u{10EEEE}\u{0305}\u{0305}"); + + // Get our top left pin + const pin = t.screen.pages.getTopLeft(.viewport); + + // Should have exactly one placement + var it = placementIterator(pin, null); + { + const p = it.next().?; + _ = p; + } + try testing.expect(it.next() == null); +} From 13df93a1d021bc2c7d5b066aa7d68acf8c0ea38d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 25 Jul 2024 10:35:50 -0700 Subject: [PATCH 11/36] temrinal/kitty: really basic row/col diacritic decoding --- src/terminal/PageList.zig | 2 +- src/terminal/kitty/graphics_unicode.zig | 74 +++++++++++++++++++++---- src/terminal/page.zig | 2 +- 3 files changed, 66 insertions(+), 12 deletions(-) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index 8a5713333..c1bed792f 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -3224,7 +3224,7 @@ pub const Pin = struct { /// Returns the grapheme codepoints for the given cell. These are only /// the EXTRA codepoints and not the first codepoint. - pub fn grapheme(self: Pin, cell: *pagepkg.Cell) ?[]u21 { + pub fn grapheme(self: Pin, cell: *const pagepkg.Cell) ?[]u21 { return self.page.data.lookupGrapheme(cell); } diff --git a/src/terminal/kitty/graphics_unicode.zig b/src/terminal/kitty/graphics_unicode.zig index 21f3657e5..ff1e60955 100644 --- a/src/terminal/kitty/graphics_unicode.zig +++ b/src/terminal/kitty/graphics_unicode.zig @@ -32,21 +32,60 @@ pub const PlacementIterator = struct { // A row must have graphemes to possibly have virtual placements // since virtual placements are done via diacritics. if (row.rowAndCell().row.grapheme) { + // TODO: document + const prev: ?Placement = null; + _ = prev; + // Iterate over our remaining cells and find one with a placeholder. const cells = row.cells(.right); - for (cells, row.x..) |cell, x| { + for (cells, row.x..) |*cell, x| { if (cell.codepoint() != placeholder) continue; + // TODO: we need to support non-grapheme cells that just + // do continuations all the way through. + assert(cell.hasGrapheme()); + + // "row" now points to the top-left pin of the placement. + row.x = @intCast(x); + + // Build our placement + var p: Placement = .{ + .pin = row.*, + + // Filled in below. Marked as undefined so we can catch + // bugs with safety checks. + .col = undefined, + .row = undefined, + + // For now we don't build runs and we always produce + // single cell placements. + .width = 1, + .height = 1, + }; + + // Determine our row/col by looking at the diacritics. + const cps: []const u21 = row.grapheme(cell) orelse &.{}; + if (cps.len > 0) { + p.row = getIndex(cps[0]) orelse @panic("TODO: invalid"); + if (cps.len > 1) { + p.col = getIndex(cps[1]) orelse @panic("TODO: invalid"); + if (cps.len > 2) { + @panic("TODO: higher 8 bits of image ID"); + } + } + } else @panic("TODO: continuations"); + if (x == cells.len - 1) { // We are at the end of this row so move to the next row self.row = self.row_it.next(); } else { - // We can move right to the next cell. - row.x = @intCast(x + 1); + // We can move right to the next cell. row is a pointer + // to self.row so we can modify it directly. + assert(@intFromPtr(row) == @intFromPtr(&self.row)); + row.x += 1; } - // TODO - return .{}; + return p; } } @@ -60,16 +99,30 @@ pub const PlacementIterator = struct { /// A virtual placement in the terminal. This can represent more than /// one cell if the cells combine to be a run. -pub const Placement = struct {}; +pub const Placement = struct { + /// The top-left pin of the placement. This can be used to get the + /// screen x/y. + pin: terminal.Pin, + + /// Starting row/col index for the image itself. This is the "fragment" + /// of the image we want to show in this placement. This is 0-indexed. + col: u32, + row: u32, + + /// The width/height in cells of this placement. + width: u32, + height: u32, +}; /// Get the row/col index for a diacritic codepoint. These are 0-indexed. -pub fn getIndex(cp: u21) ?usize { - return std.sort.binarySearch(u21, cp, diacritics, {}, (struct { +pub fn getIndex(cp: u21) ?u32 { + const idx = std.sort.binarySearch(u21, cp, diacritics, {}, (struct { fn order(context: void, lhs: u21, rhs: u21) std.math.Order { _ = context; return std.math.order(lhs, rhs); } - }).order); + }).order) orelse return null; + return @intCast(idx); } /// These are the diacritics used with the Kitty graphics protocol @@ -425,7 +478,8 @@ test "unicode placement: single" { var it = placementIterator(pin, null); { const p = it.next().?; - _ = p; + try testing.expectEqual(0, p.row); + try testing.expectEqual(0, p.col); } try testing.expect(it.next() == null); } diff --git a/src/terminal/page.zig b/src/terminal/page.zig index 396e976be..18b63f126 100644 --- a/src/terminal/page.zig +++ b/src/terminal/page.zig @@ -1220,7 +1220,7 @@ pub const Page = struct { /// Returns the codepoints for the given cell. These are the codepoints /// in addition to the first codepoint. The first codepoint is NOT /// included since it is on the cell itself. - pub fn lookupGrapheme(self: *const Page, cell: *Cell) ?[]u21 { + pub fn lookupGrapheme(self: *const Page, cell: *const Cell) ?[]u21 { const cell_offset = getOffset(Cell, self.memory, cell); const map = self.grapheme_map.map(self.memory); const slice = map.get(cell_offset) orelse return null; From 578bfc8d230665c2ff75234e089648695af8b3de Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 25 Jul 2024 10:54:00 -0700 Subject: [PATCH 12/36] terminal/kitty: parse image/placement id from style --- src/terminal/kitty/graphics_unicode.zig | 96 +++++++++++++++++++++++-- 1 file changed, 92 insertions(+), 4 deletions(-) diff --git a/src/terminal/kitty/graphics_unicode.zig b/src/terminal/kitty/graphics_unicode.zig index ff1e60955..e67790425 100644 --- a/src/terminal/kitty/graphics_unicode.zig +++ b/src/terminal/kitty/graphics_unicode.zig @@ -22,6 +22,23 @@ pub fn placementIterator( return .{ .row_it = row_it, .row = row }; } +/// Convert a style color to a Kitty image protocol ID. This works by +/// taking the 24 most significant bits of the color, which lets it work +/// for both palette and rgb-based colors. +fn colorToId(c: terminal.Style.Color) u32 { + // TODO: test this + return switch (c) { + .none => 0, + .palette => |v| @intCast(v), + .rgb => |rgb| rgb: { + const r: u24 = @intCast(rgb.r); + const g: u24 = @intCast(rgb.g); + const b: u24 = @intCast(rgb.b); + break :rgb (r << 16) | (g << 8) | b; + }, + }; +} + /// Iterator over unicode virtual placements. pub const PlacementIterator = struct { row_it: terminal.PageList.RowIterator, @@ -32,9 +49,11 @@ pub const PlacementIterator = struct { // A row must have graphemes to possibly have virtual placements // since virtual placements are done via diacritics. if (row.rowAndCell().row.grapheme) { - // TODO: document - const prev: ?Placement = null; - _ = prev; + // Our current run. A run is always only a single row. This + // assumption is built-in to our logic so if we want to change + // this later we have to redo the logic; tests should cover; + const run: ?Placement = null; + _ = run; // Iterate over our remaining cells and find one with a placeholder. const cells = row.cells(.right); @@ -48,9 +67,16 @@ pub const PlacementIterator = struct { // "row" now points to the top-left pin of the placement. row.x = @intCast(x); + // Determine our image ID and placement ID from the style. + const style = row.style(cell); + const image_id = colorToId(style.fg_color); + const placement_id = colorToId(style.underline_color); + // Build our placement var p: Placement = .{ .pin = row.*, + .image_id = image_id, + .placement_id = placement_id, // Filled in below. Marked as undefined so we can catch // bugs with safety checks. @@ -64,6 +90,8 @@ pub const PlacementIterator = struct { }; // Determine our row/col by looking at the diacritics. + // If the cell doesn't have graphemes that's okay because + // of continuations. const cps: []const u21 = row.grapheme(cell) orelse &.{}; if (cps.len > 0) { p.row = getIndex(cps[0]) orelse @panic("TODO: invalid"); @@ -104,6 +132,13 @@ pub const Placement = struct { /// screen x/y. pin: terminal.Pin, + /// The image ID and placement ID for this virtual placement. The + /// image ID is encoded in the fg color (plus optional a 8-bit high + /// value in the 3rd diacritic). The placement ID is encoded in the + /// underline color (optionally). + image_id: u32, + placement_id: u32, + /// Starting row/col index for the image itself. This is the "fragment" /// of the image we want to show in this placement. This is 0-indexed. col: u32, @@ -462,7 +497,7 @@ test "unicode placement: none" { try testing.expect(it.next() == null); } -test "unicode placement: single" { +test "unicode placement: single row/col" { const alloc = testing.allocator; var t = try terminal.Terminal.init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc); @@ -478,6 +513,59 @@ test "unicode placement: single" { var it = placementIterator(pin, null); { const p = it.next().?; + try testing.expectEqual(0, p.image_id); + try testing.expectEqual(0, p.placement_id); + try testing.expectEqual(0, p.row); + try testing.expectEqual(0, p.col); + } + try testing.expect(it.next() == null); +} + +test "unicode placement: specifying image id as palette" { + const alloc = testing.allocator; + var t = try terminal.Terminal.init(alloc, .{ .rows = 5, .cols = 5 }); + defer t.deinit(alloc); + t.modes.set(.grapheme_cluster, true); + + // Single cell + try t.setAttribute(.{ .@"256_fg" = 42 }); + try t.printString("\u{10EEEE}\u{0305}\u{0305}"); + + // Get our top left pin + const pin = t.screen.pages.getTopLeft(.viewport); + + // Should have exactly one placement + var it = placementIterator(pin, null); + { + const p = it.next().?; + try testing.expectEqual(42, p.image_id); + try testing.expectEqual(0, p.placement_id); + try testing.expectEqual(0, p.row); + try testing.expectEqual(0, p.col); + } + try testing.expect(it.next() == null); +} + +test "unicode placement: specifying placement id as palette" { + const alloc = testing.allocator; + var t = try terminal.Terminal.init(alloc, .{ .rows = 5, .cols = 5 }); + defer t.deinit(alloc); + t.modes.set(.grapheme_cluster, true); + + // Single cell + try t.setAttribute(.{ .@"256_fg" = 42 }); + try t.setAttribute(.{ .@"256_underline_color" = 21 }); + try t.printString("\u{10EEEE}\u{0305}\u{0305}"); + + // Get our top left pin + const pin = t.screen.pages.getTopLeft(.viewport); + + // Should have exactly one placement + var it = placementIterator(pin, null); + { + const p = it.next().?; + try testing.expectEqual(42, p.image_id); + try testing.expectEqual(21, p.placement_id); try testing.expectEqual(0, p.row); try testing.expectEqual(0, p.col); } From 7c6ae90300527f7e54ad06eb4ab8ba605524ec60 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 25 Jul 2024 14:09:58 -0700 Subject: [PATCH 13/36] terminal/kitty: implement high bit image id parsing --- src/terminal/kitty/graphics_unicode.zig | 28 ++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/terminal/kitty/graphics_unicode.zig b/src/terminal/kitty/graphics_unicode.zig index e67790425..e04b20bd5 100644 --- a/src/terminal/kitty/graphics_unicode.zig +++ b/src/terminal/kitty/graphics_unicode.zig @@ -98,7 +98,8 @@ pub const PlacementIterator = struct { if (cps.len > 1) { p.col = getIndex(cps[1]) orelse @panic("TODO: invalid"); if (cps.len > 2) { - @panic("TODO: higher 8 bits of image ID"); + const high = getIndex(cps[2]) orelse @panic("TODO: invalid"); + p.image_id += high << 24; } } } else @panic("TODO: continuations"); @@ -546,6 +547,31 @@ test "unicode placement: specifying image id as palette" { try testing.expect(it.next() == null); } +test "unicode placement: specifying image id with high bits" { + const alloc = testing.allocator; + var t = try terminal.Terminal.init(alloc, .{ .rows = 5, .cols = 5 }); + defer t.deinit(alloc); + t.modes.set(.grapheme_cluster, true); + + // Single cell + try t.setAttribute(.{ .@"256_fg" = 42 }); + try t.printString("\u{10EEEE}\u{0305}\u{0305}\u{030E}"); + + // Get our top left pin + const pin = t.screen.pages.getTopLeft(.viewport); + + // Should have exactly one placement + var it = placementIterator(pin, null); + { + const p = it.next().?; + try testing.expectEqual(33554474, p.image_id); + try testing.expectEqual(0, p.placement_id); + try testing.expectEqual(0, p.row); + try testing.expectEqual(0, p.col); + } + try testing.expect(it.next() == null); +} + test "unicode placement: specifying placement id as palette" { const alloc = testing.allocator; var t = try terminal.Terminal.init(alloc, .{ .rows = 5, .cols = 5 }); From cf6463fec0bef77c173d969ecb0e9a4058e4f9d3 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 25 Jul 2024 14:56:46 -0700 Subject: [PATCH 14/36] terminal/kitty: preparing to build runs of placements --- src/terminal/PageList.zig | 2 +- src/terminal/kitty/graphics_unicode.zig | 255 +++++++++++++++++------- 2 files changed, 183 insertions(+), 74 deletions(-) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index c1bed792f..9590f2fbb 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -3229,7 +3229,7 @@ pub const Pin = struct { } /// Returns the style for the given cell in this pin. - pub fn style(self: Pin, cell: *pagepkg.Cell) stylepkg.Style { + pub fn style(self: Pin, cell: *const pagepkg.Cell) stylepkg.Style { if (cell.style_id == stylepkg.default_id) return .{}; return self.page.data.styles.get( self.page.data.memory, diff --git a/src/terminal/kitty/graphics_unicode.zig b/src/terminal/kitty/graphics_unicode.zig index e04b20bd5..121e3a157 100644 --- a/src/terminal/kitty/graphics_unicode.zig +++ b/src/terminal/kitty/graphics_unicode.zig @@ -6,6 +6,8 @@ const assert = std.debug.assert; const testing = std.testing; const terminal = @import("../main.zig"); +const log = std.log.scoped(.kitty_gfx); + /// Codepoint for the unicode placeholder character. pub const placeholder: u21 = 0x10EEEE; @@ -22,23 +24,6 @@ pub fn placementIterator( return .{ .row_it = row_it, .row = row }; } -/// Convert a style color to a Kitty image protocol ID. This works by -/// taking the 24 most significant bits of the color, which lets it work -/// for both palette and rgb-based colors. -fn colorToId(c: terminal.Style.Color) u32 { - // TODO: test this - return switch (c) { - .none => 0, - .palette => |v| @intCast(v), - .rgb => |rgb| rgb: { - const r: u24 = @intCast(rgb.r); - const g: u24 = @intCast(rgb.g); - const b: u24 = @intCast(rgb.b); - break :rgb (r << 16) | (g << 8) | b; - }, - }; -} - /// Iterator over unicode virtual placements. pub const PlacementIterator = struct { row_it: terminal.PageList.RowIterator, @@ -46,80 +31,62 @@ pub const PlacementIterator = struct { pub fn next(self: *PlacementIterator) ?Placement { while (self.row) |*row| { + // Our current run. A run is always only a single row. This + // assumption is built-in to our logic so if we want to change + // this later we have to redo the logic; tests should cover; + var run: ?IncompletePlacement = null; + // A row must have graphemes to possibly have virtual placements // since virtual placements are done via diacritics. if (row.rowAndCell().row.grapheme) { - // Our current run. A run is always only a single row. This - // assumption is built-in to our logic so if we want to change - // this later we have to redo the logic; tests should cover; - const run: ?Placement = null; - _ = run; - // Iterate over our remaining cells and find one with a placeholder. const cells = row.cells(.right); for (cells, row.x..) |*cell, x| { - if (cell.codepoint() != placeholder) continue; + // "row" now points to the top-left pin of the placement. + // We need this temporary state to build our incomplete + // placement. + assert(@intFromPtr(row) == @intFromPtr(&self.row)); + row.x = @intCast(x); + + // If this cell doesn't have the placeholder, then we + // complete the run if we have it otherwise we just move + // on and keep searching. + if (cell.codepoint() != placeholder) { + if (run) |prev| return prev.complete(); + continue; + } // TODO: we need to support non-grapheme cells that just // do continuations all the way through. assert(cell.hasGrapheme()); - // "row" now points to the top-left pin of the placement. - row.x = @intCast(x); - - // Determine our image ID and placement ID from the style. - const style = row.style(cell); - const image_id = colorToId(style.fg_color); - const placement_id = colorToId(style.underline_color); - - // Build our placement - var p: Placement = .{ - .pin = row.*, - .image_id = image_id, - .placement_id = placement_id, - - // Filled in below. Marked as undefined so we can catch - // bugs with safety checks. - .col = undefined, - .row = undefined, - - // For now we don't build runs and we always produce - // single cell placements. - .width = 1, - .height = 1, - }; - - // Determine our row/col by looking at the diacritics. - // If the cell doesn't have graphemes that's okay because - // of continuations. - const cps: []const u21 = row.grapheme(cell) orelse &.{}; - if (cps.len > 0) { - p.row = getIndex(cps[0]) orelse @panic("TODO: invalid"); - if (cps.len > 1) { - p.col = getIndex(cps[1]) orelse @panic("TODO: invalid"); - if (cps.len > 2) { - const high = getIndex(cps[2]) orelse @panic("TODO: invalid"); - p.image_id += high << 24; - } + // If we don't have a previous run, then we save this + // incomplete one, start a run, and move on. + const curr = IncompletePlacement.init(row, cell); + if (run) |*prev| { + // If we can't append, then we complete the previous + // run and return it. + if (!prev.append(&curr)) { + // Note: self.row is already updated due to the + // row pointer above. It points back at this same + // cell so we can continue the new placements from + // here. + return prev.complete(); } - } else @panic("TODO: continuations"); - if (x == cells.len - 1) { - // We are at the end of this row so move to the next row - self.row = self.row_it.next(); + // append is mutating so if we reached this point + // then prev has been updated. } else { - // We can move right to the next cell. row is a pointer - // to self.row so we can modify it directly. - assert(@intFromPtr(row) == @intFromPtr(&self.row)); - row.x += 1; + run = curr; } - - return p; } } - // We didn't find any placements. Move to the next row. + // We move to the next row no matter what self.row = self.row_it.next(); + + // If we have a run, we complete it here. + if (run) |prev| return prev.complete(); } return null; @@ -150,8 +117,150 @@ pub const Placement = struct { height: u32, }; +/// IncompletePlacement is the placement information present in a single +/// cell. It is "incomplete" because the specification allows for missing +/// diacritics and so on that continue from previous valid placements. +const IncompletePlacement = struct { + /// The pin of the cell that created this incomplete placement. + pin: terminal.Pin, + + /// Lower 24 bits of the image ID. This is specified in the fg color + /// and is always required. + image_id_low: u24, + + /// Higher 8 bits of the image ID specified using the 3rd diacritic. + /// This is optional. + image_id_high: ?u8 = null, + + /// Placement ID is optionally specified in the underline color. + placement_id: ?u24 = null, + + /// The row/col index for the image. These are 0-indexed. These + /// are specified using diacritics. The row is first and the col + /// is second. Both are optional. If not specified, they can continue + /// a previous placement under certain conditions. + row: ?u32 = null, + col: ?u32 = null, + + /// Parse the incomplete placement information from a row and cell. + /// + /// The cell could be derived from the row but in our usage we already + /// have the cell and we don't want to waste cycles recomputing it. + pub fn init( + row: *const terminal.Pin, + cell: *const terminal.Cell, + ) IncompletePlacement { + assert(cell.codepoint() == placeholder); + const style = row.style(cell); + + var result: IncompletePlacement = .{ + .pin = row.*, + .image_id_low = colorToId(style.fg_color), + .placement_id = placement_id: { + const id = colorToId(style.underline_color); + break :placement_id if (id != 0) id else null; + }, + }; + + // Try to decode all our diacritics. Any invalid diacritics are + // treated as if they don't exist. This isn't explicitly specified + // at the time of writing this but it appears to be how Kitty behaves. + const cps: []const u21 = row.grapheme(cell) orelse &.{}; + if (cps.len > 0) { + result.row = getIndex(cps[0]) orelse value: { + log.warn("virtual placement with invalid row diacritic cp={X}", .{cps[0]}); + break :value null; + }; + + if (cps.len > 1) { + result.col = getIndex(cps[1]) orelse value: { + log.warn("virtual placement with invalid col diacritic cp={X}", .{cps[1]}); + break :value null; + }; + + if (cps.len > 2) { + const high_ = getIndex(cps[2]) orelse value: { + log.warn("virtual placement with invalid high diacritic cp={X}", .{cps[2]}); + break :value null; + }; + + if (high_) |high| { + result.image_id_high = std.math.cast( + u8, + high, + ) orelse value: { + log.warn("virtual placement with invalid high diacritic cp={X} value={}", .{ + cps[2], + high, + }); + break :value null; + }; + } + + // Any additional diacritics are ignored. + } + } + } + + return result; + } + + /// Append this incomplete placement to an existing placement to + /// create a run. This returns true if the placements are compatible + /// and were combined. If this returns false, the other placement is + /// unchanged. + pub fn append(self: *IncompletePlacement, other: *const IncompletePlacement) bool { + return self.canAppend(other); + } + + fn canAppend(self: *const IncompletePlacement, other: *const IncompletePlacement) bool { + if (self.image_id_low != other.image_id_low) return false; + if (self.placement_id != other.placement_id) return false; + return false; + } + + /// Complete the incomplete placement to create a full placement. + /// This creates a new placement that isn't continuous with any previous + /// placements. + /// + /// The pin is the pin of the cell that created this incomplete placement. + pub fn complete(self: *const IncompletePlacement) Placement { + return .{ + .pin = self.pin, + .image_id = image_id: { + const low: u32 = @intCast(self.image_id_low); + const high: u32 = @intCast(self.image_id_high orelse 0); + break :image_id low | (high << 24); + }, + + .placement_id = self.placement_id orelse 0, + .col = self.col orelse 0, + .row = self.row orelse 0, + .width = 1, + .height = 1, + }; + } + + /// Convert a style color to a Kitty image protocol ID. This works by + /// taking the 24 most significant bits of the color, which lets it work + /// for both palette and rgb-based colors. + fn colorToId(c: terminal.Style.Color) u24 { + // TODO: test this + return switch (c) { + .none => 0, + .palette => |v| @intCast(v), + .rgb => |rgb| rgb: { + const r: u24 = @intCast(rgb.r); + const g: u24 = @intCast(rgb.g); + const b: u24 = @intCast(rgb.b); + break :rgb (r << 16) | (g << 8) | b; + }, + }; + } +}; + /// Get the row/col index for a diacritic codepoint. These are 0-indexed. -pub fn getIndex(cp: u21) ?u32 { +fn getIndex(cp: u21) ?u32 { const idx = std.sort.binarySearch(u21, cp, diacritics, {}, (struct { fn order(context: void, lhs: u21, rhs: u21) std.math.Order { _ = context; From 1786502f15bcc2d5808189f1a31637b653954d5d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 25 Jul 2024 15:31:40 -0700 Subject: [PATCH 15/36] terminal/kitty: working runs --- src/terminal/kitty/graphics_unicode.zig | 112 ++++++++++++++++++++++-- 1 file changed, 106 insertions(+), 6 deletions(-) diff --git a/src/terminal/kitty/graphics_unicode.zig b/src/terminal/kitty/graphics_unicode.zig index 121e3a157..cb9703dd7 100644 --- a/src/terminal/kitty/graphics_unicode.zig +++ b/src/terminal/kitty/graphics_unicode.zig @@ -77,7 +77,11 @@ pub const PlacementIterator = struct { // append is mutating so if we reached this point // then prev has been updated. } else { - run = curr; + // For appending, we need to set our initial values. + var prev = curr; + if (prev.row == null) prev.row = 0; + if (prev.col == null) prev.col = 0; + run = prev; } } } @@ -142,6 +146,9 @@ const IncompletePlacement = struct { row: ?u32 = null, col: ?u32 = null, + /// The run width so far in cells. + width: u32 = 1, + /// Parse the incomplete placement information from a row and cell. /// /// The cell could be derived from the row but in our usage we already @@ -210,13 +217,18 @@ const IncompletePlacement = struct { /// and were combined. If this returns false, the other placement is /// unchanged. pub fn append(self: *IncompletePlacement, other: *const IncompletePlacement) bool { - return self.canAppend(other); + if (!self.canAppend(other)) return false; + self.width += 1; + return true; } fn canAppend(self: *const IncompletePlacement, other: *const IncompletePlacement) bool { - if (self.image_id_low != other.image_id_low) return false; - if (self.placement_id != other.placement_id) return false; - return false; + // Converted from Kitty's logic, don't @ me. + return self.image_id_low == other.image_id_low and + self.placement_id == other.placement_id and + (other.row == null or other.row == self.row) and + (other.col == null or other.col == self.col.? + self.width) and + (other.image_id_high == null or other.image_id_high == self.image_id_high); } /// Complete the incomplete placement to create a full placement. @@ -236,7 +248,7 @@ const IncompletePlacement = struct { .placement_id = self.placement_id orelse 0, .col = self.col orelse 0, .row = self.row orelse 0, - .width = 1, + .width = self.width, .height = 1, }; } @@ -631,6 +643,94 @@ test "unicode placement: single row/col" { try testing.expect(it.next() == null); } +test "unicode placement: continuation break" { + const alloc = testing.allocator; + var t = try terminal.Terminal.init(alloc, .{ .rows = 5, .cols = 10 }); + defer t.deinit(alloc); + t.modes.set(.grapheme_cluster, true); + + // Two runs because it jumps cols + try t.printString("\u{10EEEE}\u{0305}\u{0305}"); + try t.printString("\u{10EEEE}\u{0305}\u{030E}"); + + // Get our top left pin + const pin = t.screen.pages.getTopLeft(.viewport); + + // Should have exactly one placement + var it = placementIterator(pin, null); + { + const p = it.next().?; + try testing.expectEqual(0, p.image_id); + try testing.expectEqual(0, p.placement_id); + try testing.expectEqual(0, p.row); + try testing.expectEqual(0, p.col); + try testing.expectEqual(1, p.width); + } + { + const p = it.next().?; + try testing.expectEqual(0, p.image_id); + try testing.expectEqual(0, p.placement_id); + try testing.expectEqual(0, p.row); + try testing.expectEqual(2, p.col); + try testing.expectEqual(1, p.width); + } + try testing.expect(it.next() == null); +} + +test "unicode placement: continuation with diacritics set" { + const alloc = testing.allocator; + var t = try terminal.Terminal.init(alloc, .{ .rows = 5, .cols = 10 }); + defer t.deinit(alloc); + t.modes.set(.grapheme_cluster, true); + + // Three cells. They'll continue even though they're explicit + try t.printString("\u{10EEEE}\u{0305}\u{0305}"); + try t.printString("\u{10EEEE}\u{0305}\u{030D}"); + try t.printString("\u{10EEEE}\u{0305}\u{030E}"); + + // Get our top left pin + const pin = t.screen.pages.getTopLeft(.viewport); + + // Should have exactly one placement + var it = placementIterator(pin, null); + { + const p = it.next().?; + try testing.expectEqual(0, p.image_id); + try testing.expectEqual(0, p.placement_id); + try testing.expectEqual(0, p.row); + try testing.expectEqual(0, p.col); + try testing.expectEqual(3, p.width); + } + try testing.expect(it.next() == null); +} + +test "unicode placement: continuation with no col" { + const alloc = testing.allocator; + var t = try terminal.Terminal.init(alloc, .{ .rows = 5, .cols = 10 }); + defer t.deinit(alloc); + t.modes.set(.grapheme_cluster, true); + + // Three cells. They'll continue even though they're explicit + try t.printString("\u{10EEEE}\u{0305}\u{0305}"); + try t.printString("\u{10EEEE}\u{0305}"); + try t.printString("\u{10EEEE}\u{0305}"); + + // Get our top left pin + const pin = t.screen.pages.getTopLeft(.viewport); + + // Should have exactly one placement + var it = placementIterator(pin, null); + { + const p = it.next().?; + try testing.expectEqual(0, p.image_id); + try testing.expectEqual(0, p.placement_id); + try testing.expectEqual(0, p.row); + try testing.expectEqual(0, p.col); + try testing.expectEqual(3, p.width); + } + try testing.expect(it.next() == null); +} + test "unicode placement: specifying image id as palette" { const alloc = testing.allocator; var t = try terminal.Terminal.init(alloc, .{ .rows = 5, .cols = 5 }); From 91a6e70d1b1d9fb307e644bfb430df06b3fcbb98 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 25 Jul 2024 18:56:47 -0700 Subject: [PATCH 16/36] terminal/kitty: extra placement tests --- src/terminal/kitty/graphics_unicode.zig | 55 ++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/src/terminal/kitty/graphics_unicode.zig b/src/terminal/kitty/graphics_unicode.zig index cb9703dd7..c77d037fa 100644 --- a/src/terminal/kitty/graphics_unicode.zig +++ b/src/terminal/kitty/graphics_unicode.zig @@ -257,7 +257,6 @@ const IncompletePlacement = struct { /// taking the 24 most significant bits of the color, which lets it work /// for both palette and rgb-based colors. fn colorToId(c: terminal.Style.Color) u24 { - // TODO: test this return switch (c) { .none => 0, .palette => |v| @intCast(v), @@ -731,6 +730,60 @@ test "unicode placement: continuation with no col" { try testing.expect(it.next() == null); } +test "unicode placement: run ending" { + const alloc = testing.allocator; + var t = try terminal.Terminal.init(alloc, .{ .rows = 5, .cols = 10 }); + defer t.deinit(alloc); + t.modes.set(.grapheme_cluster, true); + + // Three cells. They'll continue even though they're explicit + try t.printString("\u{10EEEE}\u{0305}\u{0305}"); + try t.printString("\u{10EEEE}\u{0305}\u{030D}"); + try t.printString("ABC"); + + // Get our top left pin + const pin = t.screen.pages.getTopLeft(.viewport); + + // Should have exactly one placement + var it = placementIterator(pin, null); + { + const p = it.next().?; + try testing.expectEqual(0, p.image_id); + try testing.expectEqual(0, p.placement_id); + try testing.expectEqual(0, p.row); + try testing.expectEqual(0, p.col); + try testing.expectEqual(2, p.width); + } + try testing.expect(it.next() == null); +} + +test "unicode placement: run starting in the middle" { + const alloc = testing.allocator; + var t = try terminal.Terminal.init(alloc, .{ .rows = 5, .cols = 10 }); + defer t.deinit(alloc); + t.modes.set(.grapheme_cluster, true); + + // Three cells. They'll continue even though they're explicit + try t.printString("ABC"); + try t.printString("\u{10EEEE}\u{0305}\u{0305}"); + try t.printString("\u{10EEEE}\u{0305}\u{030D}"); + + // Get our top left pin + const pin = t.screen.pages.getTopLeft(.viewport); + + // Should have exactly one placement + var it = placementIterator(pin, null); + { + const p = it.next().?; + try testing.expectEqual(0, p.image_id); + try testing.expectEqual(0, p.placement_id); + try testing.expectEqual(0, p.row); + try testing.expectEqual(0, p.col); + try testing.expectEqual(2, p.width); + } + try testing.expect(it.next() == null); +} + test "unicode placement: specifying image id as palette" { const alloc = testing.allocator; var t = try terminal.Terminal.init(alloc, .{ .rows = 5, .cols = 5 }); From 3549619a64aab3d85f729008026663c5990b16a2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 25 Jul 2024 19:35:11 -0700 Subject: [PATCH 17/36] terminal: introduce row bit for kitty virtual placeholders --- src/terminal/PageList.zig | 6 ++++++ src/terminal/Screen.zig | 10 ++++++++++ src/terminal/Terminal.zig | 6 ++++++ src/terminal/page.zig | 23 ++++++++++++++++++++++- 4 files changed, 44 insertions(+), 1 deletion(-) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index 9590f2fbb..403fe7e5a 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -7,6 +7,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const assert = std.debug.assert; const fastmem = @import("../fastmem.zig"); +const kitty = @import("kitty.zig"); const point = @import("point.zig"); const pagepkg = @import("page.zig"); const stylepkg = @import("style.zig"); @@ -1056,6 +1057,11 @@ const ReflowCursor = struct { self.page_cell.style_id = id; } + // Copy Kitty virtual placeholder status + if (cell.codepoint() == kitty.graphics.unicode.placeholder) { + self.page_row.kitty_virtual_placeholder = true; + } + self.cursorForward(); } diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 12bdaa73c..30ffc39e3 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -984,6 +984,16 @@ pub fn clearCells( if (cells.len == self.pages.cols) row.styled = false; } + if (row.kitty_virtual_placeholder and + cells.len == self.pages.cols) + { + for (cells) |c| { + if (c.codepoint() == kitty.graphics.unicode.placeholder) { + break; + } + } else row.kitty_virtual_placeholder = false; + } + @memset(cells, self.blankCell()); } diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 013325c3a..d6b8a7376 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -640,6 +640,12 @@ fn printCell( } } + // If this is a Kitty unicode placeholder then we need to mark the + // row so that the renderer can lookup rows with these much faster. + if (c == kitty.graphics.unicode.placeholder) { + self.screen.cursor.page_row.kitty_virtual_placeholder = true; + } + // We check for an active hyperlink first because setHyperlink // handles clearing the old hyperlink and an optimization if we're // overwriting the same hyperlink. diff --git a/src/terminal/page.zig b/src/terminal/page.zig index 18b63f126..9d149270e 100644 --- a/src/terminal/page.zig +++ b/src/terminal/page.zig @@ -826,6 +826,9 @@ pub const Page = struct { src_cell.style_id, ) orelse src_cell.style_id; } + if (src_cell.codepoint() == kitty.graphics.unicode.placeholder) { + dst_row.kitty_virtual_placeholder = true; + } } } @@ -913,6 +916,9 @@ pub const Page = struct { dst.hyperlink = true; dst_row.hyperlink = true; } + if (src.codepoint() == kitty.graphics.unicode.placeholder) { + dst_row.kitty_virtual_placeholder = true; + } } } @@ -932,6 +938,7 @@ pub const Page = struct { src_row.grapheme = false; src_row.hyperlink = false; src_row.styled = false; + src_row.kitty_virtual_placeholder = false; } } @@ -1029,6 +1036,16 @@ pub const Page = struct { if (cells.len == self.size.cols) row.styled = false; } + if (row.kitty_virtual_placeholder and + cells.len == self.size.cols) + { + for (cells) |c| { + if (c.codepoint() == kitty.graphics.unicode.placeholder) { + break; + } + } else row.kitty_virtual_placeholder = false; + } + // Zero the cells as u64s since empirically this seems // to be a bit faster than using @memset(cells, .{}) @memset(@as([]u64, @ptrCast(cells)), 0); @@ -1552,7 +1569,11 @@ pub const Row = packed struct(u64) { /// running program, or "unknown" if it was never set. semantic_prompt: SemanticPrompt = .unknown, - _padding: u24 = 0, + /// True if this row contains a virtual placeholder for the Kitty + /// graphics protocol. (U+10EEEE) + kitty_virtual_placeholder: bool = false, + + _padding: u23 = 0, /// Semantic prompt type. pub const SemanticPrompt = enum(u3) { From 266033670d7f3ca7c4b29189ffe3d4885d62a860 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 25 Jul 2024 19:42:52 -0700 Subject: [PATCH 18/36] terminal/kitty: support cells with no diacritics --- src/terminal/kitty/graphics_unicode.zig | 113 +++++++++++++++--------- 1 file changed, 69 insertions(+), 44 deletions(-) diff --git a/src/terminal/kitty/graphics_unicode.zig b/src/terminal/kitty/graphics_unicode.zig index c77d037fa..a908ac200 100644 --- a/src/terminal/kitty/graphics_unicode.zig +++ b/src/terminal/kitty/graphics_unicode.zig @@ -31,58 +31,56 @@ pub const PlacementIterator = struct { pub fn next(self: *PlacementIterator) ?Placement { while (self.row) |*row| { + // This row flag is set on rows that have the virtual placeholder + if (!row.rowAndCell().row.kitty_virtual_placeholder) { + self.row = self.row_it.next(); + continue; + } + // Our current run. A run is always only a single row. This // assumption is built-in to our logic so if we want to change // this later we have to redo the logic; tests should cover; var run: ?IncompletePlacement = null; - // A row must have graphemes to possibly have virtual placements - // since virtual placements are done via diacritics. - if (row.rowAndCell().row.grapheme) { - // Iterate over our remaining cells and find one with a placeholder. - const cells = row.cells(.right); - for (cells, row.x..) |*cell, x| { - // "row" now points to the top-left pin of the placement. - // We need this temporary state to build our incomplete - // placement. - assert(@intFromPtr(row) == @intFromPtr(&self.row)); - row.x = @intCast(x); + // Iterate over our remaining cells and find one with a placeholder. + const cells = row.cells(.right); + for (cells, row.x..) |*cell, x| { + // "row" now points to the top-left pin of the placement. + // We need this temporary state to build our incomplete + // placement. + assert(@intFromPtr(row) == @intFromPtr(&self.row)); + row.x = @intCast(x); - // If this cell doesn't have the placeholder, then we - // complete the run if we have it otherwise we just move - // on and keep searching. - if (cell.codepoint() != placeholder) { - if (run) |prev| return prev.complete(); - continue; + // If this cell doesn't have the placeholder, then we + // complete the run if we have it otherwise we just move + // on and keep searching. + if (cell.codepoint() != placeholder) { + if (run) |prev| return prev.complete(); + continue; + } + + // If we don't have a previous run, then we save this + // incomplete one, start a run, and move on. + const curr = IncompletePlacement.init(row, cell); + if (run) |*prev| { + // If we can't append, then we complete the previous + // run and return it. + if (!prev.append(&curr)) { + // Note: self.row is already updated due to the + // row pointer above. It points back at this same + // cell so we can continue the new placements from + // here. + return prev.complete(); } - // TODO: we need to support non-grapheme cells that just - // do continuations all the way through. - assert(cell.hasGrapheme()); - - // If we don't have a previous run, then we save this - // incomplete one, start a run, and move on. - const curr = IncompletePlacement.init(row, cell); - if (run) |*prev| { - // If we can't append, then we complete the previous - // run and return it. - if (!prev.append(&curr)) { - // Note: self.row is already updated due to the - // row pointer above. It points back at this same - // cell so we can continue the new placements from - // here. - return prev.complete(); - } - - // append is mutating so if we reached this point - // then prev has been updated. - } else { - // For appending, we need to set our initial values. - var prev = curr; - if (prev.row == null) prev.row = 0; - if (prev.col == null) prev.col = 0; - run = prev; - } + // append is mutating so if we reached this point + // then prev has been updated. + } else { + // For appending, we need to set our initial values. + var prev = curr; + if (prev.row == null) prev.row = 0; + if (prev.col == null) prev.col = 0; + run = prev; } } @@ -730,6 +728,33 @@ test "unicode placement: continuation with no col" { try testing.expect(it.next() == null); } +test "unicode placement: continuation with no diacritics" { + const alloc = testing.allocator; + var t = try terminal.Terminal.init(alloc, .{ .rows = 5, .cols = 10 }); + defer t.deinit(alloc); + t.modes.set(.grapheme_cluster, true); + + // Three cells. They'll continue even though they're explicit + try t.printString("\u{10EEEE}"); + try t.printString("\u{10EEEE}"); + try t.printString("\u{10EEEE}"); + + // Get our top left pin + const pin = t.screen.pages.getTopLeft(.viewport); + + // Should have exactly one placement + var it = placementIterator(pin, null); + { + const p = it.next().?; + try testing.expectEqual(0, p.image_id); + try testing.expectEqual(0, p.placement_id); + try testing.expectEqual(0, p.row); + try testing.expectEqual(0, p.col); + try testing.expectEqual(3, p.width); + } + try testing.expect(it.next() == null); +} + test "unicode placement: run ending" { const alloc = testing.allocator; var t = try terminal.Terminal.init(alloc, .{ .rows = 5, .cols = 10 }); From d6d95209c6d5234d74b9adc760e0e1ef76674cd1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 25 Jul 2024 21:35:38 -0700 Subject: [PATCH 19/36] renderer/metal: extract out some image placement logic --- src/renderer/Metal.zig | 235 ++++++++++++++++++++++++----------------- 1 file changed, 136 insertions(+), 99 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 07c5956ec..c4a22beed 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -124,6 +124,7 @@ images: ImageMap = .{}, image_placements: ImagePlacementList = .{}, image_bg_end: u32 = 0, image_text_end: u32 = 0, +image_virtual: bool = false, /// Metal state shaders: Shaders, // Compiled shaders @@ -927,7 +928,14 @@ pub fn updateFrame( // If we have Kitty graphics data, we enter a SLOW SLOW SLOW path. // We only do this if the Kitty image state is dirty meaning only if // it changes. - if (state.terminal.screen.kitty_images.dirty) { + // + // If we have any virtual references, we must also rebuild our + // kitty state on every frame because any cell change can move + // an image. + // TODO(mitchellh): integrate with row dirty flags + if (state.terminal.screen.kitty_images.dirty or + self.image_virtual) + { try self.prepKittyGraphics(state.terminal); } @@ -1565,6 +1573,7 @@ fn prepKittyGraphics( // We always clear our previous placements no matter what because // we rebuild them from scratch. self.image_placements.clearRetainingCapacity(); + self.image_virtual = false; // Go through our known images and if there are any that are no longer // in use then mark them to be freed. @@ -1588,8 +1597,25 @@ fn prepKittyGraphics( // Go through the placements and ensure the image is loaded on the GPU. var it = storage.placements.iterator(); while (it.next()) |kv| { - // Find the image in storage const p = kv.value_ptr; + + // Special logic based on location + switch (p.location) { + .pin => {}, + .virtual => { + // We need to mark virtual placements on our renderer so that + // we know to rebuild in more scenarios since cell changes can + // now trigger placement changes. + self.image_virtual = true; + + // We also continue out because virtual placements are + // only triggered by the unicode placeholder, not by the + // placement itself. + continue; + }, + } + + // Get the image for the placement const image = storage.imageById(kv.key_ptr.image_id) orelse { log.warn( "missing image for placement, ignoring image_id={}", @@ -1598,103 +1624,7 @@ fn prepKittyGraphics( continue; }; - // Get the rect for the placement. If this placement doesn't have - // a rect then its virtual or something so skip it. - const rect = p.rect(image, t) orelse continue; - - // If the selection isn't within our viewport then skip it. - if (bot.before(rect.top_left)) continue; - if (rect.bottom_right.before(top)) continue; - - // If the top left is outside the viewport we need to calc an offset - // so that we render (0, 0) with some offset for the texture. - const offset_y: u32 = if (rect.top_left.before(top)) offset_y: { - const vp_y = t.screen.pages.pointFromPin(.screen, top).?.screen.y; - const img_y = t.screen.pages.pointFromPin(.screen, rect.top_left).?.screen.y; - const offset_cells = vp_y - img_y; - const offset_pixels = offset_cells * self.grid_metrics.cell_height; - break :offset_y @intCast(offset_pixels); - } else 0; - - // We need to prep this image for upload if it isn't in the cache OR - // it is in the cache but the transmit time doesn't match meaning this - // image is different. - const gop = try self.images.getOrPut(self.alloc, kv.key_ptr.image_id); - if (!gop.found_existing or - gop.value_ptr.transmit_time.order(image.transmit_time) != .eq) - { - // Copy the data into the pending state. - const data = try self.alloc.dupe(u8, image.data); - errdefer self.alloc.free(data); - - // Store it in the map - const pending: Image.Pending = .{ - .width = image.width, - .height = image.height, - .data = data.ptr, - }; - - const new_image: Image = switch (image.format) { - .grey_alpha => .{ .pending_grey_alpha = pending }, - .rgb => .{ .pending_rgb = pending }, - .rgba => .{ .pending_rgba = pending }, - .png => unreachable, // should be decoded by now - }; - - if (!gop.found_existing) { - gop.value_ptr.* = .{ - .image = new_image, - .transmit_time = undefined, - }; - } else { - try gop.value_ptr.image.markForReplace( - self.alloc, - new_image, - ); - } - - gop.value_ptr.transmit_time = image.transmit_time; - } - - // Convert our screen point to a viewport point - const viewport: terminal.point.Point = t.screen.pages.pointFromPin( - .viewport, - rect.top_left, - ) orelse .{ .viewport = .{} }; - - // Calculate the source rectangle - const source_x = @min(image.width, p.source_x); - const source_y = @min(image.height, p.source_y + offset_y); - const source_width = if (p.source_width > 0) - @min(image.width - source_x, p.source_width) - else - image.width; - const source_height = if (p.source_height > 0) - @min(image.height, p.source_height) - else - image.height -| source_y; - - // Calculate the width/height of our image. - const dest_width = if (p.columns > 0) p.columns * self.grid_metrics.cell_width else source_width; - const dest_height = if (p.rows > 0) p.rows * self.grid_metrics.cell_height else source_height; - - // Accumulate the placement - if (image.width > 0 and image.height > 0) { - try self.image_placements.append(self.alloc, .{ - .image_id = kv.key_ptr.image_id, - .x = @intCast(rect.top_left.x), - .y = @intCast(viewport.viewport.y), - .z = p.z, - .width = dest_width, - .height = dest_height, - .cell_offset_x = p.x_offset, - .cell_offset_y = p.y_offset, - .source_x = source_x, - .source_y = source_y, - .source_width = source_width, - .source_height = source_height, - }); - } + try self.prepKittyPlacement(t, &top, &bot, &image, p); } // Sort the placements by their Z value. @@ -1731,6 +1661,113 @@ fn prepKittyGraphics( } } +fn prepKittyPlacement( + self: *Metal, + t: *terminal.Terminal, + top: *const terminal.Pin, + bot: *const terminal.Pin, + image: *const terminal.kitty.graphics.Image, + p: *const terminal.kitty.graphics.ImageStorage.Placement, +) !void { + // Get the rect for the placement. If this placement doesn't have + // a rect then its virtual or something so skip it. + const rect = p.rect(image.*, t) orelse return; + + // If the selection isn't within our viewport then skip it. + if (bot.before(rect.top_left)) return; + if (rect.bottom_right.before(top.*)) return; + + // If the top left is outside the viewport we need to calc an offset + // so that we render (0, 0) with some offset for the texture. + const offset_y: u32 = if (rect.top_left.before(top.*)) offset_y: { + const vp_y = t.screen.pages.pointFromPin(.screen, top.*).?.screen.y; + const img_y = t.screen.pages.pointFromPin(.screen, rect.top_left).?.screen.y; + const offset_cells = vp_y - img_y; + const offset_pixels = offset_cells * self.grid_metrics.cell_height; + break :offset_y @intCast(offset_pixels); + } else 0; + + // We need to prep this image for upload if it isn't in the cache OR + // it is in the cache but the transmit time doesn't match meaning this + // image is different. + const gop = try self.images.getOrPut(self.alloc, image.id); + if (!gop.found_existing or + gop.value_ptr.transmit_time.order(image.transmit_time) != .eq) + { + // Copy the data into the pending state. + const data = try self.alloc.dupe(u8, image.data); + errdefer self.alloc.free(data); + + // Store it in the map + const pending: Image.Pending = .{ + .width = image.width, + .height = image.height, + .data = data.ptr, + }; + + const new_image: Image = switch (image.format) { + .grey_alpha => .{ .pending_grey_alpha = pending }, + .rgb => .{ .pending_rgb = pending }, + .rgba => .{ .pending_rgba = pending }, + .png => unreachable, // should be decoded by now + }; + + if (!gop.found_existing) { + gop.value_ptr.* = .{ + .image = new_image, + .transmit_time = undefined, + }; + } else { + try gop.value_ptr.image.markForReplace( + self.alloc, + new_image, + ); + } + + gop.value_ptr.transmit_time = image.transmit_time; + } + + // Convert our screen point to a viewport point + const viewport: terminal.point.Point = t.screen.pages.pointFromPin( + .viewport, + rect.top_left, + ) orelse .{ .viewport = .{} }; + + // Calculate the source rectangle + const source_x = @min(image.width, p.source_x); + const source_y = @min(image.height, p.source_y + offset_y); + const source_width = if (p.source_width > 0) + @min(image.width - source_x, p.source_width) + else + image.width; + const source_height = if (p.source_height > 0) + @min(image.height, p.source_height) + else + image.height -| source_y; + + // Calculate the width/height of our image. + const dest_width = if (p.columns > 0) p.columns * self.grid_metrics.cell_width else source_width; + const dest_height = if (p.rows > 0) p.rows * self.grid_metrics.cell_height else source_height; + + // Accumulate the placement + if (image.width > 0 and image.height > 0) { + try self.image_placements.append(self.alloc, .{ + .image_id = image.id, + .x = @intCast(rect.top_left.x), + .y = @intCast(viewport.viewport.y), + .z = p.z, + .width = dest_width, + .height = dest_height, + .cell_offset_x = p.x_offset, + .cell_offset_y = p.y_offset, + .source_x = source_x, + .source_y = source_y, + .source_width = source_width, + .source_height = source_height, + }); + } +} + /// Update the configuration. pub fn changeConfig(self: *Metal, config: *DerivedConfig) !void { // We always redo the font shaper in case font features changed. We From 3d4dd5277e7164b9d5d3e4d3a8432af7592ace80 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 25 Jul 2024 22:08:02 -0700 Subject: [PATCH 20/36] renderer/metal: virtual placements are kind of rendering --- src/renderer/Metal.zig | 105 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index c4a22beed..248e5f853 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -1627,6 +1627,17 @@ fn prepKittyGraphics( try self.prepKittyPlacement(t, &top, &bot, &image, p); } + // If we have virtual placements then we need to scan for placeholders. + if (self.image_virtual) { + var v_it = terminal.kitty.graphics.unicode.placementIterator(top, bot); + while (v_it.next()) |virtual_p| try self.prepKittyVirtualPlacement( + t, + &top, + &bot, + &virtual_p, + ); + } + // Sort the placements by their Z value. std.mem.sortUnstable( mtl_image.Placement, @@ -1661,6 +1672,100 @@ fn prepKittyGraphics( } } +fn prepKittyVirtualPlacement( + self: *Metal, + t: *terminal.Terminal, + top: *const terminal.Pin, + bot: *const terminal.Pin, + p: *const terminal.kitty.graphics.unicode.Placement, +) !void { + const storage = &t.screen.kitty_images; + const image = storage.imageById(p.image_id) orelse { + log.warn( + "missing image for virtual placement, ignoring image_id={}", + .{p.image_id}, + ); + return; + }; + + // Get the placement. If an ID is specified we look for the exact one. + // If no ID, then we find the first virtual placement for this image. + const placement = if (p.placement_id > 0) storage.placements.get(.{ + .image_id = p.image_id, + .placement_id = .{ .tag = .external, .id = p.placement_id }, + }) orelse { + log.warn( + "missing placement for virtual placement, ignoring image_id={} placement_id={}", + .{ p.image_id, p.placement_id }, + ); + return; + } else placement: { + var it = storage.placements.iterator(); + while (it.next()) |entry| { + if (entry.key_ptr.image_id == p.image_id and + entry.value_ptr.location == .virtual) + { + break :placement entry.value_ptr.*; + } + } + + log.warn( + "missing placement for virtual placement, ignoring image_id={}", + .{p.image_id}, + ); + return; + }; + + // Calculate our grid size for the placement. If it is isn't explicitly + // provided by the placement we try to calculate it to fit in the + // grid as closely as possible. + const img_grid: renderer.GridSize = grid: { + var rows = placement.rows; + var columns = placement.columns; + + if (rows == 0) { + const cell_height = self.grid_metrics.cell_height; + rows = (image.height + cell_height - 1) / cell_height; + } + + if (columns == 0) { + const cell_width = self.grid_metrics.cell_width; + columns = (image.width + cell_width - 1) / cell_width; + } + + break :grid .{ + .rows = std.math.cast(terminal.size.CellCountInt, rows) orelse { + log.warn( + "placement rows too large for virtual placement, ignoring image_id={}", + .{p.image_id}, + ); + return; + }, + .columns = std.math.cast(terminal.size.CellCountInt, columns) orelse { + log.warn( + "placement columns too large for virtual placement, ignoring image_id={}", + .{p.image_id}, + ); + return; + }, + }; + }; + + // Build our real placement + const real_p: terminal.kitty.graphics.ImageStorage.Placement = .{ + // Note: this constCast is safe because we only ever need a mutable + // pin if we deinit the placement. We never deinit this placement + // because it is a temporary value. + .location = .{ .pin = @constCast(&p.pin) }, + .columns = p.width, + .rows = p.height, + .z = -1, // Render behind cursor + }; + + try self.prepKittyPlacement(t, top, bot, &image, &real_p); + _ = img_grid; +} + fn prepKittyPlacement( self: *Metal, t: *terminal.Terminal, From a5d39103c9e2416f1d5e9cdc9459bdfac62b449a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 26 Jul 2024 12:21:37 -0700 Subject: [PATCH 21/36] renderer/metal: calculate proper grid offsets for image --- src/renderer/Metal.zig | 105 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 93 insertions(+), 12 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 248e5f853..e359183eb 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -1751,19 +1751,100 @@ fn prepKittyVirtualPlacement( }; }; - // Build our real placement - const real_p: terminal.kitty.graphics.ImageStorage.Placement = .{ - // Note: this constCast is safe because we only ever need a mutable - // pin if we deinit the placement. We never deinit this placement - // because it is a temporary value. - .location = .{ .pin = @constCast(&p.pin) }, - .columns = p.width, - .rows = p.height, - .z = -1, // Render behind cursor - }; + // The image is fit into the placement grid size. We need to calculate + // various offsets in order to center the image vertically/horizontally + // into the grid size while preserving the aspect ratio. + var x_offset: f64 = 0; + var y_offset: f64 = 0; + var x_scale: f64 = 0; + var y_scale: f64 = 0; + const rows_px: f64 = @floatFromInt(img_grid.rows * self.grid_metrics.cell_height); + const cols_px: f64 = @floatFromInt(img_grid.columns * self.grid_metrics.cell_width); + const img_width_f64: f64 = @floatFromInt(image.width); + const img_height_f64: f64 = @floatFromInt(image.height); + if (img_width_f64 * rows_px > img_height_f64 * cols_px) { + // Image is wider than the grid, fit width and center height + x_scale = cols_px / @max(img_width_f64, 1); + y_scale = x_scale; + y_offset = (rows_px - img_height_f64 * y_scale) / 2; + } else { + // Image is taller than the grid, fit height and center width + y_scale = rows_px / @max(img_height_f64, 1); + x_scale = y_scale; + x_offset = (cols_px - img_width_f64 * x_scale) / 2; + } - try self.prepKittyPlacement(t, top, bot, &image, &real_p); - _ = img_grid; + // A bunch of math to map the placement and image to virtual placeholder + // grid. This is ported as closely as possible from Kitty so we get this + // as right as possible. I EXPECT there are some rounding bugs in here + // so compared to Kitty we may be off by 1px here or there. If someone + // can show that to be true let's modify it. + // + // This code is purposely not super Zig-like because I want to keep it + // as close to the Kitty implementation as possible os its easy to + // compare and modify. + var pin: terminal.Pin = p.pin; + var cols: u32 = p.width; + var rows: u32 = p.height; + const x_dst: f64 = @floatFromInt(p.col * self.grid_metrics.cell_width); + const y_dst: f64 = @floatFromInt(p.row * self.grid_metrics.cell_height); + const w_dst: f64 = @floatFromInt(p.width * self.grid_metrics.cell_width); + const h_dst: f64 = @floatFromInt(p.height * self.grid_metrics.cell_height); + var cell_x_off: u32 = 0; + var cell_y_off: u32 = 0; + var src_x: f64 = (x_dst - x_offset) / x_scale; + var src_y: f64 = (y_dst - y_offset) / y_scale; + var src_w: f64 = w_dst / x_scale; + var src_h: f64 = h_dst / y_scale; + if (src_x < 0) { + src_w += src_x; + cell_x_off = @intFromFloat(@round(-src_x * x_scale)); + src_x = 0; + const col_off: u32 = cell_x_off / self.grid_metrics.cell_width; + cell_x_off %= self.grid_metrics.cell_width; + pin = pin.right(col_off); + if (cols <= col_off) return; + cols -= col_off; + } + if (src_y < 0) { + src_h += src_y; + cell_y_off = @intFromFloat(@round(-src_y * y_scale)); + src_y = 0; + const row_off: u32 = cell_y_off / self.grid_metrics.cell_height; + cell_y_off %= self.grid_metrics.cell_height; + pin = pin.down(row_off) orelse return; + if (rows <= row_off) return; + rows -= row_off; + } + + if (src_x + src_w > img_width_f64) { + const redundant_px = src_x + src_w - img_width_f64; + const redundant_cells = @as(u32, @intFromFloat(redundant_px * x_scale)) / self.grid_metrics.cell_width; + if (cols <= redundant_cells) return; + cols -= redundant_cells; + src_w -= @as(f64, @floatFromInt(redundant_cells * self.grid_metrics.cell_width)) / x_scale; + } + if (src_y + src_h > img_height_f64) { + const redundant_px = src_y + src_h - img_height_f64; + const redundant_cells = @as(u32, @intFromFloat(redundant_px * y_scale)) / self.grid_metrics.cell_height; + if (rows <= redundant_cells) return; + rows -= redundant_cells; + src_h -= @as(f64, @floatFromInt(redundant_cells * self.grid_metrics.cell_height)) / y_scale; + } + + // Build our real placement + try self.prepKittyPlacement(t, top, bot, &image, &.{ + .location = .{ .pin = &pin }, + .x_offset = cell_x_off, + .y_offset = cell_y_off, + .source_x = @intFromFloat(@round(src_x)), + .source_y = @intFromFloat(@round(src_y)), + .source_width = @intFromFloat(@round(src_w)), + .source_height = @intFromFloat(@round(src_h)), + .columns = cols, + .rows = rows, + .z = -1, // Render behind cursor + }); } fn prepKittyPlacement( From 6668930b96ff0c0476b84fd641a13b4e1270c31e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 26 Jul 2024 12:24:35 -0700 Subject: [PATCH 22/36] terminal: appendGrapheme should text for codepoint, not text --- src/renderer/Metal.zig | 1 - src/terminal/page.zig | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index e359183eb..7c4e60969 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -1816,7 +1816,6 @@ fn prepKittyVirtualPlacement( if (rows <= row_off) return; rows -= row_off; } - if (src_x + src_w > img_width_f64) { const redundant_px = src_x + src_w - img_width_f64; const redundant_cells = @as(u32, @intFromFloat(redundant_px * x_scale)) / self.grid_metrics.cell_width; diff --git a/src/terminal/page.zig b/src/terminal/page.zig index 9d149270e..c270a0e0d 100644 --- a/src/terminal/page.zig +++ b/src/terminal/page.zig @@ -1152,7 +1152,7 @@ pub const Page = struct { pub fn setGraphemes(self: *Page, row: *Row, cell: *Cell, cps: []u21) Allocator.Error!void { defer self.assertIntegrity(); - assert(cell.hasText()); + assert(cell.codepoint() > 0); assert(cell.content_tag == .codepoint); const cell_offset = getOffset(Cell, self.memory, cell); From 4bf8d30b4430ab6e3b4065b0cd698390be9d47a2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 29 Jul 2024 09:35:51 -0700 Subject: [PATCH 23/36] renderer/metal: rewrite kitty placeholder handling --- src/renderer/Metal.zig | 260 ++++++++++++++++++++++++----------------- 1 file changed, 151 insertions(+), 109 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 7c4e60969..e5d1a5a77 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -1679,6 +1679,9 @@ fn prepKittyVirtualPlacement( bot: *const terminal.Pin, p: *const terminal.kitty.graphics.unicode.Placement, ) !void { + _ = top; + _ = bot; + const storage = &t.screen.kitty_images; const image = storage.imageById(p.image_id) orelse { log.warn( @@ -1716,18 +1719,23 @@ fn prepKittyVirtualPlacement( return; }; - // Calculate our grid size for the placement. If it is isn't explicitly - // provided by the placement we try to calculate it to fit in the - // grid as closely as possible. + // Calculate the grid size for the placement. For virtual placements, + // we use the requested row/cols. If either isn't specified, we choose + // the best size based on the image size to fit the entire image in its + // original size. + // + // This part of the code does NOT do preserve any aspect ratios. Its + // dumbly fitting the image into the grid size -- possibly user specified. const img_grid: renderer.GridSize = grid: { + // Use requested rows/columns if specified var rows = placement.rows; var columns = placement.columns; + // For unspecified rows/columns, calculate based on the image size. if (rows == 0) { const cell_height = self.grid_metrics.cell_height; rows = (image.height + cell_height - 1) / cell_height; } - if (columns == 0) { const cell_width = self.grid_metrics.cell_width; columns = (image.width + cell_width - 1) / cell_width; @@ -1751,11 +1759,17 @@ fn prepKittyVirtualPlacement( }; }; - // The image is fit into the placement grid size. We need to calculate - // various offsets in order to center the image vertically/horizontally - // into the grid size while preserving the aspect ratio. + // Next we have to fit the source image into the grid size while preserving + // aspect ratio. We will center the image horizontally/vertically if + // necessary. + + // The offsets are the pixel offsets from the top-left of the top-left + // grid cell in order to center the image as best as possible. var x_offset: f64 = 0; var y_offset: f64 = 0; + + // The scale factors are the scaling factors applied to the original + // image size in order to fit it into our placement grid size. var x_scale: f64 = 0; var y_scale: f64 = 0; const rows_px: f64 = @floatFromInt(img_grid.rows * self.grid_metrics.cell_height); @@ -1773,76 +1787,93 @@ fn prepKittyVirtualPlacement( x_scale = y_scale; x_offset = (cols_px - img_width_f64 * x_scale) / 2; } + log.warn("x_offset={}, y_offset={}, x_scale={}, y_scale={}", .{ + x_offset, y_offset, x_scale, y_scale, + }); - // A bunch of math to map the placement and image to virtual placeholder - // grid. This is ported as closely as possible from Kitty so we get this - // as right as possible. I EXPECT there are some rounding bugs in here - // so compared to Kitty we may be off by 1px here or there. If someone - // can show that to be true let's modify it. + // At this point, we have the following information: + // - image.width/height - The original image width and height. + // - img_grid.rows/columns - The requested grid size for the placement. + // - offset/scale - The offset and scale to fit the image into the + // placement grid. // - // This code is purposely not super Zig-like because I want to keep it - // as close to the Kitty implementation as possible os its easy to - // compare and modify. - var pin: terminal.Pin = p.pin; - var cols: u32 = p.width; - var rows: u32 = p.height; - const x_dst: f64 = @floatFromInt(p.col * self.grid_metrics.cell_width); - const y_dst: f64 = @floatFromInt(p.row * self.grid_metrics.cell_height); - const w_dst: f64 = @floatFromInt(p.width * self.grid_metrics.cell_width); - const h_dst: f64 = @floatFromInt(p.height * self.grid_metrics.cell_height); - var cell_x_off: u32 = 0; - var cell_y_off: u32 = 0; - var src_x: f64 = (x_dst - x_offset) / x_scale; - var src_y: f64 = (y_dst - y_offset) / y_scale; - var src_w: f64 = w_dst / x_scale; - var src_h: f64 = h_dst / y_scale; - if (src_x < 0) { - src_w += src_x; - cell_x_off = @intFromFloat(@round(-src_x * x_scale)); - src_x = 0; - const col_off: u32 = cell_x_off / self.grid_metrics.cell_width; - cell_x_off %= self.grid_metrics.cell_width; - pin = pin.right(col_off); - if (cols <= col_off) return; - cols -= col_off; - } - if (src_y < 0) { - src_h += src_y; - cell_y_off = @intFromFloat(@round(-src_y * y_scale)); - src_y = 0; - const row_off: u32 = cell_y_off / self.grid_metrics.cell_height; - cell_y_off %= self.grid_metrics.cell_height; - pin = pin.down(row_off) orelse return; - if (rows <= row_off) return; - rows -= row_off; - } - if (src_x + src_w > img_width_f64) { - const redundant_px = src_x + src_w - img_width_f64; - const redundant_cells = @as(u32, @intFromFloat(redundant_px * x_scale)) / self.grid_metrics.cell_width; - if (cols <= redundant_cells) return; - cols -= redundant_cells; - src_w -= @as(f64, @floatFromInt(redundant_cells * self.grid_metrics.cell_width)) / x_scale; - } - if (src_y + src_h > img_height_f64) { - const redundant_px = src_y + src_h - img_height_f64; - const redundant_cells = @as(u32, @intFromFloat(redundant_px * y_scale)) / self.grid_metrics.cell_height; - if (rows <= redundant_cells) return; - rows -= redundant_cells; - src_h -= @as(f64, @floatFromInt(redundant_cells * self.grid_metrics.cell_height)) / y_scale; + // For our run requested coordinates and size we now need to map + // the original image down into our grid cells honoring the offsets + // calculated for the best fit. + + const img_x_offset: f64 = x_offset / x_scale; + const img_y_offset: f64 = y_offset / y_scale; + const img_width_padded: f64 = img_width_f64 + (img_x_offset * 2); + const img_height_padded: f64 = img_height_f64 + (img_y_offset * 2); + log.warn("padded_width={}, padded_height={} original_width={}, original_height={}", .{ + img_width_padded, img_height_padded, img_width_f64, img_height_f64, + }); + + const source_width_f64: f64 = img_width_padded * (@as(f64, @floatFromInt(p.width)) / @as(f64, @floatFromInt(img_grid.columns))); + var source_height_f64: f64 = img_height_padded * (@as(f64, @floatFromInt(p.height)) / @as(f64, @floatFromInt(img_grid.rows))); + const source_x_f64: f64 = img_width_padded * (@as(f64, @floatFromInt(p.col)) / @as(f64, @floatFromInt(img_grid.columns))); + var source_y_f64: f64 = img_height_padded * (@as(f64, @floatFromInt(p.row)) / @as(f64, @floatFromInt(img_grid.rows))); + + const p_x_offset_f64: f64 = 0; + var p_y_offset_f64: f64 = 0; + const dst_width_f64: f64 = @floatFromInt(p.width * self.grid_metrics.cell_width); + var dst_height_f64: f64 = @floatFromInt(p.height * self.grid_metrics.cell_height); + + // If our y is in our top offset area, we need to adjust the source to + // be shorter, and offset it into the cell. + if (source_y_f64 < img_y_offset) { + const offset: f64 = img_y_offset - source_y_f64; + source_height_f64 -= offset; + p_y_offset_f64 = offset; + dst_height_f64 -= offset * y_scale; + source_y_f64 = 0; } - // Build our real placement - try self.prepKittyPlacement(t, top, bot, &image, &.{ - .location = .{ .pin = &pin }, - .x_offset = cell_x_off, - .y_offset = cell_y_off, - .source_x = @intFromFloat(@round(src_x)), - .source_y = @intFromFloat(@round(src_y)), - .source_width = @intFromFloat(@round(src_w)), - .source_height = @intFromFloat(@round(src_h)), - .columns = cols, - .rows = rows, - .z = -1, // Render behind cursor + // if our y is in our bottom offset area, we need to shorten the + // source to fit in the cell. + if (source_y_f64 + source_height_f64 > img_height_padded - img_y_offset) { + source_y_f64 -= img_y_offset; + source_height_f64 = img_height_padded - img_y_offset - source_y_f64; + source_height_f64 -= img_y_offset; + dst_height_f64 = source_height_f64 * y_scale; + } + + const source_width: u32 = @intFromFloat(@round(source_width_f64)); + const source_height: u32 = @intFromFloat(@round(source_height_f64)); + const source_x: u32 = @intFromFloat(@round(source_x_f64)); + const source_y: u32 = @intFromFloat(@round(source_y_f64)); + const p_x_offset: u32 = @intFromFloat(@round(p_x_offset_f64 * x_scale)); + const p_y_offset: u32 = @intFromFloat(@round(p_y_offset_f64 * y_scale)); + const dest_width: u32 = @intFromFloat(@round(dst_width_f64)); + const dest_height: u32 = @intFromFloat(@round(dst_height_f64)); + + log.warn("source_x={}, source_y={}, source_width={}, source_height={}", .{ + source_x, source_y, source_width, source_height, + }); + log.warn("p_x_offset={}, p_y_offset={}", .{ p_x_offset, p_y_offset }); + log.warn("dest_width={}, dest_height={}", .{ dest_width, dest_height }); + + // Send our image to the GPU + try self.prepKittyImage(&image); + + const viewport: terminal.point.Point = t.screen.pages.pointFromPin( + .viewport, + p.pin, + ) orelse @panic("TODO: unreachable?"); + + try self.image_placements.append(self.alloc, .{ + .image_id = image.id, + .x = @intCast(p.pin.x), + .y = @intCast(viewport.viewport.y), + .z = -1, + .width = dest_width, + .height = dest_height, + .cell_offset_x = p_x_offset, + .cell_offset_y = p_y_offset, + .source_x = source_x, + .source_y = source_y, + .source_width = source_width, + .source_height = source_height, }); } @@ -1875,42 +1906,7 @@ fn prepKittyPlacement( // We need to prep this image for upload if it isn't in the cache OR // it is in the cache but the transmit time doesn't match meaning this // image is different. - const gop = try self.images.getOrPut(self.alloc, image.id); - if (!gop.found_existing or - gop.value_ptr.transmit_time.order(image.transmit_time) != .eq) - { - // Copy the data into the pending state. - const data = try self.alloc.dupe(u8, image.data); - errdefer self.alloc.free(data); - - // Store it in the map - const pending: Image.Pending = .{ - .width = image.width, - .height = image.height, - .data = data.ptr, - }; - - const new_image: Image = switch (image.format) { - .grey_alpha => .{ .pending_grey_alpha = pending }, - .rgb => .{ .pending_rgb = pending }, - .rgba => .{ .pending_rgba = pending }, - .png => unreachable, // should be decoded by now - }; - - if (!gop.found_existing) { - gop.value_ptr.* = .{ - .image = new_image, - .transmit_time = undefined, - }; - } else { - try gop.value_ptr.image.markForReplace( - self.alloc, - new_image, - ); - } - - gop.value_ptr.transmit_time = image.transmit_time; - } + try self.prepKittyImage(image); // Convert our screen point to a viewport point const viewport: terminal.point.Point = t.screen.pages.pointFromPin( @@ -1953,6 +1949,52 @@ fn prepKittyPlacement( } } +fn prepKittyImage( + self: *Metal, + image: *const terminal.kitty.graphics.Image, +) !void { + // If this image exists and its transmit time is the same we assume + // it is the identical image so we don't need to send it to the GPU. + const gop = try self.images.getOrPut(self.alloc, image.id); + if (gop.found_existing and + gop.value_ptr.transmit_time.order(image.transmit_time) == .eq) + { + return; + } + + // Copy the data into the pending state. + const data = try self.alloc.dupe(u8, image.data); + errdefer self.alloc.free(data); + + // Store it in the map + const pending: Image.Pending = .{ + .width = image.width, + .height = image.height, + .data = data.ptr, + }; + + const new_image: Image = switch (image.format) { + .grey_alpha => .{ .pending_grey_alpha = pending }, + .rgb => .{ .pending_rgb = pending }, + .rgba => .{ .pending_rgba = pending }, + .png => unreachable, // should be decoded by now + }; + + if (!gop.found_existing) { + gop.value_ptr.* = .{ + .image = new_image, + .transmit_time = undefined, + }; + } else { + try gop.value_ptr.image.markForReplace( + self.alloc, + new_image, + ); + } + + gop.value_ptr.transmit_time = image.transmit_time; +} + /// Update the configuration. pub fn changeConfig(self: *Metal, config: *DerivedConfig) !void { // We always redo the font shaper in case font features changed. We From 0ebf14fd44a829c7edd7791cb92b3c029b0df127 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 29 Jul 2024 10:39:18 -0700 Subject: [PATCH 24/36] terminal/kitty: working on moving placement math here for testing --- src/renderer/Metal.zig | 44 ++++- src/terminal/kitty/graphics.zig | 2 + src/terminal/kitty/graphics_unicode.zig | 227 ++++++++++++++++++++++++ 3 files changed, 266 insertions(+), 7 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index e5d1a5a77..e22faaf4c 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -1632,8 +1632,6 @@ fn prepKittyGraphics( var v_it = terminal.kitty.graphics.unicode.placementIterator(top, bot); while (v_it.next()) |virtual_p| try self.prepKittyVirtualPlacement( t, - &top, - &bot, &virtual_p, ); } @@ -1675,13 +1673,8 @@ fn prepKittyGraphics( fn prepKittyVirtualPlacement( self: *Metal, t: *terminal.Terminal, - top: *const terminal.Pin, - bot: *const terminal.Pin, p: *const terminal.kitty.graphics.unicode.Placement, ) !void { - _ = top; - _ = bot; - const storage = &t.screen.kitty_images; const image = storage.imageById(p.image_id) orelse { log.warn( @@ -1691,6 +1684,43 @@ fn prepKittyVirtualPlacement( return; }; + if (true) { + const rp = p.renderPlacement( + &t.screen.kitty_images, + &image, + self.grid_metrics.cell_width, + self.grid_metrics.cell_height, + ) catch |err| { + log.warn("error rendering virtual placement err={}", .{err}); + return; + }; + + // Send our image to the GPU + try self.prepKittyImage(&image); + + const viewport: terminal.point.Point = t.screen.pages.pointFromPin( + .viewport, + rp.top_left, + ) orelse @panic("TODO: unreachable?"); + + try self.image_placements.append(self.alloc, .{ + .image_id = image.id, + .x = @intCast(rp.top_left.x), + .y = @intCast(viewport.viewport.y), + .z = -1, + .width = rp.dest_width, + .height = rp.dest_height, + .cell_offset_x = rp.offset_x, + .cell_offset_y = rp.offset_y, + .source_x = rp.source_x, + .source_y = rp.source_y, + .source_width = rp.source_width, + .source_height = rp.source_height, + }); + + return; + } + // Get the placement. If an ID is specified we look for the exact one. // If no ID, then we find the first virtual placement for this image. const placement = if (p.placement_id > 0) storage.placements.get(.{ diff --git a/src/terminal/kitty/graphics.zig b/src/terminal/kitty/graphics.zig index 22d102f53..0beb4901e 100644 --- a/src/terminal/kitty/graphics.zig +++ b/src/terminal/kitty/graphics.zig @@ -16,11 +16,13 @@ //! aim to ship a v1 of this implementation came at some cost. I learned a lot //! though and I think we can go back through and fix this up. +const render = @import("graphics_render.zig"); pub usingnamespace @import("graphics_command.zig"); pub usingnamespace @import("graphics_exec.zig"); pub usingnamespace @import("graphics_image.zig"); pub usingnamespace @import("graphics_storage.zig"); pub const unicode = @import("graphics_unicode.zig"); +pub const RenderPlacement = render.Placement; test { @import("std").testing.refAllDecls(@This()); diff --git a/src/terminal/kitty/graphics_unicode.zig b/src/terminal/kitty/graphics_unicode.zig index a908ac200..23371d180 100644 --- a/src/terminal/kitty/graphics_unicode.zig +++ b/src/terminal/kitty/graphics_unicode.zig @@ -5,6 +5,8 @@ const std = @import("std"); const assert = std.debug.assert; const testing = std.testing; const terminal = @import("../main.zig"); +const kitty_gfx = terminal.kitty.graphics; +const RenderPlacement = kitty_gfx.RenderPlacement; const log = std.log.scoped(.kitty_gfx); @@ -117,6 +119,231 @@ pub const Placement = struct { /// The width/height in cells of this placement. width: u32, height: u32, + + pub const Error = error{ + PlacementGridOutOfBounds, + PlacementMissingPlacement, + }; + + /// Take this virtual placement and convert it to a render placement. + pub fn renderPlacement( + self: *const Placement, + storage: *const kitty_gfx.ImageStorage, + img: *const kitty_gfx.Image, + cell_width: u32, + cell_height: u32, + ) Error!RenderPlacement { + // In this function, there is a variable naming convention to try + // to make it slightly less confusing. The prefix will tell you what + // coordinate/size space a variable lives in: + // - img_* is for the original image + // - p_* is for the final placement + // - vp_* is for the virtual placement + + // Determine the grid size that this virtual placement fits into. + const p_grid = try self.grid(storage, img, cell_width, cell_height); + + // From here on out we switch to floating point math. These are + // constants that we'll reference repeatedly. + const img_width_f64: f64 = @floatFromInt(img.width); + const img_height_f64: f64 = @floatFromInt(img.height); + + // Next we have to fit the source image into the grid size while preserving + // aspect ratio. We will center the image horizontally/vertically if + // necessary. + const p_scale: struct { + /// The offsets are pixels from the top-left of the placement-sized + /// image in order to center the image as necessary. + x_offset: f64 = 0, + y_offset: f64 = 0, + + /// The multipliers to apply to the width/height of the original + /// image size in order to reach the placement size. + x_scale: f64 = 0, + y_scale: f64 = 0, + } = scale: { + const p_rows_px: f64 = @floatFromInt(p_grid.rows * cell_height); + const p_cols_px: f64 = @floatFromInt(p_grid.columns * cell_width); + if (img_width_f64 * p_rows_px > img_height_f64 * p_cols_px) { + // Image is wider than the grid, fit width and center height + const x_scale = p_cols_px / @max(img_width_f64, 1); + const y_scale = x_scale; + const y_offset = (p_rows_px - img_height_f64 * y_scale) / 2; + break :scale .{ + .x_scale = x_scale, + .y_scale = y_scale, + .y_offset = y_offset, + }; + } else { + // Image is taller than the grid, fit height and center width + const y_scale = p_rows_px / @max(img_height_f64, 1); + const x_scale = y_scale; + const x_offset = (p_cols_px - img_width_f64 * x_scale) / 2; + break :scale .{ + .x_scale = x_scale, + .y_scale = y_scale, + .x_offset = x_offset, + }; + } + }; + + // Scale our original image according to the aspect ratio + // and padding calculated for p_scale. + const img_scaled: struct { + x_offset: f64, + y_offset: f64, + width: f64, + height: f64, + } = scale: { + const x_offset: f64 = p_scale.x_offset / p_scale.x_scale; + const y_offset: f64 = p_scale.y_offset / p_scale.y_scale; + const width: f64 = img_width_f64 + (x_offset * 2); + const height: f64 = img_height_f64 + (y_offset * 2); + break :scale .{ + .x_offset = x_offset, + .y_offset = y_offset, + .width = width, + .height = height, + }; + }; + + // Calculate the source rectangle for the scaled image. These + // coordinates are in the scaled image space. + var img_scale_source: struct { + x: f64, + y: f64, + width: f64, + height: f64, + } = source: { + // Float-converted values we already have + const vp_width: f64 = @floatFromInt(self.width); + const vp_height: f64 = @floatFromInt(self.height); + const vp_col: f64 = @floatFromInt(self.col); + const vp_row: f64 = @floatFromInt(self.row); + const p_grid_cols: f64 = @floatFromInt(p_grid.columns); + const p_grid_rows: f64 = @floatFromInt(p_grid.rows); + + // Calculate the scaled source rectangle for the image, undoing + // the aspect ratio scaling as necessary. + const width: f64 = img_scaled.width * (vp_width / p_grid_cols); + const height: f64 = img_scaled.height * (vp_height / p_grid_rows); + const x: f64 = img_scaled.width * (vp_col / p_grid_cols); + const y: f64 = img_scaled.height * (vp_row / p_grid_rows); + + break :source .{ + .width = width, + .height = height, + .x = x, + .y = y, + }; + }; + + // The destination rectangle. The x/y is specified by offsets from + // the top-left since that's how our RenderPlacement works. + const p_dest: struct { + x_offset: f64, + y_offset: f64, + width: f64, + height: f64, + } = dest: { + const x_offset: f64 = 0; + var y_offset: f64 = 0; + const width: f64 = @floatFromInt(self.width * cell_width); + var height: f64 = @floatFromInt(self.height * cell_height); + + if (img_scale_source.y < img_scaled.y_offset) { + // If our source rect y is within the offset area, we need to + // adjust our source rect and destination since the source texture + // doesnt actually have the offset area blank. + const offset: f64 = img_scaled.y_offset - img_scale_source.y; + img_scale_source.height -= offset; + y_offset = offset; + height -= offset * p_scale.y_scale; + img_scale_source.y = 0; + } + + if (img_scale_source.y + img_scale_source.height > + img_scaled.height - img_scaled.y_offset) + { + // if our y is in our bottom offset area, we need to shorten the + // source to fit in the cell. + img_scale_source.y -= img_scaled.y_offset; + img_scale_source.height = img_scaled.height - img_scaled.y_offset - img_scale_source.y; + img_scale_source.height -= img_scaled.y_offset; + height = img_scale_source.height * p_scale.y_scale; + } + + break :dest .{ + .x_offset = x_offset, + .y_offset = y_offset, + .width = width, + .height = height, + }; + }; + + return .{ + .top_left = self.pin, + .offset_x = @intFromFloat(@round(p_dest.x_offset)), + .offset_y = @intFromFloat(@round(p_dest.y_offset)), + .source_x = @intFromFloat(@round(img_scale_source.x)), + .source_y = @intFromFloat(@round(img_scale_source.y)), + .source_width = @intFromFloat(@round(img_scale_source.width)), + .source_height = @intFromFloat(@round(img_scale_source.height)), + .dest_width = @intFromFloat(@round(p_dest.width)), + .dest_height = @intFromFloat(@round(p_dest.height)), + }; + } + + // Calculate the grid size for the placement. For virtual placements, + // we use the requested row/cols. If either isn't specified, we choose + // the best size based on the image size to fit the entire image in its + // original size. + // + // This part of the code does NOT do preserve any aspect ratios. Its + // dumbly fitting the image into the grid size -- possibly user specified. + fn grid( + self: *const Placement, + storage: *const kitty_gfx.ImageStorage, + image: *const kitty_gfx.Image, + cell_width: u32, + cell_height: u32, + ) !struct { + rows: u32, + columns: u32, + } { + // Get the placement. If an ID is specified we look for the exact one. + // If no ID, then we find the first virtual placement for this image. + const placement = if (self.placement_id > 0) storage.placements.get(.{ + .image_id = self.image_id, + .placement_id = .{ .tag = .external, .id = self.placement_id }, + }) orelse { + return Error.PlacementMissingPlacement; + } else placement: { + var it = storage.placements.iterator(); + while (it.next()) |entry| { + if (entry.key_ptr.image_id == self.image_id and + entry.value_ptr.location == .virtual) + { + break :placement entry.value_ptr.*; + } + } + + return Error.PlacementMissingPlacement; + }; + + // Use requested rows/columns if specified + // For unspecified rows/columns, calculate based on the image size. + var rows = placement.rows; + var columns = placement.columns; + if (rows == 0) rows = (image.height + cell_height - 1) / cell_height; + if (columns == 0) columns = (image.width + cell_width - 1) / cell_width; + return .{ + .rows = std.math.cast(terminal.size.CellCountInt, rows) orelse + return Error.PlacementGridOutOfBounds, + .columns = std.math.cast(terminal.size.CellCountInt, columns) orelse + return Error.PlacementGridOutOfBounds, + }; + } }; /// IncompletePlacement is the placement information present in a single From 359458b96a208683b64806710dd34d82e1ab7408 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 29 Jul 2024 10:52:10 -0700 Subject: [PATCH 25/36] terminal/kitty: switch to new placement math --- src/renderer/Metal.zig | 223 ++---------------------- src/terminal/kitty/graphics_render.zig | 28 +++ src/terminal/kitty/graphics_unicode.zig | 11 +- 3 files changed, 54 insertions(+), 208 deletions(-) create mode 100644 src/terminal/kitty/graphics_render.zig diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index e22faaf4c..06d5346b0 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -1684,226 +1684,37 @@ fn prepKittyVirtualPlacement( return; }; - if (true) { - const rp = p.renderPlacement( - &t.screen.kitty_images, - &image, - self.grid_metrics.cell_width, - self.grid_metrics.cell_height, - ) catch |err| { - log.warn("error rendering virtual placement err={}", .{err}); - return; - }; - - // Send our image to the GPU - try self.prepKittyImage(&image); - - const viewport: terminal.point.Point = t.screen.pages.pointFromPin( - .viewport, - rp.top_left, - ) orelse @panic("TODO: unreachable?"); - - try self.image_placements.append(self.alloc, .{ - .image_id = image.id, - .x = @intCast(rp.top_left.x), - .y = @intCast(viewport.viewport.y), - .z = -1, - .width = rp.dest_width, - .height = rp.dest_height, - .cell_offset_x = rp.offset_x, - .cell_offset_y = rp.offset_y, - .source_x = rp.source_x, - .source_y = rp.source_y, - .source_width = rp.source_width, - .source_height = rp.source_height, - }); - - return; - } - - // Get the placement. If an ID is specified we look for the exact one. - // If no ID, then we find the first virtual placement for this image. - const placement = if (p.placement_id > 0) storage.placements.get(.{ - .image_id = p.image_id, - .placement_id = .{ .tag = .external, .id = p.placement_id }, - }) orelse { - log.warn( - "missing placement for virtual placement, ignoring image_id={} placement_id={}", - .{ p.image_id, p.placement_id }, - ); - return; - } else placement: { - var it = storage.placements.iterator(); - while (it.next()) |entry| { - if (entry.key_ptr.image_id == p.image_id and - entry.value_ptr.location == .virtual) - { - break :placement entry.value_ptr.*; - } - } - - log.warn( - "missing placement for virtual placement, ignoring image_id={}", - .{p.image_id}, - ); + const rp = p.renderPlacement( + &t.screen.kitty_images, + &image, + self.grid_metrics.cell_width, + self.grid_metrics.cell_height, + ) catch |err| { + log.warn("error rendering virtual placement err={}", .{err}); return; }; - // Calculate the grid size for the placement. For virtual placements, - // we use the requested row/cols. If either isn't specified, we choose - // the best size based on the image size to fit the entire image in its - // original size. - // - // This part of the code does NOT do preserve any aspect ratios. Its - // dumbly fitting the image into the grid size -- possibly user specified. - const img_grid: renderer.GridSize = grid: { - // Use requested rows/columns if specified - var rows = placement.rows; - var columns = placement.columns; - - // For unspecified rows/columns, calculate based on the image size. - if (rows == 0) { - const cell_height = self.grid_metrics.cell_height; - rows = (image.height + cell_height - 1) / cell_height; - } - if (columns == 0) { - const cell_width = self.grid_metrics.cell_width; - columns = (image.width + cell_width - 1) / cell_width; - } - - break :grid .{ - .rows = std.math.cast(terminal.size.CellCountInt, rows) orelse { - log.warn( - "placement rows too large for virtual placement, ignoring image_id={}", - .{p.image_id}, - ); - return; - }, - .columns = std.math.cast(terminal.size.CellCountInt, columns) orelse { - log.warn( - "placement columns too large for virtual placement, ignoring image_id={}", - .{p.image_id}, - ); - return; - }, - }; - }; - - // Next we have to fit the source image into the grid size while preserving - // aspect ratio. We will center the image horizontally/vertically if - // necessary. - - // The offsets are the pixel offsets from the top-left of the top-left - // grid cell in order to center the image as best as possible. - var x_offset: f64 = 0; - var y_offset: f64 = 0; - - // The scale factors are the scaling factors applied to the original - // image size in order to fit it into our placement grid size. - var x_scale: f64 = 0; - var y_scale: f64 = 0; - const rows_px: f64 = @floatFromInt(img_grid.rows * self.grid_metrics.cell_height); - const cols_px: f64 = @floatFromInt(img_grid.columns * self.grid_metrics.cell_width); - const img_width_f64: f64 = @floatFromInt(image.width); - const img_height_f64: f64 = @floatFromInt(image.height); - if (img_width_f64 * rows_px > img_height_f64 * cols_px) { - // Image is wider than the grid, fit width and center height - x_scale = cols_px / @max(img_width_f64, 1); - y_scale = x_scale; - y_offset = (rows_px - img_height_f64 * y_scale) / 2; - } else { - // Image is taller than the grid, fit height and center width - y_scale = rows_px / @max(img_height_f64, 1); - x_scale = y_scale; - x_offset = (cols_px - img_width_f64 * x_scale) / 2; - } - log.warn("x_offset={}, y_offset={}, x_scale={}, y_scale={}", .{ - x_offset, y_offset, x_scale, y_scale, - }); - - // At this point, we have the following information: - // - image.width/height - The original image width and height. - // - img_grid.rows/columns - The requested grid size for the placement. - // - offset/scale - The offset and scale to fit the image into the - // placement grid. - // - // For our run requested coordinates and size we now need to map - // the original image down into our grid cells honoring the offsets - // calculated for the best fit. - - const img_x_offset: f64 = x_offset / x_scale; - const img_y_offset: f64 = y_offset / y_scale; - const img_width_padded: f64 = img_width_f64 + (img_x_offset * 2); - const img_height_padded: f64 = img_height_f64 + (img_y_offset * 2); - log.warn("padded_width={}, padded_height={} original_width={}, original_height={}", .{ - img_width_padded, img_height_padded, img_width_f64, img_height_f64, - }); - - const source_width_f64: f64 = img_width_padded * (@as(f64, @floatFromInt(p.width)) / @as(f64, @floatFromInt(img_grid.columns))); - var source_height_f64: f64 = img_height_padded * (@as(f64, @floatFromInt(p.height)) / @as(f64, @floatFromInt(img_grid.rows))); - const source_x_f64: f64 = img_width_padded * (@as(f64, @floatFromInt(p.col)) / @as(f64, @floatFromInt(img_grid.columns))); - var source_y_f64: f64 = img_height_padded * (@as(f64, @floatFromInt(p.row)) / @as(f64, @floatFromInt(img_grid.rows))); - - const p_x_offset_f64: f64 = 0; - var p_y_offset_f64: f64 = 0; - const dst_width_f64: f64 = @floatFromInt(p.width * self.grid_metrics.cell_width); - var dst_height_f64: f64 = @floatFromInt(p.height * self.grid_metrics.cell_height); - - // If our y is in our top offset area, we need to adjust the source to - // be shorter, and offset it into the cell. - if (source_y_f64 < img_y_offset) { - const offset: f64 = img_y_offset - source_y_f64; - source_height_f64 -= offset; - p_y_offset_f64 = offset; - dst_height_f64 -= offset * y_scale; - source_y_f64 = 0; - } - - // if our y is in our bottom offset area, we need to shorten the - // source to fit in the cell. - if (source_y_f64 + source_height_f64 > img_height_padded - img_y_offset) { - source_y_f64 -= img_y_offset; - source_height_f64 = img_height_padded - img_y_offset - source_y_f64; - source_height_f64 -= img_y_offset; - dst_height_f64 = source_height_f64 * y_scale; - } - - const source_width: u32 = @intFromFloat(@round(source_width_f64)); - const source_height: u32 = @intFromFloat(@round(source_height_f64)); - const source_x: u32 = @intFromFloat(@round(source_x_f64)); - const source_y: u32 = @intFromFloat(@round(source_y_f64)); - const p_x_offset: u32 = @intFromFloat(@round(p_x_offset_f64 * x_scale)); - const p_y_offset: u32 = @intFromFloat(@round(p_y_offset_f64 * y_scale)); - const dest_width: u32 = @intFromFloat(@round(dst_width_f64)); - const dest_height: u32 = @intFromFloat(@round(dst_height_f64)); - - log.warn("source_x={}, source_y={}, source_width={}, source_height={}", .{ - source_x, source_y, source_width, source_height, - }); - log.warn("p_x_offset={}, p_y_offset={}", .{ p_x_offset, p_y_offset }); - log.warn("dest_width={}, dest_height={}", .{ dest_width, dest_height }); - // Send our image to the GPU try self.prepKittyImage(&image); const viewport: terminal.point.Point = t.screen.pages.pointFromPin( .viewport, - p.pin, + rp.top_left, ) orelse @panic("TODO: unreachable?"); try self.image_placements.append(self.alloc, .{ .image_id = image.id, - .x = @intCast(p.pin.x), + .x = @intCast(rp.top_left.x), .y = @intCast(viewport.viewport.y), .z = -1, - .width = dest_width, - .height = dest_height, - .cell_offset_x = p_x_offset, - .cell_offset_y = p_y_offset, - .source_x = source_x, - .source_y = source_y, - .source_width = source_width, - .source_height = source_height, + .width = rp.dest_width, + .height = rp.dest_height, + .cell_offset_x = rp.offset_x, + .cell_offset_y = rp.offset_y, + .source_x = rp.source_x, + .source_y = rp.source_y, + .source_width = rp.source_width, + .source_height = rp.source_height, }); } diff --git a/src/terminal/kitty/graphics_render.zig b/src/terminal/kitty/graphics_render.zig new file mode 100644 index 000000000..af888582f --- /dev/null +++ b/src/terminal/kitty/graphics_render.zig @@ -0,0 +1,28 @@ +const std = @import("std"); +const assert = std.debug.assert; +const testing = std.testing; +const terminal = @import("../main.zig"); + +/// A render placement is a way to position a Kitty graphics image onto +/// the screen. It is broken down into the fields that make it easier to +/// position the image using a renderer. +pub const Placement = struct { + /// The top-left corner of the image in grid coordinates. + top_left: terminal.Pin, + + /// The offset in pixels from the top-left corner of the grid cell. + offset_x: u32 = 0, + offset_y: u32 = 0, + + /// The source rectangle of the image to render. This doesn't have to + /// match the size the destination size and the renderer is expected + /// to scale the image to fit the destination size. + source_x: u32 = 0, + source_y: u32 = 0, + source_width: u32 = 0, + source_height: u32 = 0, + + /// The final width/height of the image in pixels. + dest_width: u32 = 0, + dest_height: u32 = 0, +}; diff --git a/src/terminal/kitty/graphics_unicode.zig b/src/terminal/kitty/graphics_unicode.zig index 23371d180..b6ed5b55a 100644 --- a/src/terminal/kitty/graphics_unicode.zig +++ b/src/terminal/kitty/graphics_unicode.zig @@ -274,12 +274,19 @@ pub const Placement = struct { } break :dest .{ - .x_offset = x_offset, - .y_offset = y_offset, + .x_offset = x_offset * p_scale.x_scale, + .y_offset = y_offset * p_scale.y_scale, .width = width, .height = height, }; }; + // log.warn("p_grid={} p_scale={} img_scaled={} img_scale_source={} p_dest={}", .{ + // p_grid, + // p_scale, + // img_scaled, + // img_scale_source, + // p_dest, + // }); return .{ .top_left = self.pin, From 079420730a57763f9dd0c32fb1e2ca79fc15dd52 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 29 Jul 2024 10:55:50 -0700 Subject: [PATCH 26/36] renderer/metal: address some todos --- src/renderer/Metal.zig | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 06d5346b0..47112384a 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -1685,7 +1685,7 @@ fn prepKittyVirtualPlacement( }; const rp = p.renderPlacement( - &t.screen.kitty_images, + storage, &image, self.grid_metrics.cell_width, self.grid_metrics.cell_height, @@ -1694,14 +1694,19 @@ fn prepKittyVirtualPlacement( return; }; - // Send our image to the GPU - try self.prepKittyImage(&image); - const viewport: terminal.point.Point = t.screen.pages.pointFromPin( .viewport, rp.top_left, - ) orelse @panic("TODO: unreachable?"); + ) orelse { + // This is unreachable with virtual placements because we should + // only ever be looking at virtual placements that are in our + // viewport in the renderer and virtual placements only ever take + // up one row. + unreachable; + }; + // Send our image to the GPU and store the placement for rendering. + try self.prepKittyImage(&image); try self.image_placements.append(self.alloc, .{ .image_id = image.id, .x = @intCast(rp.top_left.x), From 5e9b87102803a88b57c42f4e443d4254ab95a7e3 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 29 Jul 2024 14:15:51 -0700 Subject: [PATCH 27/36] terminal/kitty: unit tests for unicode placements --- src/terminal/kitty/graphics_unicode.zig | 77 ++++++++++++++++++++++-- src/terminal/kitty/testdata/dog.png | Bin 0 -> 237387 bytes 2 files changed, 73 insertions(+), 4 deletions(-) create mode 100644 src/terminal/kitty/testdata/dog.png diff --git a/src/terminal/kitty/graphics_unicode.zig b/src/terminal/kitty/graphics_unicode.zig index b6ed5b55a..9d306e2b3 100644 --- a/src/terminal/kitty/graphics_unicode.zig +++ b/src/terminal/kitty/graphics_unicode.zig @@ -6,6 +6,8 @@ const assert = std.debug.assert; const testing = std.testing; const terminal = @import("../main.zig"); const kitty_gfx = terminal.kitty.graphics; +const Image = kitty_gfx.Image; +const ImageStorage = kitty_gfx.ImageStorage; const RenderPlacement = kitty_gfx.RenderPlacement; const log = std.log.scoped(.kitty_gfx); @@ -128,8 +130,8 @@ pub const Placement = struct { /// Take this virtual placement and convert it to a render placement. pub fn renderPlacement( self: *const Placement, - storage: *const kitty_gfx.ImageStorage, - img: *const kitty_gfx.Image, + storage: *const ImageStorage, + img: *const Image, cell_width: u32, cell_height: u32, ) Error!RenderPlacement { @@ -310,8 +312,8 @@ pub const Placement = struct { // dumbly fitting the image into the grid size -- possibly user specified. fn grid( self: *const Placement, - storage: *const kitty_gfx.ImageStorage, - image: *const kitty_gfx.Image, + storage: *const ImageStorage, + image: *const Image, cell_width: u32, cell_height: u32, ) !struct { @@ -1118,3 +1120,70 @@ test "unicode placement: specifying placement id as palette" { } try testing.expect(it.next() == null); } + +// Fish: +// printf "\033_Gf=100,i=1,t=f,q=2;$(printf dog.png | base64)\033\\" +// printf "\e[38;5;1m\U10EEEE\U0305\U0305\U10EEEE\U0305\U030D\U10EEEE\U0305\U030E\U10EEEE\U0305\U0310\e[39m\n" +// printf "\e[38;5;1m\U10EEEE\U030D\U0305\U10EEEE\U030D\U030D\U10EEEE\U030D\U030E\U10EEEE\U030D\U0310\e[39m\n" +// printf "\033_Ga=p,i=1,U=1,q=2,c=4,r=2\033\\" +test "unicode render placement: dog 4x2" { + const alloc = testing.allocator; + const cell_width = 36; + const cell_height = 80; + + var t = try terminal.Terminal.init(alloc, .{ .cols = 100, .rows = 100 }); + defer t.deinit(alloc); + var s: ImageStorage = .{}; + defer s.deinit(alloc, &t.screen); + + const image: Image = .{ .id = 1, .width = 500, .height = 306 }; + try s.addImage(alloc, image); + try s.addPlacement(alloc, 1, 0, .{ + .location = .{ .virtual = {} }, + .columns = 4, + .rows = 2, + }); + + // Row 1 + { + const p: Placement = .{ + .pin = t.screen.cursor.page_pin.*, + .image_id = 1, + .placement_id = 0, + .col = 0, + .row = 0, + .width = 4, + .height = 1, + }; + const rp = try p.renderPlacement(&s, &image, cell_width, cell_height); + try testing.expectEqual(0, rp.offset_x); + try testing.expectEqual(36, rp.offset_y); + try testing.expectEqual(0, rp.source_x); + try testing.expectEqual(0, rp.source_y); + try testing.expectEqual(500, rp.source_width); + try testing.expectEqual(153, rp.source_height); + try testing.expectEqual(144, rp.dest_width); + try testing.expectEqual(44, rp.dest_height); + } + // Row 2 + { + const p: Placement = .{ + .pin = t.screen.cursor.page_pin.*, + .image_id = 1, + .placement_id = 0, + .col = 0, + .row = 1, + .width = 4, + .height = 1, + }; + const rp = try p.renderPlacement(&s, &image, cell_width, cell_height); + try testing.expectEqual(0, rp.offset_x); + try testing.expectEqual(0, rp.offset_y); + try testing.expectEqual(0, rp.source_x); + try testing.expectEqual(153, rp.source_y); + try testing.expectEqual(500, rp.source_width); + try testing.expectEqual(153, rp.source_height); + try testing.expectEqual(144, rp.dest_width); + try testing.expectEqual(44, rp.dest_height); + } +} diff --git a/src/terminal/kitty/testdata/dog.png b/src/terminal/kitty/testdata/dog.png new file mode 100644 index 0000000000000000000000000000000000000000..a5749c232030b3f73c9708eb3065d5ffcbcfacd4 GIT binary patch literal 237387 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�F0K) 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 OX^e=i= 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 jTD8n`_J`i#@UQ Date: Mon, 29 Jul 2024 14:36:49 -0700 Subject: [PATCH 28/36] terminal: hasText no longer special cases kitty placeholders --- src/font/shaper/run.zig | 20 +++++++++++++++++++- src/terminal/page.zig | 15 ++------------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/font/shaper/run.zig b/src/font/shaper/run.zig index ef55ba981..8d53c601b 100644 --- a/src/font/shaper/run.zig +++ b/src/font/shaper/run.zig @@ -228,6 +228,12 @@ pub const RunIterator = struct { 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, @@ -284,8 +290,20 @@ pub const RunIterator = struct { 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.grid.getIndex( + alloc, + ' ', + style, + presentation, + ); + } + // Get the font index for the primary codepoint. - const primary_cp: u32 = if (cell.isEmpty() or cell.codepoint() == 0) ' ' else cell.codepoint(); + const primary_cp: u32 = cell.codepoint(); const primary = try self.grid.getIndex( alloc, primary_cp, diff --git a/src/terminal/page.zig b/src/terminal/page.zig index c270a0e0d..b396493fe 100644 --- a/src/terminal/page.zig +++ b/src/terminal/page.zig @@ -1705,8 +1705,7 @@ pub const Cell = packed struct(u64) { return switch (self.content_tag) { .codepoint, .codepoint_grapheme, - => self.content.codepoint != 0 and - self.content.codepoint != kitty.graphics.unicode.placeholder, + => self.content.codepoint != 0, .bg_color_palette, .bg_color_rgb, @@ -1738,8 +1737,7 @@ pub const Cell = packed struct(u64) { return self.style_id != style.default_id; } - /// Returns true if the cell has no text or styling. This also returns - /// true if the cell represents a Kitty graphics unicode placeholder. + /// Returns true if the cell has no text or styling. pub fn isEmpty(self: Cell) bool { return switch (self.content_tag) { // Textual cells are empty if they have no text and are narrow. @@ -2671,12 +2669,3 @@ test "Page verifyIntegrity zero cols" { page.verifyIntegrity(testing.allocator), ); } - -test "Cell isEmpty for kitty placeholder" { - var c: Cell = .{ - .content_tag = .codepoint_grapheme, - .content = .{ .codepoint = kitty.graphics.unicode.placeholder }, - }; - try testing.expectEqual(@as(u21, kitty.graphics.unicode.placeholder), c.codepoint()); - try testing.expect(c.isEmpty()); -} From 39b915ac2502f6576b837668f0c3434a3021a162 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 29 Jul 2024 14:42:27 -0700 Subject: [PATCH 29/36] terminal/kitty: handle width-stretched --- src/terminal/kitty/graphics_unicode.zig | 95 +++++++++++++++++++++++-- 1 file changed, 90 insertions(+), 5 deletions(-) diff --git a/src/terminal/kitty/graphics_unicode.zig b/src/terminal/kitty/graphics_unicode.zig index 9d306e2b3..31e8fce55 100644 --- a/src/terminal/kitty/graphics_unicode.zig +++ b/src/terminal/kitty/graphics_unicode.zig @@ -248,9 +248,9 @@ pub const Placement = struct { width: f64, height: f64, } = dest: { - const x_offset: f64 = 0; + var x_offset: f64 = 0; var y_offset: f64 = 0; - const width: f64 = @floatFromInt(self.width * cell_width); + var width: f64 = @floatFromInt(self.width * cell_width); var height: f64 = @floatFromInt(self.height * cell_height); if (img_scale_source.y < img_scaled.y_offset) { @@ -262,9 +262,7 @@ pub const Placement = struct { y_offset = offset; height -= offset * p_scale.y_scale; img_scale_source.y = 0; - } - - if (img_scale_source.y + img_scale_source.height > + } else if (img_scale_source.y + img_scale_source.height > img_scaled.height - img_scaled.y_offset) { // if our y is in our bottom offset area, we need to shorten the @@ -275,6 +273,26 @@ pub const Placement = struct { height = img_scale_source.height * p_scale.y_scale; } + if (img_scale_source.x < img_scaled.x_offset) { + // If our source rect x is within the offset area, we need to + // adjust our source rect and destination since the source texture + // doesnt actually have the offset area blank. + const offset: f64 = img_scaled.x_offset - img_scale_source.x; + img_scale_source.width -= offset; + x_offset = offset; + width -= offset * p_scale.x_scale; + img_scale_source.x = 0; + } else if (img_scale_source.x + img_scale_source.width > + img_scaled.width - img_scaled.x_offset) + { + // if our x is in our right offset area, we need to shorten the + // source to fit in the cell. + img_scale_source.x -= img_scaled.x_offset; + img_scale_source.width = img_scaled.width - img_scaled.x_offset - img_scale_source.x; + img_scale_source.width -= img_scaled.x_offset; + width = img_scale_source.width * p_scale.x_scale; + } + break :dest .{ .x_offset = x_offset * p_scale.x_scale, .y_offset = y_offset * p_scale.y_scale, @@ -1187,3 +1205,70 @@ test "unicode render placement: dog 4x2" { try testing.expectEqual(44, rp.dest_height); } } + +// Fish: +// printf "\033_Gf=100,i=1,t=f,q=2;$(printf dog.png | base64)\033\\" +// printf "\e[38;5;1m\U10EEEE\U0305\U0305\U10EEEE\U0305\U030D\U10EEEE\U0305\U030E\U10EEEE\U0305\U0310\e[39m\n" +// printf "\e[38;5;1m\U10EEEE\U030D\U0305\U10EEEE\U030D\U030D\U10EEEE\U030D\U030E\U10EEEE\U030D\U0310\e[39m\n" +// printf "\033_Ga=p,i=1,U=1,q=2,c=2,r=2\033\\" +test "unicode render placement: dog 2x2 with blank cells" { + const alloc = testing.allocator; + const cell_width = 36; + const cell_height = 80; + + var t = try terminal.Terminal.init(alloc, .{ .cols = 100, .rows = 100 }); + defer t.deinit(alloc); + var s: ImageStorage = .{}; + defer s.deinit(alloc, &t.screen); + + const image: Image = .{ .id = 1, .width = 500, .height = 306 }; + try s.addImage(alloc, image); + try s.addPlacement(alloc, 1, 0, .{ + .location = .{ .virtual = {} }, + .columns = 2, + .rows = 2, + }); + + // Row 1 + { + const p: Placement = .{ + .pin = t.screen.cursor.page_pin.*, + .image_id = 1, + .placement_id = 0, + .col = 0, + .row = 0, + .width = 4, + .height = 1, + }; + const rp = try p.renderPlacement(&s, &image, cell_width, cell_height); + try testing.expectEqual(0, rp.offset_x); + try testing.expectEqual(58, rp.offset_y); + try testing.expectEqual(0, rp.source_x); + try testing.expectEqual(0, rp.source_y); + try testing.expectEqual(500, rp.source_width); + try testing.expectEqual(153, rp.source_height); + try testing.expectEqual(72, rp.dest_width); + try testing.expectEqual(22, rp.dest_height); + } + // Row 2 + { + const p: Placement = .{ + .pin = t.screen.cursor.page_pin.*, + .image_id = 1, + .placement_id = 0, + .col = 0, + .row = 1, + .width = 4, + .height = 1, + }; + const rp = try p.renderPlacement(&s, &image, cell_width, cell_height); + try testing.expectEqual(0, rp.offset_x); + try testing.expectEqual(0, rp.offset_y); + try testing.expectEqual(0, rp.source_x); + try testing.expectEqual(153, rp.source_y); + try testing.expectEqual(500, rp.source_width); + try testing.expectEqual(153, rp.source_height); + try testing.expectEqual(72, rp.dest_width); + try testing.expectEqual(22, rp.dest_height); + } +} From 0c81ca44b8a4f0ca20f7093d94805c16ceabff31 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 29 Jul 2024 14:52:09 -0700 Subject: [PATCH 30/36] terminal/kitty: do not render blank virtual placement cells --- src/renderer/Metal.zig | 3 +++ src/terminal/kitty/graphics_unicode.zig | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 47112384a..77b92040c 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -1694,6 +1694,9 @@ fn prepKittyVirtualPlacement( return; }; + // If our placement is zero sized then we don't do anything. + if (rp.dest_width == 0 or rp.dest_height == 0) return; + const viewport: terminal.point.Point = t.screen.pages.pointFromPin( .viewport, rp.top_left, diff --git a/src/terminal/kitty/graphics_unicode.zig b/src/terminal/kitty/graphics_unicode.zig index 31e8fce55..2fc34b67e 100644 --- a/src/terminal/kitty/graphics_unicode.zig +++ b/src/terminal/kitty/graphics_unicode.zig @@ -293,6 +293,13 @@ pub const Placement = struct { width = img_scale_source.width * p_scale.x_scale; } + // If our modified source width/height is less than zero then + // we render nothing because it means we're rendering outside + // of the visible image. + if (img_scale_source.width <= 0 or img_scale_source.height <= 0) { + return .{ .top_left = self.pin }; + } + break :dest .{ .x_offset = x_offset * p_scale.x_scale, .y_offset = y_offset * p_scale.y_scale, From d510bb449775f6365916e8de9cd6b05e29207311 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 29 Jul 2024 17:26:57 -0700 Subject: [PATCH 31/36] terminal/kitty: adjust middle rows for image offsets --- src/terminal/kitty/graphics_unicode.zig | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/terminal/kitty/graphics_unicode.zig b/src/terminal/kitty/graphics_unicode.zig index 2fc34b67e..b8b76407b 100644 --- a/src/terminal/kitty/graphics_unicode.zig +++ b/src/terminal/kitty/graphics_unicode.zig @@ -271,6 +271,8 @@ pub const Placement = struct { img_scale_source.height = img_scaled.height - img_scaled.y_offset - img_scale_source.y; img_scale_source.height -= img_scaled.y_offset; height = img_scale_source.height * p_scale.y_scale; + } else { + img_scale_source.y -= img_scaled.y_offset; } if (img_scale_source.x < img_scaled.x_offset) { @@ -291,6 +293,8 @@ pub const Placement = struct { img_scale_source.width = img_scaled.width - img_scaled.x_offset - img_scale_source.x; img_scale_source.width -= img_scaled.x_offset; width = img_scale_source.width * p_scale.x_scale; + } else { + img_scale_source.x -= img_scaled.x_offset; } // If our modified source width/height is less than zero then From bf3075365770d387ce5496eb7ceb258271791038 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 29 Jul 2024 18:57:31 -0700 Subject: [PATCH 32/36] terminal/kitty: handle case where both offsets are in one grid cell --- src/terminal/kitty/graphics_unicode.zig | 65 ++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/src/terminal/kitty/graphics_unicode.zig b/src/terminal/kitty/graphics_unicode.zig index b8b76407b..339ad6202 100644 --- a/src/terminal/kitty/graphics_unicode.zig +++ b/src/terminal/kitty/graphics_unicode.zig @@ -262,6 +262,14 @@ pub const Placement = struct { y_offset = offset; height -= offset * p_scale.y_scale; img_scale_source.y = 0; + + // If our height is greater than our original height, + // bring it back down. This addresses the case where the top + // and bottom offsets are both used. + if (img_scale_source.height > img_height_f64) { + img_scale_source.height = img_height_f64; + height = img_height_f64 * p_scale.y_scale; + } } else if (img_scale_source.y + img_scale_source.height > img_scaled.height - img_scaled.y_offset) { @@ -284,6 +292,14 @@ pub const Placement = struct { x_offset = offset; width -= offset * p_scale.x_scale; img_scale_source.x = 0; + + // If our width is greater than our original width, + // bring it back down. This addresses the case where the left + // and right offsets are both used. + if (img_scale_source.width > img_width_f64) { + img_scale_source.width = img_width_f64; + width = img_width_f64 * p_scale.x_scale; + } } else if (img_scale_source.x + img_scale_source.width > img_scaled.width - img_scaled.x_offset) { @@ -311,7 +327,9 @@ pub const Placement = struct { .height = height, }; }; - // log.warn("p_grid={} p_scale={} img_scaled={} img_scale_source={} p_dest={}", .{ + // log.warn("img_width={} img_height={}\np_grid={}\np_scale={}\nimg_scaled={}\nimg_scale_source={}\np_dest={}\n", .{ + // img_width_f64, + // img_height_f64, // p_grid, // p_scale, // img_scaled, @@ -1283,3 +1301,48 @@ test "unicode render placement: dog 2x2 with blank cells" { try testing.expectEqual(22, rp.dest_height); } } + +// Fish: +// printf "\033_Gf=100,i=1,t=f,q=2;$(printf dog.png | base64)\033\\" +// printf "\e[38;5;1m\U10EEEE\U0305\U0305\U10EEEE\U0305\U030D\U10EEEE\U0305\U030E\U10EEEE\U0305\U0310\e[39m\n" +// printf "\033_Ga=p,i=1,U=1,q=2,c=1,r=1\033\\" +test "unicode render placement: dog 1x1" { + const alloc = testing.allocator; + const cell_width = 36; + const cell_height = 80; + + var t = try terminal.Terminal.init(alloc, .{ .cols = 100, .rows = 100 }); + defer t.deinit(alloc); + var s: ImageStorage = .{}; + defer s.deinit(alloc, &t.screen); + + const image: Image = .{ .id = 1, .width = 500, .height = 306 }; + try s.addImage(alloc, image); + try s.addPlacement(alloc, 1, 0, .{ + .location = .{ .virtual = {} }, + .columns = 1, + .rows = 1, + }); + + // Row 1 + { + const p: Placement = .{ + .pin = t.screen.cursor.page_pin.*, + .image_id = 1, + .placement_id = 0, + .col = 0, + .row = 0, + .width = 4, + .height = 1, + }; + const rp = try p.renderPlacement(&s, &image, cell_width, cell_height); + try testing.expectEqual(0, rp.offset_x); + try testing.expectEqual(29, rp.offset_y); + try testing.expectEqual(0, rp.source_x); + try testing.expectEqual(0, rp.source_y); + try testing.expectEqual(500, rp.source_width); + try testing.expectEqual(306, rp.source_height); + try testing.expectEqual(36, rp.dest_width); + try testing.expectEqual(22, rp.dest_height); + } +} From 765254e78467b5207be609116d4876dc31a72d44 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 29 Jul 2024 19:15:42 -0700 Subject: [PATCH 33/36] renderer/opengl: unicode placeholder support --- src/renderer/Metal.zig | 1 - src/renderer/OpenGL.zig | 307 +++++++++++++++++++++++++++------------- 2 files changed, 210 insertions(+), 98 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 77b92040c..3e4cac699 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -932,7 +932,6 @@ pub fn updateFrame( // If we have any virtual references, we must also rebuild our // kitty state on every frame because any cell change can move // an image. - // TODO(mitchellh): integrate with row dirty flags if (state.terminal.screen.kitty_images.dirty or self.image_virtual) { diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index fd9261874..2645ebc26 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -120,6 +120,7 @@ images: ImageMap = .{}, image_placements: ImagePlacementList = .{}, image_bg_end: u32 = 0, image_text_end: u32 = 0, +image_virtual: bool = false, /// Defererred OpenGL operation to update the screen size. const SetScreenSize = struct { @@ -693,7 +694,13 @@ pub fn updateFrame( // If we have Kitty graphics data, we enter a SLOW SLOW SLOW path. // We only do this if the Kitty image state is dirty meaning only if // it changes. - if (state.terminal.screen.kitty_images.dirty) { + // + // If we have any virtual references, we must also rebuild our + // kitty state on every frame because any cell change can move + // an image. + if (state.terminal.screen.kitty_images.dirty or + self.image_virtual) + { // prepKittyGraphics touches self.images which is also used // in drawFrame so if we're drawing on a separate thread we need // to lock this. @@ -752,6 +759,7 @@ fn prepKittyGraphics( // We always clear our previous placements no matter what because // we rebuild them from scratch. self.image_placements.clearRetainingCapacity(); + self.image_virtual = false; // Go through our known images and if there are any that are no longer // in use then mark them to be freed. @@ -777,6 +785,23 @@ fn prepKittyGraphics( while (it.next()) |kv| { // Find the image in storage const p = kv.value_ptr; + + // Special logic based on location + switch (p.location) { + .pin => {}, + .virtual => { + // We need to mark virtual placements on our renderer so that + // we know to rebuild in more scenarios since cell changes can + // now trigger placement changes. + self.image_virtual = true; + + // We also continue out because virtual placements are + // only triggered by the unicode placeholder, not by the + // placement itself. + continue; + }, + } + const image = storage.imageById(kv.key_ptr.image_id) orelse { log.warn( "missing image for placement, ignoring image_id={}", @@ -785,103 +810,16 @@ fn prepKittyGraphics( continue; }; - // Get the rect for the placement. If this placement doesn't have - // a rect then its virtual or something so skip it. - const rect = p.rect(image, t) orelse continue; + try self.prepKittyPlacement(t, &top, &bot, &image, p); + } - // If the selection isn't within our viewport then skip it. - if (bot.before(rect.top_left)) continue; - if (rect.bottom_right.before(top)) continue; - - // If the top left is outside the viewport we need to calc an offset - // so that we render (0, 0) with some offset for the texture. - const offset_y: u32 = if (rect.top_left.before(top)) offset_y: { - const vp_y = t.screen.pages.pointFromPin(.screen, top).?.screen.y; - const img_y = t.screen.pages.pointFromPin(.screen, rect.top_left).?.screen.y; - const offset_cells = vp_y - img_y; - const offset_pixels = offset_cells * self.grid_metrics.cell_height; - break :offset_y @intCast(offset_pixels); - } else 0; - - // We need to prep this image for upload if it isn't in the cache OR - // it is in the cache but the transmit time doesn't match meaning this - // image is different. - const gop = try self.images.getOrPut(self.alloc, kv.key_ptr.image_id); - if (!gop.found_existing or - gop.value_ptr.transmit_time.order(image.transmit_time) != .eq) - { - // Copy the data into the pending state. - const data = try self.alloc.dupe(u8, image.data); - errdefer self.alloc.free(data); - - // Store it in the map - const pending: Image.Pending = .{ - .width = image.width, - .height = image.height, - .data = data.ptr, - }; - - const new_image: Image = switch (image.format) { - .grey_alpha => .{ .pending_grey_alpha = pending }, - .rgb => .{ .pending_rgb = pending }, - .rgba => .{ .pending_rgba = pending }, - .png => unreachable, // should be decoded by now - }; - - if (!gop.found_existing) { - gop.value_ptr.* = .{ - .image = new_image, - .transmit_time = undefined, - }; - } else { - try gop.value_ptr.image.markForReplace( - self.alloc, - new_image, - ); - } - - gop.value_ptr.transmit_time = image.transmit_time; - } - - // Convert our screen point to a viewport point - const viewport: terminal.point.Point = t.screen.pages.pointFromPin( - .viewport, - rect.top_left, - ) orelse .{ .viewport = .{} }; - - // Calculate the source rectangle - const source_x = @min(image.width, p.source_x); - const source_y = @min(image.height, p.source_y + offset_y); - const source_width = if (p.source_width > 0) - @min(image.width - source_x, p.source_width) - else - image.width; - const source_height = if (p.source_height > 0) - @min(image.height, p.source_height) - else - image.height -| source_y; - - // Calculate the width/height of our image. - const dest_width = if (p.columns > 0) p.columns * self.grid_metrics.cell_width else source_width; - const dest_height = if (p.rows > 0) p.rows * self.grid_metrics.cell_height else source_height; - - // Accumulate the placement - if (image.width > 0 and image.height > 0) { - try self.image_placements.append(self.alloc, .{ - .image_id = kv.key_ptr.image_id, - .x = @intCast(rect.top_left.x), - .y = @intCast(viewport.viewport.y), - .z = p.z, - .width = dest_width, - .height = dest_height, - .cell_offset_x = p.x_offset, - .cell_offset_y = p.y_offset, - .source_x = source_x, - .source_y = source_y, - .source_width = source_width, - .source_height = source_height, - }); - } + // If we have virtual placements then we need to scan for placeholders. + if (self.image_virtual) { + var v_it = terminal.kitty.graphics.unicode.placementIterator(top, bot); + while (v_it.next()) |virtual_p| try self.prepKittyVirtualPlacement( + t, + &virtual_p, + ); } // Sort the placements by their Z value. @@ -918,6 +856,181 @@ fn prepKittyGraphics( } } +fn prepKittyVirtualPlacement( + self: *OpenGL, + t: *terminal.Terminal, + p: *const terminal.kitty.graphics.unicode.Placement, +) !void { + const storage = &t.screen.kitty_images; + const image = storage.imageById(p.image_id) orelse { + log.warn( + "missing image for virtual placement, ignoring image_id={}", + .{p.image_id}, + ); + return; + }; + + const rp = p.renderPlacement( + storage, + &image, + self.grid_metrics.cell_width, + self.grid_metrics.cell_height, + ) catch |err| { + log.warn("error rendering virtual placement err={}", .{err}); + return; + }; + + // If our placement is zero sized then we don't do anything. + if (rp.dest_width == 0 or rp.dest_height == 0) return; + + const viewport: terminal.point.Point = t.screen.pages.pointFromPin( + .viewport, + rp.top_left, + ) orelse { + // This is unreachable with virtual placements because we should + // only ever be looking at virtual placements that are in our + // viewport in the renderer and virtual placements only ever take + // up one row. + unreachable; + }; + + // Send our image to the GPU and store the placement for rendering. + try self.prepKittyImage(&image); + try self.image_placements.append(self.alloc, .{ + .image_id = image.id, + .x = @intCast(rp.top_left.x), + .y = @intCast(viewport.viewport.y), + .z = -1, + .width = rp.dest_width, + .height = rp.dest_height, + .cell_offset_x = rp.offset_x, + .cell_offset_y = rp.offset_y, + .source_x = rp.source_x, + .source_y = rp.source_y, + .source_width = rp.source_width, + .source_height = rp.source_height, + }); +} + +fn prepKittyPlacement( + self: *OpenGL, + t: *terminal.Terminal, + top: *const terminal.Pin, + bot: *const terminal.Pin, + image: *const terminal.kitty.graphics.Image, + p: *const terminal.kitty.graphics.ImageStorage.Placement, +) !void { + // Get the rect for the placement. If this placement doesn't have + // a rect then its virtual or something so skip it. + const rect = p.rect(image.*, t) orelse return; + + // If the selection isn't within our viewport then skip it. + if (bot.before(rect.top_left)) return; + if (rect.bottom_right.before(top.*)) return; + + // If the top left is outside the viewport we need to calc an offset + // so that we render (0, 0) with some offset for the texture. + const offset_y: u32 = if (rect.top_left.before(top.*)) offset_y: { + const vp_y = t.screen.pages.pointFromPin(.screen, top.*).?.screen.y; + const img_y = t.screen.pages.pointFromPin(.screen, rect.top_left).?.screen.y; + const offset_cells = vp_y - img_y; + const offset_pixels = offset_cells * self.grid_metrics.cell_height; + break :offset_y @intCast(offset_pixels); + } else 0; + + // We need to prep this image for upload if it isn't in the cache OR + // it is in the cache but the transmit time doesn't match meaning this + // image is different. + try self.prepKittyImage(image); + + // Convert our screen point to a viewport point + const viewport: terminal.point.Point = t.screen.pages.pointFromPin( + .viewport, + rect.top_left, + ) orelse .{ .viewport = .{} }; + + // Calculate the source rectangle + const source_x = @min(image.width, p.source_x); + const source_y = @min(image.height, p.source_y + offset_y); + const source_width = if (p.source_width > 0) + @min(image.width - source_x, p.source_width) + else + image.width; + const source_height = if (p.source_height > 0) + @min(image.height, p.source_height) + else + image.height -| source_y; + + // Calculate the width/height of our image. + const dest_width = if (p.columns > 0) p.columns * self.grid_metrics.cell_width else source_width; + const dest_height = if (p.rows > 0) p.rows * self.grid_metrics.cell_height else source_height; + + // Accumulate the placement + if (image.width > 0 and image.height > 0) { + try self.image_placements.append(self.alloc, .{ + .image_id = image.id, + .x = @intCast(rect.top_left.x), + .y = @intCast(viewport.viewport.y), + .z = p.z, + .width = dest_width, + .height = dest_height, + .cell_offset_x = p.x_offset, + .cell_offset_y = p.y_offset, + .source_x = source_x, + .source_y = source_y, + .source_width = source_width, + .source_height = source_height, + }); + } +} + +fn prepKittyImage( + self: *OpenGL, + image: *const terminal.kitty.graphics.Image, +) !void { + // We need to prep this image for upload if it isn't in the cache OR + // it is in the cache but the transmit time doesn't match meaning this + // image is different. + const gop = try self.images.getOrPut(self.alloc, image.id); + if (gop.found_existing and + gop.value_ptr.transmit_time.order(image.transmit_time) == .eq) + { + return; + } + + // Copy the data into the pending state. + const data = try self.alloc.dupe(u8, image.data); + errdefer self.alloc.free(data); + + // Store it in the map + const pending: Image.Pending = .{ + .width = image.width, + .height = image.height, + .data = data.ptr, + }; + + const new_image: Image = switch (image.format) { + .grey_alpha => .{ .pending_grey_alpha = pending }, + .rgb => .{ .pending_rgb = pending }, + .rgba => .{ .pending_rgba = pending }, + .png => unreachable, // should be decoded by now + }; + + if (!gop.found_existing) { + gop.value_ptr.* = .{ + .image = new_image, + .transmit_time = undefined, + }; + } else { + try gop.value_ptr.image.markForReplace( + self.alloc, + new_image, + ); + } + + gop.value_ptr.transmit_time = image.transmit_time; +} + /// rebuildCells rebuilds all the GPU cells from our CPU state. This is a /// slow operation but ensures that the GPU state exactly matches the CPU state. /// In steady-state operation, we use some GPU tricks to send down stale data From a1276b3cc37c01f2b0a89a769b71031cc25d0776 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 29 Jul 2024 19:37:47 -0700 Subject: [PATCH 34/36] terminal/kitty: delete all images ignores virtual placements --- src/terminal/kitty/graphics_storage.zig | 34 +++++++++++++++---------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/src/terminal/kitty/graphics_storage.zig b/src/terminal/kitty/graphics_storage.zig index 82441a54e..bf8633c88 100644 --- a/src/terminal/kitty/graphics_storage.zig +++ b/src/terminal/kitty/graphics_storage.zig @@ -218,19 +218,27 @@ pub const ImageStorage = struct { cmd: command.Delete, ) void { switch (cmd) { - // TODO: virtual placeholders must not be deleted according to spec - .all => |delete_images| if (delete_images) { - // We just reset our entire state. - self.deinit(alloc, &t.screen); - self.* = .{ - .dirty = true, - .total_limit = self.total_limit, - }; - } else { - // Delete all our placements - self.clearPlacements(&t.screen); - self.placements.deinit(alloc); - self.placements = .{}; + .all => |delete_images| { + var it = self.placements.iterator(); + while (it.next()) |entry| { + // Skip virtual placements + switch (entry.value_ptr.location) { + .pin => {}, + .virtual => continue, + } + + // Deinit the placement and remove it + const image_id = entry.key_ptr.image_id; + entry.value_ptr.deinit(&t.screen); + self.placements.removeByPtr(entry.key_ptr); + if (delete_images) self.deleteIfUnused(alloc, image_id); + } + + if (delete_images) { + var image_it = self.images.iterator(); + while (image_it.next()) |kv| self.deleteIfUnused(alloc, kv.key_ptr.*); + } + self.dirty = true; }, From 2c4ddc594ec2bdeda5c08f9deea39589d6c8367b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 31 Jul 2024 09:48:00 -0700 Subject: [PATCH 35/36] terminal: reflow tests for kitty virtual placeholder flag --- src/terminal/PageList.zig | 119 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index 403fe7e5a..64b741f2c 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -7962,3 +7962,122 @@ test "PageList resize reflow less cols to wrap a wide char" { } } } + +test "PageList resize reflow less cols copy kitty placeholder" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 4, 2, 0); + defer s.deinit(); + { + try testing.expect(s.pages.first == s.pages.last); + const page = &s.pages.first.?.data; + + // Write unicode placeholders + for (0..s.cols - 1) |x| { + const rac = page.getRowAndCell(x, 0); + rac.row.kitty_virtual_placeholder = true; + rac.cell.* = .{ + .content_tag = .codepoint, + .content = .{ .codepoint = kitty.graphics.unicode.placeholder }, + }; + } + } + + // Resize + try s.resize(.{ .cols = 2, .reflow = true }); + try testing.expectEqual(@as(usize, 2), s.cols); + try testing.expectEqual(@as(usize, 2), s.totalRows()); + + var it = s.rowIterator(.right_down, .{ .active = .{} }, null); + while (it.next()) |offset| { + for (0..s.cols - 1) |x| { + var offset_copy = offset; + offset_copy.x = @intCast(x); + const rac = offset_copy.rowAndCell(); + + const row = rac.row; + try testing.expect(row.kitty_virtual_placeholder); + } + } +} + +test "PageList resize reflow more cols clears kitty placeholder" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 4, 2, 0); + defer s.deinit(); + { + try testing.expect(s.pages.first == s.pages.last); + const page = &s.pages.first.?.data; + + // Write unicode placeholders + for (0..s.cols - 1) |x| { + const rac = page.getRowAndCell(x, 0); + rac.row.kitty_virtual_placeholder = true; + rac.cell.* = .{ + .content_tag = .codepoint, + .content = .{ .codepoint = kitty.graphics.unicode.placeholder }, + }; + } + } + + // Resize smaller then larger + try s.resize(.{ .cols = 2, .reflow = true }); + try s.resize(.{ .cols = 4, .reflow = true }); + try testing.expectEqual(@as(usize, 4), s.cols); + try testing.expectEqual(@as(usize, 2), s.totalRows()); + + var it = s.rowIterator(.right_down, .{ .active = .{} }, null); + { + const row = it.next().?; + const rac = row.rowAndCell(); + try testing.expect(rac.row.kitty_virtual_placeholder); + } + { + const row = it.next().?; + const rac = row.rowAndCell(); + try testing.expect(!rac.row.kitty_virtual_placeholder); + } + try testing.expect(it.next() == null); +} + +test "PageList resize reflow wrap moves kitty placeholder" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 4, 2, 0); + defer s.deinit(); + { + try testing.expect(s.pages.first == s.pages.last); + const page = &s.pages.first.?.data; + + // Write unicode placeholders + for (2..s.cols - 1) |x| { + const rac = page.getRowAndCell(x, 0); + rac.row.kitty_virtual_placeholder = true; + rac.cell.* = .{ + .content_tag = .codepoint, + .content = .{ .codepoint = kitty.graphics.unicode.placeholder }, + }; + } + } + + try s.resize(.{ .cols = 2, .reflow = true }); + try testing.expectEqual(@as(usize, 2), s.cols); + try testing.expectEqual(@as(usize, 2), s.totalRows()); + + var it = s.rowIterator(.right_down, .{ .active = .{} }, null); + { + const row = it.next().?; + const rac = row.rowAndCell(); + try testing.expect(!rac.row.kitty_virtual_placeholder); + } + { + const row = it.next().?; + const rac = row.rowAndCell(); + try testing.expect(rac.row.kitty_virtual_placeholder); + } + try testing.expect(it.next() == null); +} From d40101fea0e276189fb04be57ad47a9985075c08 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 31 Jul 2024 09:51:32 -0700 Subject: [PATCH 36/36] terminal: more tests --- src/terminal/Terminal.zig | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index d6b8a7376..063368960 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -3552,6 +3552,24 @@ test "Terminal: print invoke charset single" { } } +test "Terminal: print kitty unicode placeholder" { + var t = try init(testing.allocator, .{ .cols = 10, .rows = 10 }); + defer t.deinit(testing.allocator); + + try t.print(kitty.graphics.unicode.placeholder); + try testing.expectEqual(@as(usize, 0), t.screen.cursor.y); + try testing.expectEqual(@as(usize, 1), t.screen.cursor.x); + + { + const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?; + const cell = list_cell.cell; + try testing.expectEqual(@as(u21, kitty.graphics.unicode.placeholder), cell.content.codepoint); + try testing.expect(list_cell.row.kitty_virtual_placeholder); + } + + try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); +} + test "Terminal: soft wrap" { var t = try init(testing.allocator, .{ .cols = 3, .rows = 80 }); defer t.deinit(testing.allocator);