mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-18 05:20:29 +00:00
libghostty: add resolved source rect for placements
Add ghostty_kitty_graphics_placement_source_rect which returns the fully resolved and clamped source rectangle for a placement. This applies kitty protocol semantics (width/height of 0 means full image dimension) and clamps the result to the actual image bounds, eliminating ~20 lines of protocol-aware logic from each embedder.
This commit is contained in:
@@ -516,6 +516,33 @@ GHOSTTY_API GhosttyResult ghostty_kitty_graphics_placement_viewport_pos(
|
||||
int32_t* out_col,
|
||||
int32_t* out_row);
|
||||
|
||||
/**
|
||||
* Get the resolved source rectangle for the current placement.
|
||||
*
|
||||
* Applies kitty protocol semantics: a width or height of 0 in the
|
||||
* placement means "use the full image dimension", and the resulting
|
||||
* rectangle is clamped to the actual image bounds. The returned
|
||||
* values are in pixels and are ready to use for texture sampling.
|
||||
*
|
||||
* @param iterator The placement iterator positioned on a placement
|
||||
* @param image The image handle for this placement's image
|
||||
* @param[out] out_x Source rect x origin in pixels
|
||||
* @param[out] out_y Source rect y origin in pixels
|
||||
* @param[out] out_width Source rect width in pixels
|
||||
* @param[out] out_height Source rect height in pixels
|
||||
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if any
|
||||
* handle is NULL or the iterator is not positioned
|
||||
*
|
||||
* @ingroup kitty_graphics
|
||||
*/
|
||||
GHOSTTY_API GhosttyResult ghostty_kitty_graphics_placement_source_rect(
|
||||
GhosttyKittyGraphicsPlacementIterator iterator,
|
||||
GhosttyKittyGraphicsImage image,
|
||||
uint32_t* out_x,
|
||||
uint32_t* out_y,
|
||||
uint32_t* out_width,
|
||||
uint32_t* out_height);
|
||||
|
||||
/** @} */
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
@@ -246,6 +246,7 @@ comptime {
|
||||
@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.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.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" });
|
||||
|
||||
@@ -467,6 +467,35 @@ pub fn placement_viewport_pos(
|
||||
return .success;
|
||||
}
|
||||
|
||||
pub fn placement_source_rect(
|
||||
iter_: PlacementIterator,
|
||||
image_: ImageHandle,
|
||||
out_x: *u32,
|
||||
out_y: *u32,
|
||||
out_width: *u32,
|
||||
out_height: *u32,
|
||||
) callconv(lib.calling_conv) Result {
|
||||
if (comptime !build_options.kitty_graphics) return .no_value;
|
||||
|
||||
const image = image_ orelse return .invalid_value;
|
||||
const iter = iter_ orelse return .invalid_value;
|
||||
const entry = iter.entry orelse return .invalid_value;
|
||||
const p = entry.value_ptr;
|
||||
|
||||
// Apply "0 = full image dimension" convention, then clamp to image bounds.
|
||||
const x = @min(p.source_x, image.width);
|
||||
const y = @min(p.source_y, image.height);
|
||||
const w = @min(if (p.source_width > 0) p.source_width else image.width, image.width - x);
|
||||
const h = @min(if (p.source_height > 0) p.source_height else image.height, image.height - y);
|
||||
|
||||
out_x.* = x;
|
||||
out_y.* = y;
|
||||
out_width.* = w;
|
||||
out_height.* = h;
|
||||
|
||||
return .success;
|
||||
}
|
||||
|
||||
test "placement_iterator new/free" {
|
||||
var iter: PlacementIterator = null;
|
||||
try testing.expectEqual(Result.success, placement_iterator_new(
|
||||
@@ -1197,6 +1226,143 @@ test "placement_viewport_pos null args return invalid_value" {
|
||||
try testing.expectEqual(Result.invalid_value, placement_viewport_pos(null, null, null, &col, &row));
|
||||
}
|
||||
|
||||
test "placement_source_rect defaults to full image" {
|
||||
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));
|
||||
|
||||
// Transmit and display a 1x2 RGB image with no source rect specified.
|
||||
// source_width=0 and source_height=0 should resolve to full image (1x2).
|
||||
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 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 x: u32 = undefined;
|
||||
var y: u32 = undefined;
|
||||
var w: u32 = undefined;
|
||||
var h: u32 = undefined;
|
||||
try testing.expectEqual(Result.success, placement_source_rect(iter, img, &x, &y, &w, &h));
|
||||
try testing.expectEqual(0, x);
|
||||
try testing.expectEqual(0, y);
|
||||
try testing.expectEqual(1, w);
|
||||
try testing.expectEqual(2, h);
|
||||
}
|
||||
|
||||
test "placement_source_rect with explicit source rect" {
|
||||
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));
|
||||
|
||||
// Transmit a 4x4 RGBA image (64 bytes = 4*4*4).
|
||||
// Base64 of 64 zero bytes: 86 chars.
|
||||
const transmit = "\x1b_Ga=t,t=d,f=32,i=1,s=4,v=4;" ++
|
||||
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" ++
|
||||
"\x1b\\";
|
||||
// Display with explicit source rect: x=1, y=1, w=2, h=2.
|
||||
const display = "\x1b_Ga=p,i=1,p=1,x=1,y=1,w=2,h=2;\x1b\\";
|
||||
terminal_c.vt_write(t, transmit.ptr, transmit.len);
|
||||
terminal_c.vt_write(t, display.ptr, display.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 x: u32 = undefined;
|
||||
var y: u32 = undefined;
|
||||
var w: u32 = undefined;
|
||||
var h: u32 = undefined;
|
||||
try testing.expectEqual(Result.success, placement_source_rect(iter, img, &x, &y, &w, &h));
|
||||
try testing.expectEqual(1, x);
|
||||
try testing.expectEqual(1, y);
|
||||
try testing.expectEqual(2, w);
|
||||
try testing.expectEqual(2, h);
|
||||
}
|
||||
|
||||
test "placement_source_rect clamps to image bounds" {
|
||||
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));
|
||||
|
||||
// Transmit a 4x4 RGBA image.
|
||||
const transmit = "\x1b_Ga=t,t=d,f=32,i=1,s=4,v=4;" ++
|
||||
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" ++
|
||||
"\x1b\\";
|
||||
// Display with source rect that exceeds image bounds: x=3, y=3, w=10, h=10.
|
||||
// Should clamp to x=3, y=3, w=1, h=1.
|
||||
const display = "\x1b_Ga=p,i=1,p=1,x=3,y=3,w=10,h=10;\x1b\\";
|
||||
terminal_c.vt_write(t, transmit.ptr, transmit.len);
|
||||
terminal_c.vt_write(t, display.ptr, display.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 x: u32 = undefined;
|
||||
var y: u32 = undefined;
|
||||
var w: u32 = undefined;
|
||||
var h: u32 = undefined;
|
||||
try testing.expectEqual(Result.success, placement_source_rect(iter, img, &x, &y, &w, &h));
|
||||
try testing.expectEqual(3, x);
|
||||
try testing.expectEqual(3, y);
|
||||
try testing.expectEqual(1, w);
|
||||
try testing.expectEqual(1, h);
|
||||
}
|
||||
|
||||
test "placement_source_rect null args return invalid_value" {
|
||||
if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
|
||||
|
||||
var x: u32 = undefined;
|
||||
var y: u32 = undefined;
|
||||
var w: u32 = undefined;
|
||||
var h: u32 = undefined;
|
||||
try testing.expectEqual(Result.invalid_value, placement_source_rect(null, null, &x, &y, &w, &h));
|
||||
}
|
||||
|
||||
test "image_get on null returns invalid_value" {
|
||||
if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ 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 kitty_graphics_placement_viewport_pos = kitty_graphics.placement_viewport_pos;
|
||||
pub const kitty_graphics_placement_source_rect = kitty_graphics.placement_source_rect;
|
||||
pub const types = @import("types.zig");
|
||||
pub const modes = @import("modes.zig");
|
||||
pub const osc = @import("osc.zig");
|
||||
|
||||
Reference in New Issue
Block a user