diff --git a/src/apprt/gtk-ng/class/split_tree.zig b/src/apprt/gtk-ng/class/split_tree.zig index 2e2af118a..4e7e55f00 100644 --- a/src/apprt/gtk-ng/class/split_tree.zig +++ b/src/apprt/gtk-ng/class/split_tree.zig @@ -1,6 +1,7 @@ const std = @import("std"); const build_config = @import("../../../build_config.zig"); const assert = std.debug.assert; +const Allocator = std.mem.Allocator; const adw = @import("adw"); const gio = @import("gio"); const glib = @import("glib"); @@ -36,6 +37,30 @@ pub const SplitTree = extern struct { }); pub const properties = struct { + /// The active surface is the surface that should be receiving all + /// surface-targeted actions. This is usually the focused surface, + /// but may also not be focused if the user has selected a non-surface + /// widget. + pub const @"active-surface" = struct { + pub const name = "active-surface"; + const impl = gobject.ext.defineProperty( + name, + Self, + ?*Surface, + .{ + .nick = "Active Surface", + .blurb = "The currently active surface.", + .accessor = gobject.ext.typedAccessor( + Self, + ?*Surface, + .{ + .getter = getActiveSurface, + }, + ), + }, + ); + }; + pub const @"has-surfaces" = struct { pub const name = "has-surfaces"; const impl = gobject.ext.defineProperty( @@ -98,11 +123,132 @@ pub const SplitTree = extern struct { fn init(self: *Self, _: *Class) callconv(.c) void { gtk.Widget.initTemplate(self.as(gtk.Widget)); + + // Initialize our actions + self.initActions(); + } + + fn initActions(self: *Self) void { + // The set of actions. Each action has (in order): + // [0] The action name + // [1] The callback function + // [2] The glib.VariantType of the parameter + // + // For action names: + // https://docs.gtk.org/gio/type_func.Action.name_is_valid.html + const actions = .{ + // All of these will eventually take a target surface parameter. + // For now all our targets originate from the focused surface. + .{ "new-left", actionNew, null }, + .{ "new-right", actionNew, null }, + .{ "new-up", actionNew, null }, + .{ "new-down", actionNew, null }, + }; + + // We need to 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(); + const map = group.as(gio.ActionMap); + inline for (actions) |entry| { + const action = gio.SimpleAction.new( + entry[0], + entry[2], + ); + defer action.unref(); + _ = gio.SimpleAction.signals.activate.connect( + action, + *Self, + entry[1], + self, + .{}, + ); + map.addAction(action.as(gio.Action)); + } + + self.as(gtk.Widget).insertActionGroup( + "split-tree", + group.as(gio.ActionGroup), + ); + } + + /// Create a new split in the given direction from the currently + /// active surface. + /// + /// If the tree is empty this will create a new tree with a new surface + /// and ignore the direction. + /// + /// The parent will be used as the parent of the surface regardless of + /// if that parent is in this split tree or not. This allows inheriting + /// surface properties from anywhere. + pub fn newSplit( + self: *Self, + direction: Surface.Tree.Split.Direction, + parent_: ?*Surface, + ) Allocator.Error!void { + const alloc = Application.default().allocator(); + + // Create our new surface. + const surface: *Surface = .new(); + defer surface.unref(); + _ = surface.refSink(); + + // Inherit properly if we were asked to. + if (parent_) |p| { + if (p.core()) |core| { + surface.setParent(core); + } + } + + // Create our tree + var single_tree = try Surface.Tree.init(alloc, surface); + defer single_tree.deinit(); + + // If we have no tree yet, then this becomes our tree and we're done. + const old_tree = self.getTree() orelse { + self.setTree(&single_tree); + return; + }; + + // The handle we create the split relative to. Today this is the active + // surface but this might be the handle of the given parent if we want. + const handle = self.getActiveSurfaceHandle() orelse 0; + + // Create our split! + var new_tree = try old_tree.split( + alloc, + handle, + direction, + &single_tree, + ); + defer new_tree.deinit(); + self.setTree(&new_tree); + + // Focus our new surface + surface.grabFocus(); } //--------------------------------------------------------------- // Properties + /// Get the currently active surface. See the "active-surface" property. + /// This does not ref the value. + pub fn getActiveSurface(self: *Self) ?*Surface { + const tree = self.getTree() orelse return null; + const handle = self.getActiveSurfaceHandle() orelse return null; + return tree.nodes[handle].leaf; + } + + fn getActiveSurfaceHandle(self: *Self) ?Surface.Tree.Node.Handle { + const tree = self.getTree() orelse return null; + var it = tree.iterator(); + while (it.next()) |entry| { + if (entry.view.getFocused()) return entry.handle; + } + + return null; + } + pub fn getHasSurfaces(self: *Self) bool { const tree: *const Surface.Tree = self.private().tree orelse &.empty; return !tree.isEmpty(); @@ -185,6 +331,20 @@ pub const SplitTree = extern struct { //--------------------------------------------------------------- // Signal handlers + pub fn actionNew( + _: *gio.SimpleAction, + parameter_: ?*glib.Variant, + self: *Self, + ) callconv(.c) void { + _ = parameter_; + self.newSplit( + .right, + self.getActiveSurface(), + ) catch |err| { + log.warn("new split failed error={}", .{err}); + }; + } + fn propTree( self: *Self, _: *gobject.ParamSpec, diff --git a/src/apprt/gtk-ng/class/tab.zig b/src/apprt/gtk-ng/class/tab.zig index a5c088d15..4b75701bf 100644 --- a/src/apprt/gtk-ng/class/tab.zig +++ b/src/apprt/gtk-ng/class/tab.zig @@ -36,7 +36,7 @@ pub const Tab = extern struct { }); pub const properties = struct { - /// The active surface is the focus that should be receiving all + /// The active surface is the surface that should be receiving all /// surface-targeted actions. This is usually the focused surface, /// but may also not be focused if the user has selected a non-surface /// widget. @@ -164,23 +164,15 @@ pub const Tab = extern struct { .{}, ); - // A tab always starts with a single surface. - const surface: *Surface = .new(); - defer surface.unref(); - _ = surface.refSink(); - const alloc = Application.default().allocator(); - if (Surface.Tree.init(alloc, surface)) |tree| { - priv.split_tree.setTree(&tree); - - // Hacky because we need a non-const result. - var mut = tree; - mut.deinit(); - } else |_| { - // TODO: We should make our "no surfaces" state more aesthetically - // pleasing and show something like an "Oops, something went wrong" - // message. For now, this is incredibly unlikely. - @panic("oom"); - } + // Create our initial surface in the split tree. + priv.split_tree.newSplit(.right, null) catch |err| switch (err) { + error.OutOfMemory => { + // TODO: We should make our "no surfaces" state more aesthetically + // pleasing and show something like an "Oops, something went wrong" + // message. For now, this is incredibly unlikely. + @panic("oom"); + }, + }; } fn connectSurfaceHandlers( @@ -232,13 +224,7 @@ pub const Tab = extern struct { /// Get the currently active surface. See the "active-surface" property. /// This does not ref the value. pub fn getActiveSurface(self: *Self) ?*Surface { - const tree = self.getSurfaceTree() orelse return null; - var it = tree.iterator(); - while (it.next()) |entry| { - if (entry.view.getFocused()) return entry.view; - } - - return null; + return self.getSplitTree().getActiveSurface(); } /// Get the surface tree of this tab. diff --git a/src/apprt/gtk-ng/ui/1.2/surface.blp b/src/apprt/gtk-ng/ui/1.2/surface.blp index e671a0d82..23499c7f3 100644 --- a/src/apprt/gtk-ng/ui/1.2/surface.blp +++ b/src/apprt/gtk-ng/ui/1.2/surface.blp @@ -172,22 +172,22 @@ menu context_menu_model { item { label: _("Split Up"); - action: "win.split-up"; + action: "split-tree.new-up"; } item { label: _("Split Down"); - action: "win.split-down"; + action: "split-tree.new-down"; } item { label: _("Split Left"); - action: "win.split-left"; + action: "split-tree.new-left"; } item { label: _("Split Right"); - action: "win.split-right"; + action: "split-tree.new-right"; } }