apprt/gtk-ng: setup split tree property

pull/8165/head
Mitchell Hashimoto 2025-08-06 10:34:25 -07:00
parent fa08434b28
commit 70b050ebb4
No known key found for this signature in database
GPG Key ID: 523D5DC389D273BC
4 changed files with 125 additions and 20 deletions

View File

@ -5,6 +5,7 @@ const glib = @import("glib");
const gobject = @import("gobject"); const gobject = @import("gobject");
const gtk = @import("gtk"); const gtk = @import("gtk");
const ext = @import("ext.zig");
pub const Application = @import("class/application.zig").Application; pub const Application = @import("class/application.zig").Application;
pub const Window = @import("class/window.zig").Window; pub const Window = @import("class/window.zig").Window;
pub const Config = @import("class/config.zig").Config; pub const Config = @import("class/config.zig").Config;
@ -79,7 +80,10 @@ pub fn Common(
fn set(self: *Self, value: *const gobject.Value) void { fn set(self: *Self, value: *const gobject.Value) void {
const priv = private(self); const priv = private(self);
if (@field(priv, name)) |v| { if (@field(priv, name)) |v| {
glib.ext.destroy(v); ext.boxedFree(
@typeInfo(@TypeOf(v)).pointer.child,
v,
);
} }
const T = @TypeOf(@field(priv, name)); const T = @TypeOf(@field(priv, name));

View File

@ -13,6 +13,7 @@ const input = @import("../../../input.zig");
const CoreSurface = @import("../../../Surface.zig"); const CoreSurface = @import("../../../Surface.zig");
const gtk_version = @import("../gtk_version.zig"); const gtk_version = @import("../gtk_version.zig");
const adw_version = @import("../adw_version.zig"); const adw_version = @import("../adw_version.zig");
const ext = @import("../ext.zig");
const gresource = @import("../build/gresource.zig"); const gresource = @import("../build/gresource.zig");
const Common = @import("../class.zig").Common; const Common = @import("../class.zig").Common;
const Config = @import("config.zig").Config; 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 { const Private = struct {
/// The tree datastructure containing all of our surface views. /// The tree datastructure containing all of our surface views.
tree: Surface.Tree, tree: ?*Surface.Tree,
pub var offset: c_int = 0; pub var offset: c_int = 0;
}; };
fn init(self: *Self, _: *Class) callconv(.c) void { fn init(self: *Self, _: *Class) callconv(.c) void {
gtk.Widget.initTemplate(self.as(gtk.Widget)); gtk.Widget.initTemplate(self.as(gtk.Widget));
// Start with an empty split tree.
const priv = self.private();
priv.tree = .empty;
} }
//--------------------------------------------------------------- //---------------------------------------------------------------
// Properties // Properties
pub fn getIsEmpty(self: *Self) bool { pub fn getIsEmpty(self: *Self) bool {
const priv = self.private(); const tree: *const Surface.Tree = self.private().tree orelse &.empty;
return priv.tree.isEmpty(); return tree.isEmpty();
} }
//--------------------------------------------------------------- //---------------------------------------------------------------
@ -97,8 +108,10 @@ pub const SplitTree = extern struct {
fn finalize(self: *Self) callconv(.c) void { fn finalize(self: *Self) callconv(.c) void {
const priv = self.private(); const priv = self.private();
priv.tree.deinit(); if (priv.tree) |tree| {
priv.tree = .empty; ext.boxedFree(Surface.Tree, tree);
priv.tree = null;
}
gobject.Object.virtual_methods.finalize.call( gobject.Object.virtual_methods.finalize.call(
Class.parent, Class.parent,
@ -109,6 +122,14 @@ pub const SplitTree = extern struct {
//--------------------------------------------------------------- //---------------------------------------------------------------
// Signal handlers // Signal handlers
fn propTree(
self: *Self,
_: *gobject.ParamSpec,
_: ?*anyopaque,
) callconv(.c) void {
self.as(gobject.Object).notifyByPspec(properties.@"is-empty".impl.param_spec);
}
//--------------------------------------------------------------- //---------------------------------------------------------------
// Class // Class
@ -137,11 +158,13 @@ pub const SplitTree = extern struct {
// Properties // Properties
gobject.ext.registerProperties(class, &.{ gobject.ext.registerProperties(class, &.{
properties.@"is-empty".impl, properties.@"is-empty".impl,
properties.tree.impl,
}); });
// Bindings // Bindings
// Template Callbacks // Template Callbacks
class.bindTemplateCallback("notify_tree", &propTree);
// Signals // Signals

View File

@ -2,15 +2,28 @@ using Gtk 4.0;
using Adw 1; using Adw 1;
template $GhosttySplitTree: Adw.Bin { template $GhosttySplitTree: Adw.Bin {
// This could be a lot more visually pleasing but in practice this doesn't notify::tree => $notify_tree();
// ever happen at the time of writing this comment. A surface-less split
// tree always closes its parent. Box {
Label { orientation: vertical;
visible: bind template.is-empty;
// Purposely not localized currently because this shouldn't really Box surface_box {
// ever appear. When we have a situation it does appear, we may want visible: bind template.is-empty inverted;
// to change the styling and text so I don't want to burden localizers orientation: vertical;
// to handle this yet. hexpand: true;
label: "No surfaces."; 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.";
}
} }
} }

View File

@ -1,5 +1,6 @@
const std = @import("std"); const std = @import("std");
const assert = std.debug.assert; const assert = std.debug.assert;
const build_config = @import("../build_config.zig");
const ArenaAllocator = std.heap.ArenaAllocator; const ArenaAllocator = std.heap.ArenaAllocator;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
@ -116,6 +117,25 @@ pub fn SplitTree(comptime V: type) type {
self.* = undefined; 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. /// Returns true if this is an empty tree.
pub fn isEmpty(self: *const Self) bool { pub fn isEmpty(self: *const Self) bool {
// An empty tree has no nodes. // An empty tree has no nodes.
@ -685,6 +705,51 @@ pub fn SplitTree(comptime V: type) type {
else => @compileError("invalid view unref function"), 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,
};
}; };
} }