From c6e7e9e4e2fa27e1a5dea57e878eb1146faaf62b Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Sat, 8 Aug 2026 17:12:48 +0800 Subject: [PATCH 1/3] config: add `drag-handle` --- src/config/Config.zig | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/config/Config.zig b/src/config/Config.zig index 7fa282437..b96300d14 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -2345,6 +2345,30 @@ keybind: Keybinds = .{}, /// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color. @"window-titlebar-foreground": ?Color = null, +/// Controls when drag handles are shown over splits, +/// allowing splits to be rearranged with mouse controls. +/// +/// Valid values: +/// +/// - `always` +/// +/// Always display the drag handle, even when there's only one split. +/// +/// - `auto` *(default)* +/// +/// Automatically show and hide the drag handle. The handle is only +/// shown when there are two or more splits present. +/// +/// - `never` +/// +/// Never show the drag handle. Splits then cannot be rearranged with +/// mouse controls. +/// +/// Available since: 1.4.0. +/// +/// Currently only supported on Linux (GTK). +@"drag-handle": DragHandle = .auto, + /// This controls when resize overlays are shown. Resize overlays are a /// transient popup that shows the size of the terminal while the surfaces are /// being resized. The possible options are: @@ -9297,6 +9321,13 @@ pub const WindowShowTabBar = enum { never, }; +/// See drag-handle +pub const DragHandle = enum { + always, + auto, + never, +}; + /// See resize-overlay pub const ResizeOverlay = enum { always, From 65a3e666efdda5191051f94a5414eb2cf516245c Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Sat, 8 Aug 2026 17:25:06 +0800 Subject: [PATCH 2/3] gtk: drag overlay toggle --- src/apprt/gtk/class/surface.zig | 17 ++++++++++++++++- src/apprt/gtk/ui/1.2/surface.blp | 1 + 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index e9aea6d51..5937fab76 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -3650,6 +3650,21 @@ pub const Surface = extern struct { }; } + fn closureShouldDragHandleBeShown( + _: *Self, + config_: ?*Config, + is_split: c_int, + ) callconv(.c) c_int { + const config = config_ orelse return @intFromBool(false); + + const shown = switch (config.get().@"drag-handle") { + .always => true, + .auto => is_split != 0, + .never => false, + }; + return @intFromBool(shown); + } + fn surfaceDragPrepare( src: *gtk.DragSource, x: f64, @@ -3722,7 +3737,6 @@ pub const Surface = extern struct { const dropped = self.core().?.app.findSurfaceByID(dropped_id) orelse return; const from = dropped.rt_surface.gobj(); - // TODO: Find a better way to access the split tree from here const st = ext.getAncestor( SplitTree, self.as(gtk.Widget), @@ -3892,6 +3906,7 @@ pub const Surface = extern struct { class.bindTemplateCallback("search_changed", &searchChanged); class.bindTemplateCallback("search_next_match", &searchNextMatch); class.bindTemplateCallback("search_previous_match", &searchPreviousMatch); + class.bindTemplateCallback("should_drag_handle_be_shown", &closureShouldDragHandleBeShown); class.bindTemplateCallback("surface_drag_prepare", &surfaceDragPrepare); class.bindTemplateCallback("surface_drag_begin", &surfaceDragBegin); class.bindTemplateCallback("surface_drop", &surfaceDrop); diff --git a/src/apprt/gtk/ui/1.2/surface.blp b/src/apprt/gtk/ui/1.2/surface.blp index b7d07cb3e..fc14fd3b5 100644 --- a/src/apprt/gtk/ui/1.2/surface.blp +++ b/src/apprt/gtk/ui/1.2/surface.blp @@ -232,6 +232,7 @@ Overlay terminal_page { CenterBox drag_handle { halign: fill; valign: start; + visible: bind $should_drag_handle_be_shown(template.config, template.is-split) as ; styles [ "drag-handle", From 850ca8c7b1c69078621aea638bb6564d7b3d53b5 Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Sun, 9 Aug 2026 12:02:44 +0800 Subject: [PATCH 3/3] gtk: rebind is-split after moving split cross-tree MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It turns out we never unbound the split from its original tree after moving, which means `is-split` in particular is desynced and leads to hilarious artifacts like how `unfocused-split-*` options just stop working properly. I only realized this is a thing after the naïve drag handle config option didn't work properly. Fun! --- src/apprt/gtk/class/split_tree.zig | 10 ++++------ src/apprt/gtk/class/surface.zig | 13 +++++++++++++ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/apprt/gtk/class/split_tree.zig b/src/apprt/gtk/class/split_tree.zig index 473ae3daf..660c7c61c 100644 --- a/src/apprt/gtk/class/split_tree.zig +++ b/src/apprt/gtk/class/split_tree.zig @@ -239,12 +239,7 @@ pub const SplitTree = extern struct { } // Bind is-split property for new surface - _ = self.as(gobject.Object).bindProperty( - "is-split", - surface.as(gobject.Object), - "is-split", - .{ .sync_create = true }, - ); + surface.bindIsSplit(self); // Create our tree var single_tree = try Surface.Tree.init(alloc, surface); @@ -452,6 +447,9 @@ pub const SplitTree = extern struct { // Finally, set the final tree structures for both tree widgets source_tree_widget.setTree(&new_source_tree); self.setTree(&after_split); + + // Re-bind vital properties like `is-split` + source.bindIsSplit(self); } } diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index 5937fab76..cc7726617 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -691,6 +691,7 @@ pub const Surface = extern struct { // True if the current surface is a split, this is used to apply // unfocused-split-* options is_split: bool = false, + is_split_binding: ?*gobject.Binding = null, action_group: ?*gio.SimpleActionGroup = null, @@ -847,6 +848,18 @@ pub const Surface = extern struct { return @intFromBool(config.@"bell-features".border); } + pub fn bindIsSplit(self: *Self, tree: *SplitTree) void { + const priv = self.private(); + if (priv.is_split_binding) |bind| bind.unbind(); + + priv.is_split_binding = tree.as(gobject.Object).bindProperty( + "is-split", + self.as(gobject.Object), + "is-split", + .{ .sync_create = true }, + ); + } + /// Callback used to determine whether unfocused-split-fill / unfocused-split-opacity /// should be applied to the surface fn closureShouldUnfocusedSplitBeShown(