aboutsummaryrefslogtreecommitdiffstats
path: root/vendor/libghostty-vt/src/apprt/gtk/ext/actions.zig
blob: 6ba9c678b0b380f7453cf03ffe16a6c05f920aba (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
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 = expected: {
        const rng_impl: std.Random.IoSource = .{ .io = testing.io };
        const rng = rng_impl.interface();
        break :expected rng.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) {}
}