libghostty: add z-layer filtered placement iterator

Add a placement_iterator_set function that configures iterator
properties via an enum, following the same pattern as other set
functions in the C API (e.g. render_state_set). The first settable
option is a z-layer filter.

The GhosttyKittyPlacementLayer enum classifies placements into three
layers based on kitty protocol z-index conventions: below background
(z < INT32_MIN/2), below text (INT32_MIN/2 <= z < 0), and above text
(z >= 0). The default is ALL which preserves existing behavior.

When a layer filter is set, placement_iterator_next automatically
skips non-matching placements, so embedders no longer need to
reimplement the z-index bucketing logic or iterate all placements
three times per frame just to filter by layer.
This commit is contained in:
Mitchell Hashimoto
2026-04-06 12:22:23 -07:00
parent 800cc64f1b
commit 66bfdf8e7a
4 changed files with 221 additions and 2 deletions

View File

@@ -138,6 +138,38 @@ typedef enum {
GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_Z = 12,
} GhosttyKittyGraphicsPlacementData;
/**
* Z-layer classification for kitty graphics placements.
*
* Based on the kitty protocol z-index conventions:
* - BELOW_BG: z < INT32_MIN/2 (drawn below cell background)
* - BELOW_TEXT: INT32_MIN/2 <= z < 0 (above background, below text)
* - ABOVE_TEXT: z >= 0 (above text)
* - ALL: no filtering (current behavior)
*
* @ingroup kitty_graphics
*/
typedef enum {
GHOSTTY_KITTY_PLACEMENT_LAYER_ALL = 0,
GHOSTTY_KITTY_PLACEMENT_LAYER_BELOW_BG = 1,
GHOSTTY_KITTY_PLACEMENT_LAYER_BELOW_TEXT = 2,
GHOSTTY_KITTY_PLACEMENT_LAYER_ABOVE_TEXT = 3,
} GhosttyKittyPlacementLayer;
/**
* Settable options for ghostty_kitty_graphics_placement_iterator_set().
*
* @ingroup kitty_graphics
*/
typedef enum {
/**
* Set the z-layer filter for the iterator.
*
* Input type: GhosttyKittyPlacementLayer *
*/
GHOSTTY_KITTY_GRAPHICS_PLACEMENT_ITERATOR_OPTION_LAYER = 0,
} GhosttyKittyGraphicsPlacementIteratorOption;
/**
* Pixel format of a Kitty graphics image.
*
@@ -310,9 +342,36 @@ GHOSTTY_API GhosttyResult ghostty_kitty_graphics_placement_iterator_new(
GHOSTTY_API void ghostty_kitty_graphics_placement_iterator_free(
GhosttyKittyGraphicsPlacementIterator iterator);
/**
* Set an option on a placement iterator.
*
* Use GHOSTTY_KITTY_GRAPHICS_PLACEMENT_ITERATOR_OPTION_LAYER with a
* GhosttyKittyPlacementLayer value to filter placements by z-layer.
* The filter is applied during iteration: ghostty_kitty_graphics_placement_next()
* will skip placements that do not match the configured layer.
*
* The default layer is GHOSTTY_KITTY_PLACEMENT_LAYER_ALL (no filtering).
*
* @param iterator The iterator handle (NULL returns GHOSTTY_INVALID_VALUE)
* @param option The option to set
* @param value Pointer to the value (type depends on option; NULL returns
* GHOSTTY_INVALID_VALUE)
* @return GHOSTTY_SUCCESS on success
*
* @ingroup kitty_graphics
*/
GHOSTTY_API GhosttyResult ghostty_kitty_graphics_placement_iterator_set(
GhosttyKittyGraphicsPlacementIterator iterator,
GhosttyKittyGraphicsPlacementIteratorOption option,
const void* value);
/**
* Advance the placement iterator to the next placement.
*
* If a layer filter has been set via
* ghostty_kitty_graphics_placement_iterator_set(), only placements
* matching that layer are returned.
*
* @param iterator The iterator handle (may be NULL)
* @return true if advanced to the next placement, false if at the end
*

View File

@@ -239,6 +239,7 @@ comptime {
@export(&c.kitty_graphics_image_get, .{ .name = "ghostty_kitty_graphics_image_get" });
@export(&c.kitty_graphics_placement_iterator_new, .{ .name = "ghostty_kitty_graphics_placement_iterator_new" });
@export(&c.kitty_graphics_placement_iterator_free, .{ .name = "ghostty_kitty_graphics_placement_iterator_free" });
@export(&c.kitty_graphics_placement_iterator_set, .{ .name = "ghostty_kitty_graphics_placement_iterator_set" });
@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" });

View File

@@ -42,6 +42,7 @@ const PlacementIteratorWrapper = if (build_options.kitty_graphics)
alloc: std.mem.Allocator,
inner: PlacementMap.Iterator = undefined,
entry: ?PlacementMap.Entry = null,
layer_filter: PlacementLayer = .all,
}
else
void;
@@ -130,6 +131,34 @@ fn getTyped(
return .success;
}
/// C: GhosttyKittyPlacementLayer
pub const PlacementLayer = enum(c_int) {
all = 0,
below_bg = 1,
below_text = 2,
above_text = 3,
fn matches(self: PlacementLayer, z: i32) bool {
return switch (self) {
.all => true,
.below_bg => z < std.math.minInt(i32) / 2,
.below_text => z >= std.math.minInt(i32) / 2 and z < 0,
.above_text => z >= 0,
};
}
};
/// C: GhosttyKittyGraphicsPlacementIteratorOption
pub const PlacementIteratorOption = enum(c_int) {
layer = 0,
pub fn InType(comptime self: PlacementIteratorOption) type {
return switch (self) {
.layer => PlacementLayer,
};
}
};
/// C: GhosttyKittyImageFormat
pub const ImageFormat = kitty_cmd.Transmission.Format;
@@ -233,12 +262,51 @@ pub fn placement_iterator_free(iter_: PlacementIterator) callconv(lib.calling_co
iter.alloc.destroy(iter);
}
pub fn placement_iterator_set(
iter_: PlacementIterator,
option: PlacementIteratorOption,
value: ?*const anyopaque,
) callconv(lib.calling_conv) Result {
if (comptime !build_options.kitty_graphics) return .no_value;
if (comptime std.debug.runtime_safety) {
_ = std.meta.intToEnum(PlacementIteratorOption, @intFromEnum(option)) catch {
return .invalid_value;
};
}
return switch (option) {
inline else => |comptime_option| placementIteratorSetTyped(
iter_,
comptime_option,
@ptrCast(@alignCast(value orelse return .invalid_value)),
),
};
}
fn placementIteratorSetTyped(
iter_: PlacementIterator,
comptime option: PlacementIteratorOption,
value: *const option.InType(),
) Result {
const iter = iter_ orelse return .invalid_value;
switch (option) {
.layer => iter.layer_filter = value.*,
}
return .success;
}
pub fn placement_iterator_next(iter_: PlacementIterator) callconv(lib.calling_conv) bool {
if (comptime !build_options.kitty_graphics) return false;
const iter = iter_ orelse return false;
iter.entry = iter.inner.next() orelse return false;
return true;
while (iter.inner.next()) |entry| {
if (iter.layer_filter.matches(entry.value_ptr.z)) {
iter.entry = entry;
return true;
}
}
return false;
}
pub fn placement_get(
@@ -537,6 +605,96 @@ test "placement_iterator with multiple placements" {
try testing.expect(seen_p2);
}
test "placement_iterator_set layer filter" {
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);
// Transmit image 1.
const transmit = "\x1b_Ga=t,t=d,f=24,i=1,s=1,v=2;////////\x1b\\";
terminal_c.vt_write(t, transmit.ptr, transmit.len);
// Display with z=5 (above text), z=-1 (below text), z=-1073741825 (below bg).
// INT32_MIN/2 = -1073741824, so -1073741825 < INT32_MIN/2.
const d1 = "\x1b_Ga=p,i=1,p=1,z=5;\x1b\\";
const d2 = "\x1b_Ga=p,i=1,p=2,z=-1;\x1b\\";
const d3 = "\x1b_Ga=p,i=1,p=3,z=-1073741825;\x1b\\";
terminal_c.vt_write(t, d1.ptr, d1.len);
terminal_c.vt_write(t, d2.ptr, d2.len);
terminal_c.vt_write(t, d3.ptr, d3.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);
// Filter: above_text (z >= 0) — should yield only p=1.
var layer = PlacementLayer.above_text;
try testing.expectEqual(Result.success, placement_iterator_set(iter, .layer, @ptrCast(&layer)));
try testing.expectEqual(Result.success, get(graphics, .placement_iterator, @ptrCast(&iter)));
var count: u32 = 0;
while (placement_iterator_next(iter)) {
var z: i32 = undefined;
try testing.expectEqual(Result.success, placement_get(iter, .z, @ptrCast(&z)));
try testing.expect(z >= 0);
count += 1;
}
try testing.expectEqual(1, count);
// Filter: below_text (INT32_MIN/2 <= z < 0) — should yield only p=2.
layer = .below_text;
try testing.expectEqual(Result.success, placement_iterator_set(iter, .layer, @ptrCast(&layer)));
try testing.expectEqual(Result.success, get(graphics, .placement_iterator, @ptrCast(&iter)));
count = 0;
while (placement_iterator_next(iter)) {
var z: i32 = undefined;
try testing.expectEqual(Result.success, placement_get(iter, .z, @ptrCast(&z)));
try testing.expect(z >= std.math.minInt(i32) / 2 and z < 0);
count += 1;
}
try testing.expectEqual(1, count);
// Filter: below_bg (z < INT32_MIN/2) — should yield only p=3.
layer = .below_bg;
try testing.expectEqual(Result.success, placement_iterator_set(iter, .layer, @ptrCast(&layer)));
try testing.expectEqual(Result.success, get(graphics, .placement_iterator, @ptrCast(&iter)));
count = 0;
while (placement_iterator_next(iter)) {
var z: i32 = undefined;
try testing.expectEqual(Result.success, placement_get(iter, .z, @ptrCast(&z)));
try testing.expect(z < std.math.minInt(i32) / 2);
count += 1;
}
try testing.expectEqual(1, count);
// Filter: all — should yield all 3.
layer = .all;
try testing.expectEqual(Result.success, placement_iterator_set(iter, .layer, @ptrCast(&layer)));
try testing.expectEqual(Result.success, get(graphics, .placement_iterator, @ptrCast(&iter)));
count = 0;
while (placement_iterator_next(iter)) count += 1;
try testing.expectEqual(3, count);
}
test "image_get_handle returns null for missing id" {
if (comptime !build_options.kitty_graphics) return error.SkipZigTest;

View File

@@ -14,6 +14,7 @@ pub const kitty_graphics_image = kitty_graphics.image_get_handle;
pub const kitty_graphics_image_get = kitty_graphics.image_get;
pub const kitty_graphics_placement_iterator_new = kitty_graphics.placement_iterator_new;
pub const kitty_graphics_placement_iterator_free = kitty_graphics.placement_iterator_free;
pub const kitty_graphics_placement_iterator_set = kitty_graphics.placement_iterator_set;
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;