mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-06-06 03:44:22 +00:00
Add a C-facing GhosttyRenderStateColors sized struct and a ghostty_render_state_colors_get accessor so renderers can read background, foreground, cursor color state, and palette data directly from the render state.
406 lines
11 KiB
Zig
406 lines
11 KiB
Zig
const std = @import("std");
|
|
const testing = std.testing;
|
|
const lib = @import("../../lib/main.zig");
|
|
const lib_alloc = @import("../../lib/allocator.zig");
|
|
const CAllocator = lib_alloc.Allocator;
|
|
const colorpkg = @import("../color.zig");
|
|
const size = @import("../size.zig");
|
|
const terminal_c = @import("terminal.zig");
|
|
const renderpkg = @import("../render.zig");
|
|
const Result = @import("result.zig").Result;
|
|
|
|
const RenderStateWrapper = struct {
|
|
alloc: std.mem.Allocator,
|
|
state: renderpkg.RenderState = .empty,
|
|
};
|
|
|
|
/// C: GhosttyRenderState
|
|
pub const RenderState = ?*RenderStateWrapper;
|
|
|
|
/// C: GhosttyRenderStateDirty
|
|
pub const Dirty = renderpkg.RenderState.Dirty;
|
|
|
|
/// C: GhosttyRenderStateColors
|
|
pub const Colors = extern struct {
|
|
size: usize = @sizeOf(Colors),
|
|
background: colorpkg.RGB.C,
|
|
foreground: colorpkg.RGB.C,
|
|
cursor: colorpkg.RGB.C,
|
|
cursor_has_value: bool,
|
|
palette: [256]colorpkg.RGB.C,
|
|
};
|
|
|
|
pub fn new(
|
|
alloc_: ?*const CAllocator,
|
|
result: *RenderState,
|
|
) callconv(.c) Result {
|
|
result.* = new_(alloc_) catch |err| {
|
|
result.* = null;
|
|
return switch (err) {
|
|
error.OutOfMemory => .out_of_memory,
|
|
};
|
|
};
|
|
|
|
return .success;
|
|
}
|
|
|
|
fn new_(alloc_: ?*const CAllocator) error{OutOfMemory}!*RenderStateWrapper {
|
|
const alloc = lib_alloc.default(alloc_);
|
|
const ptr = alloc.create(RenderStateWrapper) catch
|
|
return error.OutOfMemory;
|
|
ptr.* = .{ .alloc = alloc };
|
|
return ptr;
|
|
}
|
|
|
|
pub fn update(
|
|
state_: RenderState,
|
|
terminal_: terminal_c.Terminal,
|
|
) callconv(.c) Result {
|
|
const state = state_ orelse return .invalid_value;
|
|
const t = terminal_ orelse return .invalid_value;
|
|
|
|
state.state.update(state.alloc, t) catch return .out_of_memory;
|
|
return .success;
|
|
}
|
|
|
|
pub fn size_get(
|
|
state_: RenderState,
|
|
out_cols_: ?*size.CellCountInt,
|
|
out_rows_: ?*size.CellCountInt,
|
|
) callconv(.c) Result {
|
|
const state = state_ orelse return .invalid_value;
|
|
const out_cols = out_cols_ orelse return .invalid_value;
|
|
const out_rows = out_rows_ orelse return .invalid_value;
|
|
|
|
out_cols.* = state.state.cols;
|
|
out_rows.* = state.state.rows;
|
|
return .success;
|
|
}
|
|
|
|
pub fn colors_get(
|
|
state_: RenderState,
|
|
out_colors_: ?*Colors,
|
|
) callconv(.c) Result {
|
|
const state = state_ orelse return .invalid_value;
|
|
const out_colors = out_colors_ orelse return .invalid_value;
|
|
const out_size = out_colors.size;
|
|
if (out_size < @sizeOf(usize)) return .invalid_value;
|
|
|
|
const colors = state.state.colors;
|
|
if (lib.structSizedFieldFits(
|
|
Colors,
|
|
out_size,
|
|
"background",
|
|
)) {
|
|
out_colors.background = colors.background.cval();
|
|
}
|
|
|
|
if (lib.structSizedFieldFits(
|
|
Colors,
|
|
out_size,
|
|
"foreground",
|
|
)) {
|
|
out_colors.foreground = colors.foreground.cval();
|
|
}
|
|
|
|
if (colors.cursor) |cursor| {
|
|
if (lib.structSizedFieldFits(
|
|
Colors,
|
|
out_size,
|
|
"cursor",
|
|
)) {
|
|
out_colors.cursor = cursor.cval();
|
|
}
|
|
}
|
|
|
|
if (lib.structSizedFieldFits(
|
|
Colors,
|
|
out_size,
|
|
"cursor_has_value",
|
|
)) {
|
|
out_colors.cursor_has_value = colors.cursor != null;
|
|
}
|
|
|
|
if (lib.structSizedFieldFits(
|
|
Colors,
|
|
out_size,
|
|
"palette",
|
|
)) {
|
|
const palette_offset = @offsetOf(Colors, "palette");
|
|
if (out_size > palette_offset) {
|
|
const available = out_size - palette_offset;
|
|
const max_entries = @min(colors.palette.len, available / @sizeOf(colorpkg.RGB.C));
|
|
for (0..max_entries) |i| {
|
|
out_colors.palette[i] = colors.palette[i].cval();
|
|
}
|
|
}
|
|
}
|
|
|
|
return .success;
|
|
}
|
|
|
|
pub fn dirty_get(
|
|
state_: RenderState,
|
|
out_dirty: *Dirty,
|
|
) callconv(.c) Result {
|
|
const state = state_ orelse return .invalid_value;
|
|
out_dirty.* = state.state.dirty;
|
|
return .success;
|
|
}
|
|
|
|
pub fn dirty_set(
|
|
state_: RenderState,
|
|
dirty_: c_int,
|
|
) callconv(.c) Result {
|
|
const state = state_ orelse return .invalid_value;
|
|
const dirty = std.meta.intToEnum(Dirty, dirty_) catch
|
|
return .invalid_value;
|
|
state.state.dirty = dirty;
|
|
return .success;
|
|
}
|
|
|
|
pub fn free(state_: RenderState) callconv(.c) void {
|
|
const state = state_ orelse return;
|
|
const alloc = state.alloc;
|
|
state.state.deinit(alloc);
|
|
alloc.destroy(state);
|
|
}
|
|
|
|
test "render: new/free" {
|
|
var state: RenderState = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib_alloc.test_allocator,
|
|
&state,
|
|
));
|
|
try testing.expect(state != null);
|
|
free(state);
|
|
}
|
|
|
|
test "render: free null" {
|
|
free(null);
|
|
}
|
|
|
|
test "render: update invalid value" {
|
|
var state: RenderState = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib_alloc.test_allocator,
|
|
&state,
|
|
));
|
|
defer free(state);
|
|
|
|
try testing.expectEqual(Result.invalid_value, update(null, null));
|
|
try testing.expectEqual(Result.invalid_value, update(state, null));
|
|
}
|
|
|
|
test "render: size get invalid value" {
|
|
var state: RenderState = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib_alloc.test_allocator,
|
|
&state,
|
|
));
|
|
defer free(state);
|
|
|
|
var cols: size.CellCountInt = 0;
|
|
var rows: size.CellCountInt = 0;
|
|
try testing.expectEqual(Result.invalid_value, size_get(
|
|
null,
|
|
&cols,
|
|
&rows,
|
|
));
|
|
try testing.expectEqual(Result.invalid_value, size_get(
|
|
state,
|
|
null,
|
|
&rows,
|
|
));
|
|
try testing.expectEqual(Result.invalid_value, size_get(
|
|
state,
|
|
&cols,
|
|
null,
|
|
));
|
|
}
|
|
|
|
test "render: colors get invalid value" {
|
|
var state: RenderState = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib_alloc.test_allocator,
|
|
&state,
|
|
));
|
|
defer free(state);
|
|
|
|
var colors: Colors = std.mem.zeroes(Colors);
|
|
colors.size = @sizeOf(Colors);
|
|
|
|
try testing.expectEqual(Result.invalid_value, colors_get(null, &colors));
|
|
try testing.expectEqual(Result.invalid_value, colors_get(state, null));
|
|
|
|
colors.size = @sizeOf(usize) - 1;
|
|
try testing.expectEqual(Result.invalid_value, colors_get(state, &colors));
|
|
}
|
|
|
|
test "render: dirty get/set invalid value" {
|
|
var state: RenderState = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib_alloc.test_allocator,
|
|
&state,
|
|
));
|
|
defer free(state);
|
|
|
|
var dirty: Dirty = .false;
|
|
try testing.expectEqual(Result.invalid_value, dirty_get(null, &dirty));
|
|
try testing.expectEqual(Result.invalid_value, dirty_set(
|
|
null,
|
|
@intFromEnum(Dirty.full),
|
|
));
|
|
}
|
|
|
|
test "render: dirty get/set" {
|
|
var state: RenderState = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib_alloc.test_allocator,
|
|
&state,
|
|
));
|
|
defer free(state);
|
|
|
|
var dirty: Dirty = undefined;
|
|
try testing.expectEqual(Result.success, dirty_get(state, &dirty));
|
|
try testing.expectEqual(Dirty.false, dirty);
|
|
|
|
try testing.expectEqual(Result.success, dirty_set(
|
|
state,
|
|
@intFromEnum(Dirty.partial),
|
|
));
|
|
try testing.expectEqual(Result.success, dirty_get(state, &dirty));
|
|
try testing.expectEqual(Dirty.partial, dirty);
|
|
|
|
try testing.expectEqual(Result.success, dirty_set(
|
|
state,
|
|
@intFromEnum(Dirty.full),
|
|
));
|
|
try testing.expectEqual(Result.success, dirty_get(state, &dirty));
|
|
try testing.expectEqual(Dirty.full, dirty);
|
|
}
|
|
|
|
test "render: dirty set invalid enum value" {
|
|
var state: RenderState = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib_alloc.test_allocator,
|
|
&state,
|
|
));
|
|
defer free(state);
|
|
|
|
try testing.expectEqual(Result.invalid_value, dirty_set(state, 99));
|
|
}
|
|
|
|
test "render: update" {
|
|
var terminal: terminal_c.Terminal = null;
|
|
try testing.expectEqual(Result.success, terminal_c.new(
|
|
&lib_alloc.test_allocator,
|
|
&terminal,
|
|
.{
|
|
.cols = 80,
|
|
.rows = 24,
|
|
.max_scrollback = 10_000,
|
|
},
|
|
));
|
|
defer terminal_c.free(terminal);
|
|
|
|
var state: RenderState = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib_alloc.test_allocator,
|
|
&state,
|
|
));
|
|
defer free(state);
|
|
|
|
try testing.expectEqual(Result.success, update(state, terminal));
|
|
|
|
terminal_c.vt_write(terminal, "hello", 5);
|
|
try testing.expectEqual(Result.success, update(state, terminal));
|
|
|
|
var cols: size.CellCountInt = 0;
|
|
var rows: size.CellCountInt = 0;
|
|
try testing.expectEqual(Result.success, size_get(
|
|
state,
|
|
&cols,
|
|
&rows,
|
|
));
|
|
try testing.expectEqual(@as(size.CellCountInt, 80), cols);
|
|
try testing.expectEqual(@as(size.CellCountInt, 24), rows);
|
|
}
|
|
|
|
test "render: colors get" {
|
|
var terminal: terminal_c.Terminal = null;
|
|
try testing.expectEqual(Result.success, terminal_c.new(
|
|
&lib_alloc.test_allocator,
|
|
&terminal,
|
|
.{
|
|
.cols = 80,
|
|
.rows = 24,
|
|
.max_scrollback = 10_000,
|
|
},
|
|
));
|
|
defer terminal_c.free(terminal);
|
|
|
|
var state: RenderState = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib_alloc.test_allocator,
|
|
&state,
|
|
));
|
|
defer free(state);
|
|
|
|
try testing.expectEqual(Result.success, update(state, terminal));
|
|
|
|
var colors: Colors = std.mem.zeroes(Colors);
|
|
colors.size = @sizeOf(Colors);
|
|
try testing.expectEqual(Result.success, colors_get(state, &colors));
|
|
|
|
const state_colors = &state.?.state.colors;
|
|
try testing.expectEqual(state_colors.background.cval(), colors.background);
|
|
try testing.expectEqual(state_colors.foreground.cval(), colors.foreground);
|
|
|
|
if (state_colors.cursor) |cursor| {
|
|
try testing.expect(colors.cursor_has_value);
|
|
try testing.expectEqual(cursor.cval(), colors.cursor);
|
|
} else {
|
|
try testing.expect(!colors.cursor_has_value);
|
|
}
|
|
|
|
for (state_colors.palette, colors.palette) |expected, actual| {
|
|
try testing.expectEqual(expected.cval(), actual);
|
|
}
|
|
}
|
|
|
|
test "render: colors get supports truncated sized struct" {
|
|
var terminal: terminal_c.Terminal = null;
|
|
try testing.expectEqual(Result.success, terminal_c.new(
|
|
&lib_alloc.test_allocator,
|
|
&terminal,
|
|
.{
|
|
.cols = 80,
|
|
.rows = 24,
|
|
.max_scrollback = 10_000,
|
|
},
|
|
));
|
|
defer terminal_c.free(terminal);
|
|
|
|
var state: RenderState = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib_alloc.test_allocator,
|
|
&state,
|
|
));
|
|
defer free(state);
|
|
|
|
try testing.expectEqual(Result.success, update(state, terminal));
|
|
|
|
var colors: Colors = std.mem.zeroes(Colors);
|
|
const sentinel: colorpkg.RGB.C = .{ .r = 0xAA, .g = 0xBB, .b = 0xCC };
|
|
for (&colors.palette) |*entry| entry.* = sentinel;
|
|
|
|
colors.size = @offsetOf(Colors, "palette") + @sizeOf(colorpkg.RGB.C) * 2;
|
|
try testing.expectEqual(Result.success, colors_get(state, &colors));
|
|
|
|
const state_colors = &state.?.state.colors;
|
|
try testing.expectEqual(state_colors.palette[0].cval(), colors.palette[0]);
|
|
try testing.expectEqual(state_colors.palette[1].cval(), colors.palette[1]);
|
|
try testing.expectEqual(sentinel, colors.palette[2]);
|
|
}
|