From a7865d79ea2e8df1a2fd4e3bba2839a61cee41c4 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 6 Aug 2025 10:53:18 -0700 Subject: [PATCH] apprt/gtk-ng: render a single artificial split --- src/apprt/gtk-ng/class/split_tree.zig | 112 ++++++++++++++++++++++++- src/apprt/gtk-ng/class/surface.zig | 1 + src/apprt/gtk-ng/class/tab.zig | 12 +++ src/apprt/gtk-ng/ui/1.5/split-tree.blp | 3 +- src/apprt/gtk-ng/ui/1.5/tab.blp | 2 +- 5 files changed, 126 insertions(+), 4 deletions(-) diff --git a/src/apprt/gtk-ng/class/split_tree.zig b/src/apprt/gtk-ng/class/split_tree.zig index 38f3d3536..3cd03e810 100644 --- a/src/apprt/gtk-ng/class/split_tree.zig +++ b/src/apprt/gtk-ng/class/split_tree.zig @@ -66,16 +66,39 @@ pub const SplitTree = extern struct { .{ .nick = "Tree Model", .blurb = "Underlying data model for the tree.", - .accessor = C.privateBoxedFieldAccessor("tree"), + .accessor = .{ + .getter = getTreeValue, + .setter = setTreeValue, + }, }, ); }; }; + pub const signals = struct { + /// Emitted whenever the tree property is about to change. + /// + /// The new value is given as the signal parameter. The old value + /// can still be retrieved from the tree property. + pub const @"tree-will-change" = struct { + pub const name = "tree-change"; + pub const connect = impl.connect; + const impl = gobject.ext.defineSignal( + name, + Self, + &.{?*const Surface.Tree}, + void, + ); + }; + }; + const Private = struct { /// The tree datastructure containing all of our surface views. tree: ?*Surface.Tree, + // Template bindings + tree_bin: *adw.Bin, + pub var offset: c_int = 0; }; @@ -91,6 +114,52 @@ pub const SplitTree = extern struct { return tree.isEmpty(); } + /// Get the tree data model that we're showing in this widget. This + /// does not clone the tree. + pub fn getTree(self: *Self) ?*Surface.Tree { + return self.private().tree; + } + + /// Set the tree data model that we're showing in this widget. This + /// will clone the given tree. + pub fn setTree(self: *Self, tree: ?*const Surface.Tree) void { + const priv = self.private(); + + // Emit the signal so that handlers can witness both the before and + // after values of the tree. + signals.@"tree-will-change".impl.emit( + self, + null, + .{tree}, + null, + ); + + if (priv.tree) |old_tree| { + ext.boxedFree(Surface.Tree, old_tree); + priv.tree = null; + } + + if (tree) |new_tree| { + priv.tree = ext.boxedCopy(Surface.Tree, new_tree); + } + + self.as(gobject.Object).notifyByPspec(properties.tree.impl.param_spec); + } + + fn getTreeValue(self: *Self, value: *gobject.Value) void { + gobject.ext.Value.set( + value, + self.private().tree, + ); + } + + fn setTreeValue(self: *Self, value: *const gobject.Value) void { + self.setTree(gobject.ext.Value.get( + value, + ?*Surface.Tree, + )); + } + //--------------------------------------------------------------- // Virtual methods @@ -127,9 +196,48 @@ pub const SplitTree = extern struct { _: *gobject.ParamSpec, _: ?*anyopaque, ) callconv(.c) void { + const priv = self.private(); + const tree: *const Surface.Tree = self.private().tree orelse &.empty; + + // Reset our widget tree. + priv.tree_bin.setChild(null); + if (!tree.isEmpty()) { + priv.tree_bin.setChild(buildTree(tree, 0)); + } + + // Dependent properties self.as(gobject.Object).notifyByPspec(properties.@"is-empty".impl.param_spec); } + /// Builds the widget tree associated with a surface split tree. + /// + /// The final returned widget is expected to be a floating reference, + /// ready to be attached to a parent widget. + fn buildTree( + tree: *const Surface.Tree, + current: Surface.Tree.Node.Handle, + ) *gtk.Widget { + switch (tree.nodes[current]) { + .leaf => |v| { + // We have to setup our signal handlers. + return v.as(gtk.Widget); + }, + + .split => |s| return gobject.ext.newInstance( + gtk.Paned, + .{ + .orientation = @as(gtk.Orientation, switch (s.layout) { + .horizontal => .horizontal, + .vertical => .vertical, + }), + .@"start-child" = buildTree(tree, s.left), + .@"end-child" = buildTree(tree, s.right), + // TODO: position/ratio + }, + ).as(gtk.Widget), + } + } + //--------------------------------------------------------------- // Class @@ -162,11 +270,13 @@ pub const SplitTree = extern struct { }); // Bindings + class.bindTemplateChildPrivate("tree_bin", .{}); // Template Callbacks class.bindTemplateCallback("notify_tree", &propTree); // Signals + signals.@"tree-will-change".impl.register(.{}); // Virtual methods gobject.Object.virtual_methods.dispose.implement(class, &dispose); diff --git a/src/apprt/gtk-ng/class/surface.zig b/src/apprt/gtk-ng/class/surface.zig index f26783747..35b4eaf88 100644 --- a/src/apprt/gtk-ng/class/surface.zig +++ b/src/apprt/gtk-ng/class/surface.zig @@ -2302,6 +2302,7 @@ pub const Surface = extern struct { const C = Common(Self, Private); pub const as = C.as; pub const ref = C.ref; + pub const refSink = C.refSink; pub const unref = C.unref; const private = C.private; diff --git a/src/apprt/gtk-ng/class/tab.zig b/src/apprt/gtk-ng/class/tab.zig index b343ba248..0619b2de8 100644 --- a/src/apprt/gtk-ng/class/tab.zig +++ b/src/apprt/gtk-ng/class/tab.zig @@ -118,6 +118,7 @@ pub const Tab = extern struct { surface_bindings: *gobject.BindingGroup, // Template bindings + split_tree: *SplitTree, surface: *Surface, pub var offset: c_int = 0; @@ -161,6 +162,16 @@ pub const Tab = extern struct { // We need to do this so that the title initializes properly, // I think because its a dynamic getter. self.as(gobject.Object).notifyByPspec(properties.@"active-surface".impl.param_spec); + + // Setup our initial split tree. + // TODO: Probably make this a property + const surface: *Surface = .new(); + defer surface.unref(); + _ = surface.refSink(); + const alloc = Application.default().allocator(); + var tree = Surface.Tree.init(alloc, surface) catch unreachable; + defer tree.deinit(); + priv.split_tree.setTree(&tree); } //--------------------------------------------------------------- @@ -271,6 +282,7 @@ pub const Tab = extern struct { }); // Bindings + class.bindTemplateChildPrivate("split_tree", .{}); class.bindTemplateChildPrivate("surface", .{}); // Template Callbacks 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 66053fd3d..2ce4b3f10 100644 --- a/src/apprt/gtk-ng/ui/1.5/split-tree.blp +++ b/src/apprt/gtk-ng/ui/1.5/split-tree.blp @@ -7,9 +7,8 @@ template $GhosttySplitTree: Adw.Bin { Box { orientation: vertical; - Box surface_box { + Adw.Bin tree_bin { visible: bind template.is-empty inverted; - orientation: vertical; hexpand: true; vexpand: true; } diff --git a/src/apprt/gtk-ng/ui/1.5/tab.blp b/src/apprt/gtk-ng/ui/1.5/tab.blp index d1c7737f6..fc20962e4 100644 --- a/src/apprt/gtk-ng/ui/1.5/tab.blp +++ b/src/apprt/gtk-ng/ui/1.5/tab.blp @@ -14,5 +14,5 @@ template $GhosttyTab: Box { close-request => $surface_close_request(); } - $GhosttySplitTree {} + $GhosttySplitTree split_tree {} }