diff --git a/src/apprt/gtk-ng/class.zig b/src/apprt/gtk-ng/class.zig index 170df1acb..a22b8771b 100644 --- a/src/apprt/gtk-ng/class.zig +++ b/src/apprt/gtk-ng/class.zig @@ -5,6 +5,7 @@ const glib = @import("glib"); const gobject = @import("gobject"); const gtk = @import("gtk"); +const ext = @import("ext.zig"); pub const Application = @import("class/application.zig").Application; pub const Window = @import("class/window.zig").Window; pub const Config = @import("class/config.zig").Config; @@ -79,7 +80,10 @@ pub fn Common( fn set(self: *Self, value: *const gobject.Value) void { const priv = private(self); if (@field(priv, name)) |v| { - glib.ext.destroy(v); + ext.boxedFree( + @typeInfo(@TypeOf(v)).pointer.child, + v, + ); } const T = @TypeOf(@field(priv, name)); diff --git a/src/apprt/gtk-ng/class/split_tree.zig b/src/apprt/gtk-ng/class/split_tree.zig index 968dbaa88..38f3d3536 100644 --- a/src/apprt/gtk-ng/class/split_tree.zig +++ b/src/apprt/gtk-ng/class/split_tree.zig @@ -13,6 +13,7 @@ const input = @import("../../../input.zig"); const CoreSurface = @import("../../../Surface.zig"); const gtk_version = @import("../gtk_version.zig"); const adw_version = @import("../adw_version.zig"); +const ext = @import("../ext.zig"); const gresource = @import("../build/gresource.zig"); const Common = @import("../class.zig").Common; const Config = @import("config.zig").Config; @@ -55,29 +56,39 @@ pub const SplitTree = extern struct { }, ); }; + + pub const tree = struct { + pub const name = "tree"; + const impl = gobject.ext.defineProperty( + name, + Self, + ?*Surface.Tree, + .{ + .nick = "Tree Model", + .blurb = "Underlying data model for the tree.", + .accessor = C.privateBoxedFieldAccessor("tree"), + }, + ); + }; }; const Private = struct { /// The tree datastructure containing all of our surface views. - tree: Surface.Tree, + tree: ?*Surface.Tree, pub var offset: c_int = 0; }; fn init(self: *Self, _: *Class) callconv(.c) void { gtk.Widget.initTemplate(self.as(gtk.Widget)); - - // Start with an empty split tree. - const priv = self.private(); - priv.tree = .empty; } //--------------------------------------------------------------- // Properties pub fn getIsEmpty(self: *Self) bool { - const priv = self.private(); - return priv.tree.isEmpty(); + const tree: *const Surface.Tree = self.private().tree orelse &.empty; + return tree.isEmpty(); } //--------------------------------------------------------------- @@ -97,8 +108,10 @@ pub const SplitTree = extern struct { fn finalize(self: *Self) callconv(.c) void { const priv = self.private(); - priv.tree.deinit(); - priv.tree = .empty; + if (priv.tree) |tree| { + ext.boxedFree(Surface.Tree, tree); + priv.tree = null; + } gobject.Object.virtual_methods.finalize.call( Class.parent, @@ -109,6 +122,14 @@ pub const SplitTree = extern struct { //--------------------------------------------------------------- // Signal handlers + fn propTree( + self: *Self, + _: *gobject.ParamSpec, + _: ?*anyopaque, + ) callconv(.c) void { + self.as(gobject.Object).notifyByPspec(properties.@"is-empty".impl.param_spec); + } + //--------------------------------------------------------------- // Class @@ -137,11 +158,13 @@ pub const SplitTree = extern struct { // Properties gobject.ext.registerProperties(class, &.{ properties.@"is-empty".impl, + properties.tree.impl, }); // Bindings // Template Callbacks + class.bindTemplateCallback("notify_tree", &propTree); // Signals diff --git a/src/apprt/gtk-ng/ui/1.5/split-tree.blp b/src/apprt/gtk-ng/ui/1.5/split-tree.blp index 0eebff7a6..66053fd3d 100644 --- a/src/apprt/gtk-ng/ui/1.5/split-tree.blp +++ b/src/apprt/gtk-ng/ui/1.5/split-tree.blp @@ -2,15 +2,28 @@ using Gtk 4.0; using Adw 1; template $GhosttySplitTree: Adw.Bin { - // This could be a lot more visually pleasing but in practice this doesn't - // ever happen at the time of writing this comment. A surface-less split - // tree always closes its parent. - Label { - visible: bind template.is-empty; - // Purposely not localized currently because this shouldn't really - // ever appear. When we have a situation it does appear, we may want - // to change the styling and text so I don't want to burden localizers - // to handle this yet. - label: "No surfaces."; + notify::tree => $notify_tree(); + + Box { + orientation: vertical; + + Box surface_box { + visible: bind template.is-empty inverted; + orientation: vertical; + hexpand: true; + vexpand: true; + } + + // This could be a lot more visually pleasing but in practice this doesn't + // ever happen at the time of writing this comment. A surface-less split + // tree always closes its parent. + Label { + visible: bind template.is-empty; + // Purposely not localized currently because this shouldn't really + // ever appear. When we have a situation it does appear, we may want + // to change the styling and text so I don't want to burden localizers + // to handle this yet. + label: "No surfaces."; + } } } diff --git a/src/datastruct/split_tree.zig b/src/datastruct/split_tree.zig index 759387073..23e9eae0c 100644 --- a/src/datastruct/split_tree.zig +++ b/src/datastruct/split_tree.zig @@ -1,5 +1,6 @@ const std = @import("std"); const assert = std.debug.assert; +const build_config = @import("../build_config.zig"); const ArenaAllocator = std.heap.ArenaAllocator; const Allocator = std.mem.Allocator; @@ -116,6 +117,25 @@ pub fn SplitTree(comptime V: type) type { self.* = undefined; } + /// Clone this tree, returning a new tree with the same nodes. + pub fn clone(self: *const Self, gpa: Allocator) Allocator.Error!Self { + // Create a new arena allocator for the clone. + var arena = ArenaAllocator.init(gpa); + errdefer arena.deinit(); + const alloc = arena.allocator(); + + // Allocate a new nodes array and copy the existing nodes into it. + const nodes = try alloc.dupe(Node, self.nodes); + + // Increase the reference count of all the views in the nodes. + try refNodes(gpa, nodes); + + return .{ + .arena = arena, + .nodes = nodes, + }; + } + /// Returns true if this is an empty tree. pub fn isEmpty(self: *const Self) bool { // An empty tree has no nodes. @@ -685,6 +705,51 @@ pub fn SplitTree(comptime V: type) type { else => @compileError("invalid view unref function"), } } + + /// Make this a valid gobject if we're in a GTK environment. + pub const getGObjectType = switch (build_config.app_runtime) { + .gtk, .@"gtk-ng" => @import("gobject").ext.defineBoxed( + Self, + .{ + // To get the type name we get the non-qualified type name + // of the view and append that to `GhosttySplitTree`. + .name = name: { + const type_name = @typeName(View); + const last = if (std.mem.lastIndexOfScalar( + u8, + type_name, + '.', + )) |idx| + type_name[idx + 1 ..] + else + type_name; + assert(last.len > 0); + break :name "GhosttySplitTree" ++ last; + }, + + .funcs = .{ + // The @ptrCast below is to workaround this bug: + // https://github.com/ianprime0509/zig-gobject/issues/115 + .copy = @ptrCast(&struct { + fn copy(self: *Self) callconv(.c) *Self { + const ptr = @import("glib").ext.create(Self); + const alloc = self.arena.child_allocator; + ptr.* = self.clone(alloc) catch @panic("oom"); + return ptr; + } + }.copy), + .free = @ptrCast(&struct { + fn free(self: *Self) callconv(.c) void { + self.deinit(); + @import("glib").ext.destroy(self); + } + }.free), + }, + }, + ), + + .none => void, + }; }; }