diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-04-04 19:22:04 +0300 |
|---|---|---|
| committer | Can Celik <ogulcancelik@gmail.com> | 2026-04-05 01:23:06 +0300 |
| commit | 41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch) | |
| tree | 3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/apprt/gtk/ext | |
| parent | 55c138263e190a1e79d4b997bfb9397306c0ec1f (diff) | |
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/apprt/gtk/ext')
| -rw-r--r-- | vendor/libghostty-vt/src/apprt/gtk/ext/actions.zig | 198 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/apprt/gtk/ext/slice.zig | 111 |
2 files changed, 309 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/apprt/gtk/ext/actions.zig b/vendor/libghostty-vt/src/apprt/gtk/ext/actions.zig new file mode 100644 index 00000000..3232bc18 --- /dev/null +++ b/vendor/libghostty-vt/src/apprt/gtk/ext/actions.zig @@ -0,0 +1,198 @@ +const std = @import("std"); + +const assert = @import("../../../quirks.zig").inlineAssert; +const testing = std.testing; + +const gio = @import("gio"); +const glib = @import("glib"); +const gobject = @import("gobject"); +const gtk = @import("gtk"); + +const gValueHolds = @import("../ext.zig").gValueHolds; + +/// Check that an action name is valid. +/// +/// Reimplementation of `g_action_name_is_valid()` so that it can be +/// used at comptime. +/// +/// See: +/// https://docs.gtk.org/gio/type_func.Action.name_is_valid.html +fn gActionNameIsValid(name: [:0]const u8) bool { + if (name.len == 0) return false; + + for (name) |c| switch (c) { + '-' => continue, + '.' => continue, + '0'...'9' => continue, + 'a'...'z' => continue, + 'A'...'Z' => continue, + else => return false, + }; + + return true; +} + +test "gActionNameIsValid" { + try testing.expect(gActionNameIsValid("ring-bell")); + try testing.expect(!gActionNameIsValid("ring_bell")); +} + +/// Function to create a structure for describing an action. +pub fn Action(comptime T: type) type { + return struct { + const Self = @This(); + pub const Callback = *const fn (*gio.SimpleAction, ?*glib.Variant, *T) callconv(.c) void; + + name: [:0]const u8, + callback: Callback, + parameter_type: ?*const glib.VariantType, + state: ?*glib.Variant = null, + + /// Function to initialize a new action so that we can comptime check + /// the name. + pub fn init( + comptime name: [:0]const u8, + callback: Callback, + parameter_type: ?*const glib.VariantType, + ) Self { + comptime assert(gActionNameIsValid(name)); + + return .{ + .name = name, + .callback = callback, + .parameter_type = parameter_type, + }; + } + + /// Function to initialize a new stateful action so that we can comptime + /// check the name. + pub fn initStateful( + comptime name: [:0]const u8, + callback: Callback, + parameter_type: ?*const glib.VariantType, + state: *glib.Variant, + ) Self { + comptime assert(gActionNameIsValid(name)); + return .{ + .name = name, + .callback = callback, + .parameter_type = parameter_type, + .state = state, + }; + } + }; +} + +/// Add actions to a widget that implements gio.ActionMap. +pub fn add(comptime T: type, self: *T, actions: []const Action(T)) void { + addToMap(T, self, self.as(gio.ActionMap), actions); +} + +/// Add actions to the given map. +pub fn addToMap(comptime T: type, self: *T, map: *gio.ActionMap, actions: []const Action(T)) void { + for (actions) |entry| { + assert(gActionNameIsValid(entry.name)); + const action = action: { + if (entry.state) |state| { + break :action gio.SimpleAction.newStateful( + entry.name, + entry.parameter_type, + state, + ); + } + break :action gio.SimpleAction.new( + entry.name, + entry.parameter_type, + ); + }; + defer action.unref(); + _ = gio.SimpleAction.signals.activate.connect( + action, + *T, + entry.callback, + self, + .{}, + ); + map.addAction(action.as(gio.Action)); + } +} + +/// Add actions to a widget that doesn't implement ActionGroup directly. +pub fn addAsGroup(comptime T: type, self: *T, comptime name: [:0]const u8, actions: []const Action(T)) *gio.SimpleActionGroup { + comptime assert(gActionNameIsValid(name)); + + // Collect our actions into a group since we're just a plain widget that + // doesn't implement ActionGroup directly. + const group = gio.SimpleActionGroup.new(); + errdefer group.unref(); + + addToMap(T, self, group.as(gio.ActionMap), actions); + + self.as(gtk.Widget).insertActionGroup( + name, + group.as(gio.ActionGroup), + ); + + return group; +} + +test "adding actions to an object" { + // This test requires a connection to an active display environment. + if (gtk.initCheck() == 0) return error.SkipZigTest; + + _ = glib.MainContext.acquire(null); + defer glib.MainContext.release(null); + + const callbacks = struct { + fn callback(_: *gio.SimpleAction, variant_: ?*glib.Variant, self: *gtk.Box) callconv(.c) void { + const i32_variant_type = glib.ext.VariantType.newFor(i32); + defer i32_variant_type.free(); + + const variant = variant_ orelse return; + assert(variant.isOfType(i32_variant_type) != 0); + + var value = std.mem.zeroes(gobject.Value); + _ = value.init(gobject.ext.types.int); + defer value.unset(); + + value.setInt(variant.getInt32()); + + self.as(gobject.Object).setProperty("spacing", &value); + } + }; + + const box = gtk.Box.new(.vertical, 0); + _ = box.as(gobject.Object).refSink(); + defer box.unref(); + + { + const i32_variant_type = glib.ext.VariantType.newFor(i32); + defer i32_variant_type.free(); + + const actions = [_]Action(gtk.Box){ + .init("test", callbacks.callback, i32_variant_type), + }; + + _ = addAsGroup(gtk.Box, box, "test", &actions); + } + + const expected = std.crypto.random.intRangeAtMost(i32, 1, std.math.maxInt(u31)); + const parameter = glib.Variant.newInt32(expected); + + try testing.expect(box.as(gtk.Widget).activateActionVariant("test.test", parameter) != 0); + + _ = glib.MainContext.iteration(null, @intFromBool(true)); + + var value = std.mem.zeroes(gobject.Value); + _ = value.init(gobject.ext.types.int); + defer value.unset(); + + box.as(gobject.Object).getProperty("spacing", &value); + + try testing.expect(gValueHolds(&value, gobject.ext.types.int)); + + const actual = value.getInt(); + try testing.expectEqual(expected, actual); + + while (glib.MainContext.iteration(null, 0) != 0) {} +} diff --git a/vendor/libghostty-vt/src/apprt/gtk/ext/slice.zig b/vendor/libghostty-vt/src/apprt/gtk/ext/slice.zig new file mode 100644 index 00000000..49ad63d8 --- /dev/null +++ b/vendor/libghostty-vt/src/apprt/gtk/ext/slice.zig @@ -0,0 +1,111 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const ArenaAllocator = std.heap.ArenaAllocator; +const glib = @import("glib"); +const gobject = @import("gobject"); + +/// A boxed type that holds a list of string slices. +pub const StringList = struct { + arena: ArenaAllocator, + strings: []const [:0]const u8, + + pub fn create( + alloc: Allocator, + strings: []const [:0]const u8, + ) Allocator.Error!*StringList { + var arena: ArenaAllocator = .init(alloc); + errdefer arena.deinit(); + const arena_alloc = arena.allocator(); + var stored = try arena_alloc.alloc([:0]const u8, strings.len); + for (strings, 0..) |s, i| stored[i] = try arena_alloc.dupeZ(u8, s); + + const ptr = try alloc.create(StringList); + errdefer alloc.destroy(ptr); + ptr.* = .{ .arena = arena, .strings = stored }; + + return ptr; + } + + pub fn deinit(self: *StringList) void { + self.arena.deinit(); + } + + pub fn destroy(self: *StringList) void { + const alloc = self.arena.child_allocator; + self.deinit(); + alloc.destroy(self); + } + + /// Returns the general-purpose allocator used by this StringList. + pub fn allocator(self: *const StringList) Allocator { + return self.arena.child_allocator; + } + + pub const getGObjectType = gobject.ext.defineBoxed( + StringList, + .{ + .name = "GhosttyStringList", + .funcs = .{ + .copy = &struct { + fn copy(self: *StringList) callconv(.c) *StringList { + return StringList.create( + self.arena.child_allocator, + self.strings, + ) catch @panic("OOM"); + } + }.copy, + .free = &struct { + fn free(self: *StringList) callconv(.c) void { + self.destroy(); + } + }.free, + }, + }, + ); +}; + +test "StringList create and destroy" { + const testing = std.testing; + const alloc = testing.allocator; + + const input: []const [:0]const u8 = &.{ "hello", "world" }; + const list = try StringList.create(alloc, input); + defer list.destroy(); + + try testing.expectEqual(@as(usize, 2), list.strings.len); + try testing.expectEqualStrings("hello", list.strings[0]); + try testing.expectEqualStrings("world", list.strings[1]); +} + +test "StringList create empty list" { + const testing = std.testing; + const alloc = testing.allocator; + + const input: []const [:0]const u8 = &.{}; + const list = try StringList.create(alloc, input); + defer list.destroy(); + + try testing.expectEqual(@as(usize, 0), list.strings.len); +} + +test "StringList boxedCopy and boxedFree" { + const testing = std.testing; + const alloc = testing.allocator; + + const input: []const [:0]const u8 = &.{ "foo", "bar", "baz" }; + const original = try StringList.create(alloc, input); + defer original.destroy(); + + const copied: *StringList = @ptrCast(@alignCast(gobject.boxedCopy( + StringList.getGObjectType(), + original, + ))); + defer gobject.boxedFree(StringList.getGObjectType(), copied); + + try testing.expectEqual(@as(usize, 3), copied.strings.len); + try testing.expectEqualStrings("foo", copied.strings[0]); + try testing.expectEqualStrings("bar", copied.strings[1]); + try testing.expectEqualStrings("baz", copied.strings[2]); + + try testing.expect(original.strings.ptr != copied.strings.ptr); +} |
