diff --git a/include/ghostty/vt/kitty_graphics.h b/include/ghostty/vt/kitty_graphics.h index 04d1daf27..062c8a237 100644 --- a/include/ghostty/vt/kitty_graphics.h +++ b/include/ghostty/vt/kitty_graphics.h @@ -214,6 +214,18 @@ typedef enum GHOSTTY_ENUM_TYPED { * Output type: int32_t * */ GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_Z = 12, + + /** + * All placement metadata as a sized struct. + * + * This is an optimization over querying each field individually, + * particularly useful in environments with high per-call overhead + * such as FFI or Cgo. + * + * Output type: GhosttyKittyGraphicsPlacementInfo * + * (initialize with GHOSTTY_INIT_SIZED) + */ + GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_INFO = 13, GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, } GhosttyKittyGraphicsPlacementData; @@ -341,9 +353,141 @@ typedef enum GHOSTTY_ENUM_TYPED { * Output type: size_t * */ GHOSTTY_KITTY_IMAGE_DATA_DATA_LEN = 8, + + /** + * All image metadata as a sized struct. + * + * This is an optimization over querying each field individually, + * particularly useful in environments with high per-call overhead + * such as FFI or Cgo. + * + * Output type: GhosttyKittyGraphicsImageInfo * + * (initialize with GHOSTTY_INIT_SIZED) + */ + GHOSTTY_KITTY_IMAGE_DATA_INFO = 9, GHOSTTY_KITTY_IMAGE_DATA_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, } GhosttyKittyGraphicsImageData; +/** + * All image metadata in a single sized struct. + * + * Returned by ghostty_kitty_graphics_image_get() with + * GHOSTTY_KITTY_IMAGE_DATA_INFO. This is an optimization over + * querying each field individually, particularly useful in + * environments with high per-call overhead such as FFI or Cgo. + * + * This struct uses the sized-struct ABI pattern. Initialize with + * GHOSTTY_INIT_SIZED(GhosttyKittyGraphicsImageInfo). + * + * @ingroup kitty_graphics + */ +typedef struct { + /** Size of this struct in bytes. Must be set to sizeof(GhosttyKittyGraphicsImageInfo). */ + size_t size; + /** The image ID. */ + uint32_t id; + /** The image number. */ + uint32_t number; + /** Image width in pixels. */ + uint32_t width; + /** Image height in pixels. */ + uint32_t height; + /** Pixel format of the image. */ + GhosttyKittyImageFormat format; + /** Compression of the image. */ + GhosttyKittyImageCompression compression; + /** Borrowed pointer to the raw pixel data. */ + const uint8_t* data_ptr; + /** Length of the raw pixel data in bytes. */ + size_t data_len; +} GhosttyKittyGraphicsImageInfo; + +/** + * All placement metadata in a single sized struct. + * + * Returned by ghostty_kitty_graphics_placement_get() with + * GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_INFO. This is an optimization + * over querying each field individually, particularly useful in + * environments with high per-call overhead such as FFI or Cgo. + * + * This struct uses the sized-struct ABI pattern. Initialize with + * GHOSTTY_INIT_SIZED(GhosttyKittyGraphicsPlacementInfo). + * + * @ingroup kitty_graphics + */ +typedef struct { + /** Size of this struct in bytes. Must be set to sizeof(GhosttyKittyGraphicsPlacementInfo). */ + size_t size; + /** The image ID this placement belongs to. */ + uint32_t image_id; + /** The placement ID. */ + uint32_t placement_id; + /** Whether this is a virtual placement (unicode placeholder). */ + bool is_virtual; + /** Pixel offset from the left edge of the cell. */ + uint32_t x_offset; + /** Pixel offset from the top edge of the cell. */ + uint32_t y_offset; + /** Source rectangle x origin in pixels. */ + uint32_t source_x; + /** Source rectangle y origin in pixels. */ + uint32_t source_y; + /** Source rectangle width in pixels (0 = full image width). */ + uint32_t source_width; + /** Source rectangle height in pixels (0 = full image height). */ + uint32_t source_height; + /** Number of columns this placement occupies. */ + uint32_t columns; + /** Number of rows this placement occupies. */ + uint32_t rows; + /** Z-index for this placement. */ + int32_t z; +} GhosttyKittyGraphicsPlacementInfo; + +/** + * Combined rendering geometry for a placement in a single sized struct. + * + * Combines the results of ghostty_kitty_graphics_placement_pixel_size(), + * ghostty_kitty_graphics_placement_grid_size(), + * ghostty_kitty_graphics_placement_viewport_pos(), and + * ghostty_kitty_graphics_placement_source_rect() into one call. This is + * an optimization over calling those four functions individually, + * particularly useful in environments with high per-call overhead such + * as FFI or Cgo. + * + * This struct uses the sized-struct ABI pattern. Initialize with + * GHOSTTY_INIT_SIZED(GhosttyKittyGraphicsPlacementRenderInfo) before calling + * ghostty_kitty_graphics_placement_render_info(). + * + * @ingroup kitty_graphics + */ +typedef struct { + /** Size of this struct in bytes. Must be set to sizeof(GhosttyKittyGraphicsPlacementRenderInfo). */ + size_t size; + /** Rendered width in pixels. */ + uint32_t pixel_width; + /** Rendered height in pixels. */ + uint32_t pixel_height; + /** Number of grid columns the placement occupies. */ + uint32_t grid_cols; + /** Number of grid rows the placement occupies. */ + uint32_t grid_rows; + /** Viewport-relative column (may be negative for partially visible placements). */ + int32_t viewport_col; + /** Viewport-relative row (may be negative for partially visible placements). */ + int32_t viewport_row; + /** False when the placement is fully off-screen or virtual. */ + bool viewport_visible; + /** Resolved source rectangle x origin in pixels. */ + uint32_t source_x; + /** Resolved source rectangle y origin in pixels. */ + uint32_t source_y; + /** Resolved source rectangle width in pixels. */ + uint32_t source_width; + /** Resolved source rectangle height in pixels. */ + uint32_t source_height; +} GhosttyKittyGraphicsPlacementRenderInfo; + /** * Get data from a kitty graphics storage instance. * @@ -627,6 +771,31 @@ GHOSTTY_API GhosttyResult ghostty_kitty_graphics_placement_source_rect( uint32_t* out_width, uint32_t* out_height); +/** + * Get all rendering geometry for a placement in a single call. + * + * Combines pixel size, grid size, viewport position, and source + * rectangle into one struct. Initialize with + * GHOSTTY_INIT_SIZED(GhosttyKittyGraphicsPlacementRenderInfo). + * + * When viewport_visible is false, the placement is fully off-screen + * or is a virtual placement; viewport_col and viewport_row may + * contain meaningless values in that case. + * + * @param iterator The iterator positioned on a placement + * @param image The image handle for this placement's image + * @param terminal The terminal handle + * @param[out] out_info Pointer to receive the rendering geometry + * @return GHOSTTY_SUCCESS on success + * + * @ingroup kitty_graphics + */ +GHOSTTY_API GhosttyResult ghostty_kitty_graphics_placement_render_info( + GhosttyKittyGraphicsPlacementIterator iterator, + GhosttyKittyGraphicsImage image, + GhosttyTerminal terminal, + GhosttyKittyGraphicsPlacementRenderInfo* out_info); + /** @} */ #ifdef __cplusplus diff --git a/src/lib_vt.zig b/src/lib_vt.zig index b8b9658fb..0fce98ab3 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -248,6 +248,7 @@ comptime { @export(&c.kitty_graphics_placement_grid_size, .{ .name = "ghostty_kitty_graphics_placement_grid_size" }); @export(&c.kitty_graphics_placement_viewport_pos, .{ .name = "ghostty_kitty_graphics_placement_viewport_pos" }); @export(&c.kitty_graphics_placement_source_rect, .{ .name = "ghostty_kitty_graphics_placement_source_rect" }); + @export(&c.kitty_graphics_placement_render_info, .{ .name = "ghostty_kitty_graphics_placement_render_info" }); @export(&c.grid_ref_cell, .{ .name = "ghostty_grid_ref_cell" }); @export(&c.grid_ref_row, .{ .name = "ghostty_grid_ref_row" }); @export(&c.grid_ref_graphemes, .{ .name = "ghostty_grid_ref_graphemes" }); diff --git a/src/terminal/c/kitty_graphics.zig b/src/terminal/c/kitty_graphics.zig index a086f8e9f..97143fd1e 100644 --- a/src/terminal/c/kitty_graphics.zig +++ b/src/terminal/c/kitty_graphics.zig @@ -9,6 +9,7 @@ const Image = @import("../kitty/graphics_image.zig").Image; const grid_ref = @import("grid_ref.zig"); const selection_c = @import("selection.zig"); const terminal_c = @import("terminal.zig"); +const Terminal = @import("../Terminal.zig"); const Result = @import("result.zig").Result; /// C: GhosttyKittyGraphics @@ -75,6 +76,7 @@ pub const PlacementData = enum(c_int) { columns = 10, rows = 11, z = 12, + info = 13, pub fn OutType(comptime self: PlacementData) type { return switch (self) { @@ -91,6 +93,7 @@ pub const PlacementData = enum(c_int) { .rows, => u32, .z => i32, + .info => PlacementInfo, }; } }; @@ -177,6 +180,7 @@ pub const ImageData = enum(c_int) { compression = 6, data_ptr = 7, data_len = 8, + info = 9, pub fn OutType(comptime self: ImageData) type { return switch (self) { @@ -186,6 +190,7 @@ pub const ImageData = enum(c_int) { .compression => ImageCompression, .data_ptr => [*]const u8, .data_len => usize, + .info => ImageInfo, }; } }; @@ -234,6 +239,17 @@ fn imageGetTyped( .compression => out.* = image.compression, .data_ptr => out.* = image.data.ptr, .data_len => out.* = image.data.len, + .info => { + if (out.size < @sizeOf(ImageInfo)) return .invalid_value; + out.id = image.id; + out.number = image.number; + out.width = image.width; + out.height = image.height; + out.format = image.format; + out.compression = image.compression; + out.data_ptr = image.data.ptr; + out.data_len = image.data.len; + }, } return .success; @@ -335,20 +351,38 @@ fn placementGetTyped( const iter = iter_ orelse return .invalid_value; const entry = iter.entry orelse return .invalid_value; + const key = entry.key_ptr; + const val = entry.value_ptr; + switch (data) { .invalid => return .invalid_value, - .image_id => out.* = entry.key_ptr.image_id, - .placement_id => out.* = entry.key_ptr.placement_id.id, - .is_virtual => out.* = entry.value_ptr.location == .virtual, - .x_offset => out.* = entry.value_ptr.x_offset, - .y_offset => out.* = entry.value_ptr.y_offset, - .source_x => out.* = entry.value_ptr.source_x, - .source_y => out.* = entry.value_ptr.source_y, - .source_width => out.* = entry.value_ptr.source_width, - .source_height => out.* = entry.value_ptr.source_height, - .columns => out.* = entry.value_ptr.columns, - .rows => out.* = entry.value_ptr.rows, - .z => out.* = entry.value_ptr.z, + .image_id => out.* = key.image_id, + .placement_id => out.* = key.placement_id.id, + .is_virtual => out.* = val.location == .virtual, + .x_offset => out.* = val.x_offset, + .y_offset => out.* = val.y_offset, + .source_x => out.* = val.source_x, + .source_y => out.* = val.source_y, + .source_width => out.* = val.source_width, + .source_height => out.* = val.source_height, + .columns => out.* = val.columns, + .rows => out.* = val.rows, + .z => out.* = val.z, + .info => { + if (out.size < @sizeOf(PlacementInfo)) return .invalid_value; + out.image_id = key.image_id; + out.placement_id = key.placement_id.id; + out.is_virtual = val.location == .virtual; + out.x_offset = val.x_offset; + out.y_offset = val.y_offset; + out.source_x = val.source_x; + out.source_y = val.source_y; + out.source_width = val.source_width; + out.source_height = val.source_height; + out.columns = val.columns; + out.rows = val.rows; + out.z = val.z; + }, } return .success; @@ -435,35 +469,12 @@ pub fn placement_viewport_pos( const image = image_ orelse return .invalid_value; const iter = iter_ orelse return .invalid_value; const entry = iter.entry orelse return .invalid_value; - const pin = switch (entry.value_ptr.location) { - .pin => |p| p, - .virtual => return .no_value, - }; - const pages = &wrapper.terminal.screens.active.pages; + const vp = computeViewportPos(entry.value_ptr, image, wrapper.terminal); + if (!vp.visible) return .no_value; - // Get screen-absolute coordinates for both the pin and the - // viewport origin, then subtract to get viewport-relative - // coordinates that can be negative for partially visible - // placements above the viewport. - const pin_screen = pages.pointFromPin(.screen, pin.*) orelse return .no_value; - const vp_tl = pages.getTopLeft(.viewport); - const vp_screen = pages.pointFromPin(.screen, vp_tl) orelse return .no_value; - - const vp_row: i32 = @as(i32, @intCast(pin_screen.screen.y)) - - @as(i32, @intCast(vp_screen.screen.y)); - const vp_col: i32 = @intCast(pin_screen.screen.x); - - // Check if the placement is fully off-screen. A placement is - // invisible if its bottom edge is above the viewport or its - // top edge is at or below the viewport's last row. - const grid_size = entry.value_ptr.gridSize(image.*, wrapper.terminal); - const rows_i32: i32 = @intCast(grid_size.rows); - const term_rows: i32 = @intCast(wrapper.terminal.rows); - if (vp_row + rows_i32 <= 0 or vp_row >= term_rows) return .no_value; - - out_col.* = vp_col; - out_row.* = vp_row; + out_col.* = vp.col; + out_row.* = vp.row; return .success; } @@ -497,6 +508,144 @@ pub fn placement_source_rect( return .success; } +/// C: GhosttyKittyGraphicsImageInfo +pub const ImageInfo = extern struct { + size: usize = @sizeOf(ImageInfo), + id: u32 = 0, + number: u32 = 0, + width: u32 = 0, + height: u32 = 0, + format: ImageFormat = .rgb, + compression: ImageCompression = .none, + data_ptr: [*]const u8 = @as([*]const u8, @ptrFromInt(1)), + data_len: usize = 0, +}; + +/// C: GhosttyKittyGraphicsPlacementInfo +pub const PlacementInfo = extern struct { + size: usize = @sizeOf(PlacementInfo), + image_id: u32 = 0, + placement_id: u32 = 0, + is_virtual: bool = false, + x_offset: u32 = 0, + y_offset: u32 = 0, + source_x: u32 = 0, + source_y: u32 = 0, + source_width: u32 = 0, + source_height: u32 = 0, + columns: u32 = 0, + rows: u32 = 0, + z: i32 = 0, +}; + +/// C: GhosttyKittyGraphicsPlacementRenderInfo +pub const PlacementRenderInfo = extern struct { + size: usize = @sizeOf(PlacementRenderInfo), + pixel_width: u32 = 0, + pixel_height: u32 = 0, + grid_cols: u32 = 0, + grid_rows: u32 = 0, + viewport_col: i32 = 0, + viewport_row: i32 = 0, + viewport_visible: bool = false, + source_x: u32 = 0, + source_y: u32 = 0, + source_width: u32 = 0, + source_height: u32 = 0, +}; + +pub fn placement_render_info( + iter_: PlacementIterator, + image_: ImageHandle, + terminal_: terminal_c.Terminal, + out_: ?*PlacementRenderInfo, +) callconv(lib.calling_conv) Result { + if (comptime !build_options.kitty_graphics) return .no_value; + + const wrapper = terminal_ orelse return .invalid_value; + const image = image_ orelse return .invalid_value; + const iter = iter_ orelse return .invalid_value; + const entry = iter.entry orelse return .invalid_value; + const out = out_ orelse return .invalid_value; + if (out.size < @sizeOf(PlacementRenderInfo)) return .invalid_value; + + const p = entry.value_ptr; + + const ps = p.pixelSize(image.*, wrapper.terminal); + out.pixel_width = ps.width; + out.pixel_height = ps.height; + + const gs = p.gridSize(image.*, wrapper.terminal); + out.grid_cols = gs.cols; + out.grid_rows = gs.rows; + + const vp = computeViewportPos(p, image, wrapper.terminal); + out.viewport_col = vp.col; + out.viewport_row = vp.row; + out.viewport_visible = vp.visible; + + const x = @min(p.source_x, image.width); + const y = @min(p.source_y, image.height); + out.source_x = x; + out.source_y = y; + out.source_width = @min(if (p.source_width > 0) p.source_width else image.width, image.width - x); + out.source_height = @min(if (p.source_height > 0) p.source_height else image.height, image.height - y); + + return .success; +} + +/// Compute viewport-relative position of a placement. +/// +/// Converts the placement's internal pin to viewport-relative column +/// and row coordinates by getting screen-absolute coordinates for +/// both the pin and the viewport origin, then subtracting to get +/// viewport-relative coordinates. The row value can be negative when +/// the placement's origin has scrolled above the top of the viewport. +/// +/// A placement is considered not visible if it is a virtual (unicode +/// placeholder) placement, or if it is fully off-screen (its bottom +/// edge is above the viewport or its top edge is at or below the +/// viewport's last row). +fn computeViewportPos( + p: *const kitty_storage.ImageStorage.Placement, + image: *const Image, + t: *Terminal, +) struct { col: i32, row: i32, visible: bool } { + // Virtual placements use unicode placeholders and don't have a + // screen position — they are rendered inline by the text layout. + const pin = switch (p.location) { + .pin => |pin| pin, + .virtual => return .{ .col = 0, .row = 0, .visible = false }, + }; + + // Convert both the placement's pin and the viewport's top-left + // corner to screen-absolute coordinates so we can subtract them + // to get viewport-relative coordinates. + const pages = &t.screens.active.pages; + const pin_screen = pages.pointFromPin(.screen, pin.*) orelse + return .{ .col = 0, .row = 0, .visible = false }; + const vp_tl = pages.getTopLeft(.viewport); + const vp_screen = pages.pointFromPin(.screen, vp_tl) orelse + return .{ .col = 0, .row = 0, .visible = false }; + + // Subtracting viewport origin from the pin gives us viewport- + // relative coordinates. The row can be negative when the + // placement has partially scrolled above the viewport. + const vp_row: i32 = @as(i32, @intCast(pin_screen.screen.y)) - + @as(i32, @intCast(vp_screen.screen.y)); + const vp_col: i32 = @intCast(pin_screen.screen.x); + + // A placement is invisible if its bottom edge (row + height) + // is above the viewport, or its top edge is at or below the + // viewport's last row. + const grid_size = p.gridSize(image.*, t); + const rows_i32: i32 = @intCast(grid_size.rows); + const term_rows: i32 = @intCast(t.rows); + const visible = vp_row + rows_i32 > 0 and vp_row < term_rows; + + return .{ .col = vp_col, .row = vp_row, .visible = visible }; +} + test "placement_iterator new/free" { var iter: PlacementIterator = null; try testing.expectEqual(Result.success, placement_iterator_new( @@ -1373,3 +1522,168 @@ test "image_get on null returns invalid_value" { var id: u32 = undefined; try testing.expectEqual(Result.invalid_value, image_get(null, .id, @ptrCast(&id))); } + +test "image_get info returns all fields" { + if (comptime !build_options.kitty_graphics) return error.SkipZigTest; + + var t: terminal_c.Terminal = null; + try testing.expectEqual(Result.success, terminal_c.new( + &lib.alloc.test_allocator, + &t, + .{ .cols = 80, .rows = 24, .max_scrollback = 0 }, + )); + defer terminal_c.free(t); + try testing.expectEqual(Result.success, terminal_c.resize(t, 80, 24, 10, 20)); + + const cmd = "\x1b_Ga=T,t=d,f=24,i=1,p=1,s=1,v=2;////////\x1b\\"; + terminal_c.vt_write(t, cmd.ptr, cmd.len); + + var graphics: KittyGraphics = undefined; + try testing.expectEqual(Result.success, terminal_c.get(t, .kitty_graphics, @ptrCast(&graphics))); + const img = image_get_handle(graphics, 1); + try testing.expect(img != null); + + var info: ImageInfo = .{}; + try testing.expectEqual(Result.success, image_get(img, .info, @ptrCast(&info))); + try testing.expectEqual(1, info.id); + try testing.expectEqual(1, info.width); + try testing.expectEqual(2, info.height); + try testing.expectEqual(ImageFormat.rgb, info.format); + try testing.expectEqual(ImageCompression.none, info.compression); + try testing.expectEqual(6, info.data_len); +} + +test "image_get info null returns invalid_value" { + if (comptime !build_options.kitty_graphics) return error.SkipZigTest; + + var info: ImageInfo = .{}; + try testing.expectEqual(Result.invalid_value, image_get(null, .info, @ptrCast(&info))); +} + +test "placement_get info returns all fields" { + if (comptime !build_options.kitty_graphics) return error.SkipZigTest; + + var t: terminal_c.Terminal = null; + try testing.expectEqual(Result.success, terminal_c.new( + &lib.alloc.test_allocator, + &t, + .{ .cols = 80, .rows = 24, .max_scrollback = 0 }, + )); + defer terminal_c.free(t); + try testing.expectEqual(Result.success, terminal_c.resize(t, 80, 24, 10, 20)); + + const cmd = "\x1b_Ga=T,t=d,f=24,i=1,p=1,s=1,v=2,c=10,r=1;////////\x1b\\"; + terminal_c.vt_write(t, cmd.ptr, cmd.len); + + var graphics: KittyGraphics = undefined; + try testing.expectEqual(Result.success, terminal_c.get(t, .kitty_graphics, @ptrCast(&graphics))); + + var iter: PlacementIterator = null; + try testing.expectEqual(Result.success, placement_iterator_new(&lib.alloc.test_allocator, &iter)); + defer placement_iterator_free(iter); + try testing.expectEqual(Result.success, get(graphics, .placement_iterator, @ptrCast(&iter))); + try testing.expect(placement_iterator_next(iter)); + + var info: PlacementInfo = .{}; + try testing.expectEqual(Result.success, placement_get(iter, .info, @ptrCast(&info))); + try testing.expectEqual(1, info.image_id); + try testing.expectEqual(1, info.placement_id); + try testing.expect(!info.is_virtual); + try testing.expectEqual(10, info.columns); + try testing.expectEqual(1, info.rows); +} + +test "placement_get info null returns invalid_value" { + if (comptime !build_options.kitty_graphics) return error.SkipZigTest; + + var info: PlacementInfo = .{}; + try testing.expectEqual(Result.invalid_value, placement_get(null, .info, @ptrCast(&info))); +} + +test "placement_render_info returns all fields" { + if (comptime !build_options.kitty_graphics) return error.SkipZigTest; + + var t: terminal_c.Terminal = null; + try testing.expectEqual(Result.success, terminal_c.new( + &lib.alloc.test_allocator, + &t, + .{ .cols = 80, .rows = 24, .max_scrollback = 0 }, + )); + defer terminal_c.free(t); + try testing.expectEqual(Result.success, terminal_c.resize(t, 80, 24, 10, 20)); + + const cmd = "\x1b_Ga=T,t=d,f=24,i=1,p=1,s=1,v=2,c=10,r=1;////////\x1b\\"; + terminal_c.vt_write(t, cmd.ptr, cmd.len); + + var graphics: KittyGraphics = undefined; + try testing.expectEqual(Result.success, terminal_c.get(t, .kitty_graphics, @ptrCast(&graphics))); + const img = image_get_handle(graphics, 1); + try testing.expect(img != null); + + var iter: PlacementIterator = null; + try testing.expectEqual(Result.success, placement_iterator_new(&lib.alloc.test_allocator, &iter)); + defer placement_iterator_free(iter); + try testing.expectEqual(Result.success, get(graphics, .placement_iterator, @ptrCast(&iter))); + try testing.expect(placement_iterator_next(iter)); + + var ri: PlacementRenderInfo = .{}; + try testing.expectEqual(Result.success, placement_render_info(iter, img, t, &ri)); + try testing.expect(ri.viewport_visible); + try testing.expectEqual(0, ri.viewport_col); + try testing.expectEqual(0, ri.viewport_row); + try testing.expectEqual(10, ri.grid_cols); + try testing.expectEqual(1, ri.grid_rows); + try testing.expectEqual(0, ri.source_x); + try testing.expectEqual(0, ri.source_y); + try testing.expectEqual(1, ri.source_width); + try testing.expectEqual(2, ri.source_height); +} + +test "placement_render_info off-screen sets viewport_visible false" { + if (comptime !build_options.kitty_graphics) return error.SkipZigTest; + + var t: terminal_c.Terminal = null; + try testing.expectEqual(Result.success, terminal_c.new( + &lib.alloc.test_allocator, + &t, + .{ .cols = 80, .rows = 5, .max_scrollback = 100 }, + )); + defer terminal_c.free(t); + try testing.expectEqual(Result.success, terminal_c.resize(t, 80, 5, 10, 20)); + + const transmit = "\x1b_Ga=t,t=d,f=24,i=1,s=1,v=2;////////\x1b\\"; + const display = "\x1b_Ga=p,i=1,p=1,c=1,r=1;\x1b\\"; + terminal_c.vt_write(t, transmit.ptr, transmit.len); + terminal_c.vt_write(t, display.ptr, display.len); + + // Scroll the image completely off-screen. + const scroll = "\n\n\n\n\n\n\n\n\n\n"; + terminal_c.vt_write(t, scroll.ptr, scroll.len); + + var graphics: KittyGraphics = undefined; + try testing.expectEqual(Result.success, terminal_c.get(t, .kitty_graphics, @ptrCast(&graphics))); + const img = image_get_handle(graphics, 1); + try testing.expect(img != null); + + var iter: PlacementIterator = null; + try testing.expectEqual(Result.success, placement_iterator_new(&lib.alloc.test_allocator, &iter)); + defer placement_iterator_free(iter); + try testing.expectEqual(Result.success, get(graphics, .placement_iterator, @ptrCast(&iter))); + try testing.expect(placement_iterator_next(iter)); + + var ri: PlacementRenderInfo = .{}; + try testing.expectEqual(Result.success, placement_render_info(iter, img, t, &ri)); + try testing.expect(!ri.viewport_visible); + // Other fields should still be populated. + try testing.expectEqual(1, ri.grid_cols); + try testing.expectEqual(1, ri.grid_rows); + try testing.expectEqual(1, ri.source_width); + try testing.expectEqual(2, ri.source_height); +} + +test "placement_render_info null returns invalid_value" { + if (comptime !build_options.kitty_graphics) return error.SkipZigTest; + + var ri: PlacementRenderInfo = .{}; + try testing.expectEqual(Result.invalid_value, placement_render_info(null, null, null, &ri)); +} diff --git a/src/terminal/c/main.zig b/src/terminal/c/main.zig index 8bd98a169..e61b83e4b 100644 --- a/src/terminal/c/main.zig +++ b/src/terminal/c/main.zig @@ -22,6 +22,7 @@ pub const kitty_graphics_placement_pixel_size = kitty_graphics.placement_pixel_s pub const kitty_graphics_placement_grid_size = kitty_graphics.placement_grid_size; pub const kitty_graphics_placement_viewport_pos = kitty_graphics.placement_viewport_pos; pub const kitty_graphics_placement_source_rect = kitty_graphics.placement_source_rect; +pub const kitty_graphics_placement_render_info = kitty_graphics.placement_render_info; pub const types = @import("types.zig"); pub const modes = @import("modes.zig"); pub const osc = @import("osc.zig");