diff --git a/src/apprt/gtk-ng/class/application.zig b/src/apprt/gtk-ng/class/application.zig index 1515aa29f..2278a0f4c 100644 --- a/src/apprt/gtk-ng/class/application.zig +++ b/src/apprt/gtk-ng/class/application.zig @@ -1112,7 +1112,7 @@ pub const Application = extern struct { const as_variant_type = glib.VariantType.new("as"); defer as_variant_type.free(); - const actions = [_]ext.Action(Self){ + const actions = [_]ext.actions.Action(Self){ .init("new-window", actionNewWindow, null), .init("new-window-command", actionNewWindow, as_variant_type), .init("open-config", actionOpenConfig, null), @@ -1121,7 +1121,7 @@ pub const Application = extern struct { .init("reload-config", actionReloadConfig, null), }; - ext.addActions(Self, self, &actions); + ext.actions.add(Self, self, &actions); } /// Setup our global shortcuts. diff --git a/src/apprt/gtk-ng/class/split_tree.zig b/src/apprt/gtk-ng/class/split_tree.zig index 997945f18..3b6dcb4a9 100644 --- a/src/apprt/gtk-ng/class/split_tree.zig +++ b/src/apprt/gtk-ng/class/split_tree.zig @@ -171,7 +171,7 @@ pub const SplitTree = extern struct { const s_variant_type = glib.ext.VariantType.newFor([:0]const u8); defer s_variant_type.free(); - const actions = [_]ext.Action(Self){ + const actions = [_]ext.actions.Action(Self){ // All of these will eventually take a target surface parameter. // For now all our targets originate from the focused surface. .init("new-split", actionNewSplit, s_variant_type), @@ -179,7 +179,7 @@ pub const SplitTree = extern struct { .init("zoom", actionZoom, null), }; - ext.addActionsAsGroup(Self, self, "split-tree", &actions); + ext.actions.addAsGroup(Self, self, "split-tree", &actions); } /// Create a new split in the given direction from the currently diff --git a/src/apprt/gtk-ng/class/surface.zig b/src/apprt/gtk-ng/class/surface.zig index 46f0a19c3..9fa82f4ee 100644 --- a/src/apprt/gtk-ng/class/surface.zig +++ b/src/apprt/gtk-ng/class/surface.zig @@ -1282,11 +1282,11 @@ pub const Surface = extern struct { } fn initActionMap(self: *Self) void { - const actions = [_]ext.Action(Self){ + const actions = [_]ext.actions.Action(Self){ .init("prompt-title", actionPromptTitle, null), }; - ext.addActionsAsGroup(Self, self, "surface", &actions); + ext.actions.addAsGroup(Self, self, "surface", &actions); } fn dispose(self: *Self) callconv(.c) void { diff --git a/src/apprt/gtk-ng/class/tab.zig b/src/apprt/gtk-ng/class/tab.zig index fad4b52e1..5f1cf50de 100644 --- a/src/apprt/gtk-ng/class/tab.zig +++ b/src/apprt/gtk-ng/class/tab.zig @@ -199,12 +199,12 @@ pub const Tab = extern struct { } fn initActionMap(self: *Self) void { - const actions = [_]ext.Action(Self){ + const actions = [_]ext.actions.Action(Self){ .init("close", actionClose, null), .init("ring-bell", actionRingBell, null), }; - ext.addActionsAsGroup(Self, self, "tab", &actions); + ext.actions.addAsGroup(Self, self, "tab", &actions); } //--------------------------------------------------------------- diff --git a/src/apprt/gtk-ng/class/window.zig b/src/apprt/gtk-ng/class/window.zig index 3758d859f..acba271c1 100644 --- a/src/apprt/gtk-ng/class/window.zig +++ b/src/apprt/gtk-ng/class/window.zig @@ -331,7 +331,7 @@ pub const Window = extern struct { /// Setup our action map. fn initActionMap(self: *Self) void { - const actions = [_]ext.Action(Self){ + const actions = [_]ext.actions.Action(Self){ .init("about", actionAbout, null), .init("close", actionClose, null), .init("close-tab", actionCloseTab, null), @@ -351,7 +351,7 @@ pub const Window = extern struct { .init("toggle-inspector", actionToggleInspector, null), }; - ext.addActions(Self, self, &actions); + ext.actions.add(Self, self, &actions); } /// Winproto backend for this window. diff --git a/src/apprt/gtk-ng/ext.zig b/src/apprt/gtk-ng/ext.zig index 75188535a..18587d9ca 100644 --- a/src/apprt/gtk-ng/ext.zig +++ b/src/apprt/gtk-ng/ext.zig @@ -12,6 +12,8 @@ const glib = @import("glib"); const gobject = @import("gobject"); const gtk = @import("gtk"); +pub const actions = @import("ext/actions.zig"); + /// Wrapper around `gobject.boxedCopy` to copy a boxed type `T`. pub fn boxedCopy(comptime T: type, ptr: *const T) *T { const copy = gobject.boxedCopy(T.getGObjectType(), ptr); @@ -61,149 +63,6 @@ pub fn gValueHolds(value_: ?*const gobject.Value, g_type: gobject.Type) bool { return gobject.typeCheckValueHolds(value, g_type) != 0; } -/// 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 { - pub const Callback = *const fn (*gio.SimpleAction, ?*glib.Variant, *T) callconv(.c) void; - - name: [:0]const u8, - callback: Callback, - parameter_type: ?*const glib.VariantType, - - /// 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) @This() { - comptime assert(gActionNameIsValid(name)); - - return .{ - .name = name, - .callback = callback, - .parameter_type = parameter_type, - }; - } - }; -} - -/// Add actions to a widget that implements gio.ActionMap. -pub fn addActions(comptime T: type, self: *T, actions: []const Action(T)) void { - addActionsToMap(T, self, self.as(gio.ActionMap), actions); -} - -/// Add actions to the given map. -pub fn addActionsToMap(comptime T: type, self: *T, map: *gio.ActionMap, actions: []const Action(T)) void { - for (actions) |entry| { - assert(gActionNameIsValid(entry.name)); - const 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 addActionsAsGroup(comptime T: type, self: *T, comptime name: [:0]const u8, actions: []const Action(T)) void { - 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(); - - addActionsToMap(T, self, group.as(gio.ActionMap), actions); - - self.as(gtk.Widget).insertActionGroup( - name, - group.as(gio.ActionGroup), - ); -} - -test "adding actions to an object" { - // This test requires a connection to an active display environment. - if (gtk.initCheck() == 0) return; - - 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), - }; - - addActionsAsGroup(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); +test { + _ = actions; } diff --git a/src/apprt/gtk-ng/ext/actions.zig b/src/apprt/gtk-ng/ext/actions.zig new file mode 100644 index 000000000..9f724c850 --- /dev/null +++ b/src/apprt/gtk-ng/ext/actions.zig @@ -0,0 +1,158 @@ +const std = @import("std"); + +const assert = std.debug.assert; +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 { + pub const Callback = *const fn (*gio.SimpleAction, ?*glib.Variant, *T) callconv(.c) void; + + name: [:0]const u8, + callback: Callback, + parameter_type: ?*const glib.VariantType, + + /// 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) @This() { + comptime assert(gActionNameIsValid(name)); + + return .{ + .name = name, + .callback = callback, + .parameter_type = parameter_type, + }; + } + }; +} + +/// 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 = 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)) void { + 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), + ); +} + +test "adding actions to an object" { + // This test requires a connection to an active display environment. + if (gtk.initCheck() == 0) return; + + 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); +}