libghostty: add placement pixel_size and grid_size, rename calculatedSize

Expose Placement.pixelSize() and Placement.gridSize() as new C API
functions ghostty_kitty_graphics_placement_pixel_size() and
ghostty_kitty_graphics_placement_grid_size(). Both take the placement
iterator, image handle, and terminal, returning their results via
out params.

Rename the internal Zig method from calculatedSize to pixelSize to
pair naturally with gridSize — one returns pixels, the other grid
cells. Updated all callers including the renderer.
This commit is contained in:
Mitchell Hashimoto
2026-04-06 10:01:43 -07:00
parent 714420409b
commit 03a6eeda1d
6 changed files with 214 additions and 7 deletions

View File

@@ -364,6 +364,57 @@ GHOSTTY_API GhosttyResult ghostty_kitty_graphics_placement_rect(
GhosttyTerminal terminal,
GhosttySelection* out_selection);
/**
* Compute the rendered pixel size of the current placement.
*
* Takes into account the placement's source rectangle, specified
* columns/rows, and aspect ratio to calculate the final rendered
* pixel dimensions.
*
* @param iterator The placement iterator positioned on a placement
* @param image The image handle for this placement's image
* @param terminal The terminal handle
* @param[out] out_width On success, receives the width in pixels
* @param[out] out_height On success, receives the height in pixels
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if any handle
* is NULL or the iterator is not positioned, GHOSTTY_NO_VALUE when
* Kitty graphics are disabled
*
* @ingroup kitty_graphics
*/
GHOSTTY_API GhosttyResult ghostty_kitty_graphics_placement_pixel_size(
GhosttyKittyGraphicsPlacementIterator iterator,
GhosttyKittyGraphicsImage image,
GhosttyTerminal terminal,
uint32_t* out_width,
uint32_t* out_height);
/**
* Compute the grid cell size of the current placement.
*
* Returns the number of columns and rows that the placement occupies
* in the terminal grid. If the placement specifies explicit columns
* and rows, those are returned directly; otherwise they are calculated
* from the pixel size and cell dimensions.
*
* @param iterator The placement iterator positioned on a placement
* @param image The image handle for this placement's image
* @param terminal The terminal handle
* @param[out] out_cols On success, receives the number of columns
* @param[out] out_rows On success, receives the number of rows
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if any handle
* is NULL or the iterator is not positioned, GHOSTTY_NO_VALUE when
* Kitty graphics are disabled
*
* @ingroup kitty_graphics
*/
GHOSTTY_API GhosttyResult ghostty_kitty_graphics_placement_grid_size(
GhosttyKittyGraphicsPlacementIterator iterator,
GhosttyKittyGraphicsImage image,
GhosttyTerminal terminal,
uint32_t* out_cols,
uint32_t* out_rows);
/** @} */
#ifdef __cplusplus

View File

@@ -241,6 +241,8 @@ comptime {
@export(&c.kitty_graphics_placement_next, .{ .name = "ghostty_kitty_graphics_placement_next" });
@export(&c.kitty_graphics_placement_get, .{ .name = "ghostty_kitty_graphics_placement_get" });
@export(&c.kitty_graphics_placement_rect, .{ .name = "ghostty_kitty_graphics_placement_rect" });
@export(&c.kitty_graphics_placement_pixel_size, .{ .name = "ghostty_kitty_graphics_placement_pixel_size" });
@export(&c.kitty_graphics_placement_grid_size, .{ .name = "ghostty_kitty_graphics_placement_grid_size" });
@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" });

View File

@@ -426,7 +426,7 @@ pub const State = struct {
// Calculate the dimensions of our image, taking in to
// account the rows / columns specified by the placement.
const dest_size = p.calculatedSize(image.*, t);
const dest_size = p.pixelSize(image.*, t);
// Calculate the source rectangle
const source_x = @min(image.width, p.source_x);

View File

@@ -297,6 +297,48 @@ pub fn placement_rect(
return .success;
}
pub fn placement_pixel_size(
iter_: PlacementIterator,
image_: ImageHandle,
terminal_: terminal_c.Terminal,
out_width: *u32,
out_height: *u32,
) 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 s = entry.value_ptr.pixelSize(image.*, wrapper.terminal);
out_width.* = s.width;
out_height.* = s.height;
return .success;
}
pub fn placement_grid_size(
iter_: PlacementIterator,
image_: ImageHandle,
terminal_: terminal_c.Terminal,
out_cols: *u32,
out_rows: *u32,
) 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 s = entry.value_ptr.gridSize(image.*, wrapper.terminal);
out_cols.* = s.cols;
out_rows.* = s.rows;
return .success;
}
test "placement_iterator new/free" {
var iter: PlacementIterator = null;
try testing.expectEqual(Result.success, placement_iterator_new(
@@ -613,6 +655,115 @@ test "placement_rect null args return invalid_value" {
try testing.expectEqual(Result.invalid_value, placement_rect(null, null, null, &sel));
}
test "placement_pixel_size with transmit and display" {
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);
// 80 cols * 10px = 800px, 24 rows * 20px = 480px.
try testing.expectEqual(Result.success, terminal_c.resize(t, 80, 24, 10, 20));
// Transmit and display a 1x2 RGB image with c=10,r=1.
// 10 cols * 10px = 100px width, 1 row * 20px = 20px height.
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 w: u32 = undefined;
var h: u32 = undefined;
try testing.expectEqual(Result.success, placement_pixel_size(iter, img, t, &w, &h));
try testing.expectEqual(100, w);
try testing.expectEqual(20, h);
}
test "placement_pixel_size null args return invalid_value" {
if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
var w: u32 = undefined;
var h: u32 = undefined;
try testing.expectEqual(Result.invalid_value, placement_pixel_size(null, null, null, &w, &h));
}
test "placement_grid_size with transmit and display" {
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);
// 80 cols * 10px = 800px, 24 rows * 20px = 480px.
try testing.expectEqual(Result.success, terminal_c.resize(t, 80, 24, 10, 20));
// Transmit and display a 1x2 RGB image with c=10,r=1.
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 cols: u32 = undefined;
var rows: u32 = undefined;
try testing.expectEqual(Result.success, placement_grid_size(iter, img, t, &cols, &rows));
try testing.expectEqual(10, cols);
try testing.expectEqual(1, rows);
}
test "placement_grid_size null args return invalid_value" {
if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
var cols: u32 = undefined;
var rows: u32 = undefined;
try testing.expectEqual(Result.invalid_value, placement_grid_size(null, null, null, &cols, &rows));
}
test "image_get on null returns invalid_value" {
if (comptime !build_options.kitty_graphics) return error.SkipZigTest;

View File

@@ -17,6 +17,8 @@ pub const kitty_graphics_placement_iterator_free = kitty_graphics.placement_iter
pub const kitty_graphics_placement_next = kitty_graphics.placement_iterator_next;
pub const kitty_graphics_placement_get = kitty_graphics.placement_get;
pub const kitty_graphics_placement_rect = kitty_graphics.placement_rect;
pub const kitty_graphics_placement_pixel_size = kitty_graphics.placement_pixel_size;
pub const kitty_graphics_placement_grid_size = kitty_graphics.placement_grid_size;
pub const types = @import("types.zig");
pub const modes = @import("modes.zig");
pub const osc = @import("osc.zig");

View File

@@ -662,9 +662,10 @@ pub const ImageStorage = struct {
}
}
/// Calculates the size of this placement's image in pixels,
/// taking in to account the specified rows and columns.
pub fn calculatedSize(
/// Returns the size of this placement's image in pixels,
/// taking into account the source rectangle, specified
/// rows/columns, and aspect ratio.
pub fn pixelSize(
self: Placement,
image: Image,
t: *const terminal.Terminal,
@@ -759,7 +760,7 @@ pub const ImageStorage = struct {
// Otherwise we calculate the pixel size, divide by
// cell size, and round up to the nearest integer.
const calc_size = self.calculatedSize(image, t);
const calc_size = self.pixelSize(image, t);
return .{
.cols = std.math.divCeil(
u32,
@@ -1338,7 +1339,7 @@ test "storage: aspect ratio calculation when only columns or rows specified" {
// that's 100px width. 100px * (9 / 16) = 56.25, which should round
// to a height of 56px.
const calc_size = placement.calculatedSize(image, &t);
const calc_size = placement.pixelSize(image, &t);
try testing.expectEqual(@as(u32, 100), calc_size.width);
try testing.expectEqual(@as(u32, 56), calc_size.height);
}
@@ -1356,7 +1357,7 @@ test "storage: aspect ratio calculation when only columns or rows specified" {
// 100px height. 100px * (16 / 9) = 177.77..., which should round to
// a width of 178px.
const calc_size = placement.calculatedSize(image, &t);
const calc_size = placement.pixelSize(image, &t);
try testing.expectEqual(@as(u32, 178), calc_size.width);
try testing.expectEqual(@as(u32, 100), calc_size.height);
}