From 46f8a300d10110a7d84e316a7844af2d6e2a9568 Mon Sep 17 00:00:00 2001 From: Bruno Volpato Date: Sat, 16 May 2026 15:14:11 -0400 Subject: [PATCH 1/3] fix: allow drag-dropping tabs into single-pane workspaces The drag-and-drop plumbing for moving tabs between workspaces was already in place, but dropping a tab onto a workspace with only a single pane (no splits) silently failed. Root cause: handle_tab_drop_to_workspace calls find_leaf_pane to locate a target pane widget inside the destination workspace. workspace.root is a SplitTreeContainer bin (a gtk::Box). For workspaces with splits, the bin contains a gtk::Paned, so find_leaf_pane correctly descends into it. But for single-pane workspaces, the bin contains the pane widget directly, and find_leaf_pane was treating the bin itself as a leaf pane. This caused pane::move_tab_to_pane to fail because the bin doesn't have PaneInternals. Fix: in find_leaf_pane, when we encounter a gtk::Box that is not a pane widget, descend into its first child instead of returning the box as a leaf. --- rust/limux-host-linux/src/window.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/rust/limux-host-linux/src/window.rs b/rust/limux-host-linux/src/window.rs index d089b3b5..9459b372 100644 --- a/rust/limux-host-linux/src/window.rs +++ b/rust/limux-host-linux/src/window.rs @@ -5688,8 +5688,16 @@ fn find_leaf_pane(widget: >k::Widget, axis: gtk::Orientation, prefer_start: bo Some(c) => find_leaf_pane(&c, axis, prefer_start), None => widget.clone(), } + } else if let Some(box_widget) = widget.downcast_ref::() { + // A gtk::Box may be a pane widget (leaf) or a SplitTreeContainer bin + // wrapping a single pane or paned. Descend into non-pane boxes. + if !pane::is_pane_widget(widget) { + if let Some(first_child) = box_widget.first_child() { + return find_leaf_pane(&first_child, axis, prefer_start); + } + } + widget.clone() } else { - // Leaf pane — this is a pane gtk::Box widget.clone() } } From 98165b71e9a57cffbc8e50cdea5c8d74fcdad15b Mon Sep 17 00:00:00 2001 From: Bruno Volpato Date: Sat, 16 May 2026 15:42:38 -0400 Subject: [PATCH 2/3] test: add regression test for find_leaf_pane with non-pane boxes --- rust/limux-host-linux/src/window.rs | 50 ++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/rust/limux-host-linux/src/window.rs b/rust/limux-host-linux/src/window.rs index 9459b372..aea02225 100644 --- a/rust/limux-host-linux/src/window.rs +++ b/rust/limux-host-linux/src/window.rs @@ -5884,7 +5884,7 @@ mod tests { desktop_notification_action_from_signal, desktop_notification_actions, desktop_notification_activation_token_from_signal, desktop_notification_closed_id_from_signal, desktop_notification_id_from_response, - directional_neighbor_score, favorites_prefix_len, font_size_after_delta, + directional_neighbor_score, favorites_prefix_len, find_leaf_pane, font_size_after_delta, ghostty_prefers_dark, gtk_system_prefers_dark_from_raw, next_active_workspace_index, pane_create_split_placement, queue_session_save_request, resolve_pane_create_source_id, resolved_system_prefers_dark, sanitize_background_opacity, @@ -6703,4 +6703,52 @@ mod tests { assert!(error.ends_with(" is not a folder")); } + + #[test] + fn find_leaf_pane_descends_into_non_pane_boxes() { + use gtk4::prelude::{BoxExt, Cast, WidgetExt}; + + // GTK widget tests need a display. Skip silently on headless CI. + if super::gtk::gdk::Display::default().is_none() { + return; + } + gtk4::init().unwrap(); + + // A plain Box wrapping a single child (simulates SplitTreeContainer bin) + let bin = gtk4::Box::new(gtk4::Orientation::Vertical, 0); + let inner_pane = gtk4::Box::new(gtk4::Orientation::Vertical, 0); + inner_pane.add_css_class("limux-pane-header"); + bin.append(&inner_pane); + + let leaf = find_leaf_pane(&bin.upcast(), gtk4::Orientation::Horizontal, true); + assert!( + leaf.is_ancestor(&inner_pane), + "find_leaf_pane should descend through a non-pane Box and return the inner pane" + ); + + // A Box that IS a pane widget should be returned as-is + let pane = gtk4::Box::new(gtk4::Orientation::Vertical, 0); + let header = gtk4::Box::new(gtk4::Orientation::Horizontal, 0); + header.add_css_class("limux-pane-header"); + pane.append(&header); + + let leaf = find_leaf_pane(&pane.clone().upcast(), gtk4::Orientation::Horizontal, true); + assert!( + leaf.is_ancestor(&pane), + "find_leaf_pane should treat a pane Box as a leaf" + ); + + // A Paned should descend to its actual leaf + let paned = gtk4::Paned::new(gtk4::Orientation::Horizontal); + let left_pane = gtk4::Box::new(gtk4::Orientation::Vertical, 0); + left_pane.add_css_class("limux-pane-header"); + paned.set_start_child(Some(&left_pane)); + paned.set_end_child(Some(>k4::Box::new(gtk4::Orientation::Vertical, 0))); + + let leaf = find_leaf_pane(&paned.upcast(), gtk4::Orientation::Horizontal, true); + assert!( + leaf.is_ancestor(&left_pane), + "find_leaf_pane should descend through a Paned to its leaf" + ); + } } From 64347a54803ef183f9442943abaa950cf9814304 Mon Sep 17 00:00:00 2001 From: Bruno Volpato Date: Sat, 16 May 2026 20:02:43 -0400 Subject: [PATCH 3/3] test: strengthen find_leaf_pane regression --- rust/limux-host-linux/src/window.rs | 47 ++++++++++++++++------------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/rust/limux-host-linux/src/window.rs b/rust/limux-host-linux/src/window.rs index aea02225..a36c1188 100644 --- a/rust/limux-host-linux/src/window.rs +++ b/rust/limux-host-linux/src/window.rs @@ -6709,45 +6709,50 @@ mod tests { use gtk4::prelude::{BoxExt, Cast, WidgetExt}; // GTK widget tests need a display. Skip silently on headless CI. - if super::gtk::gdk::Display::default().is_none() { + if gtk4::init().is_err() { return; } - gtk4::init().unwrap(); - // A plain Box wrapping a single child (simulates SplitTreeContainer bin) + let make_pane = || { + let pane = gtk4::Box::new(gtk4::Orientation::Vertical, 0); + let header = gtk4::Box::new(gtk4::Orientation::Horizontal, 0); + header.add_css_class("limux-pane-header"); + pane.append(&header); + pane + }; + + // A plain Box wrapping a single child simulates a SplitTreeContainer bin. let bin = gtk4::Box::new(gtk4::Orientation::Vertical, 0); - let inner_pane = gtk4::Box::new(gtk4::Orientation::Vertical, 0); - inner_pane.add_css_class("limux-pane-header"); + let inner_pane = make_pane(); bin.append(&inner_pane); let leaf = find_leaf_pane(&bin.upcast(), gtk4::Orientation::Horizontal, true); - assert!( - leaf.is_ancestor(&inner_pane), - "find_leaf_pane should descend through a non-pane Box and return the inner pane" + assert_eq!( + leaf, + inner_pane.clone().upcast::(), + "find_leaf_pane should descend through a non-pane Box" ); - // A Box that IS a pane widget should be returned as-is - let pane = gtk4::Box::new(gtk4::Orientation::Vertical, 0); - let header = gtk4::Box::new(gtk4::Orientation::Horizontal, 0); - header.add_css_class("limux-pane-header"); - pane.append(&header); + // A Box that is a pane widget should be returned as-is. + let pane = make_pane(); let leaf = find_leaf_pane(&pane.clone().upcast(), gtk4::Orientation::Horizontal, true); - assert!( - leaf.is_ancestor(&pane), + assert_eq!( + leaf, + pane.clone().upcast::(), "find_leaf_pane should treat a pane Box as a leaf" ); - // A Paned should descend to its actual leaf + // A Paned should descend to its actual leaf. let paned = gtk4::Paned::new(gtk4::Orientation::Horizontal); - let left_pane = gtk4::Box::new(gtk4::Orientation::Vertical, 0); - left_pane.add_css_class("limux-pane-header"); + let left_pane = make_pane(); paned.set_start_child(Some(&left_pane)); - paned.set_end_child(Some(>k4::Box::new(gtk4::Orientation::Vertical, 0))); + paned.set_end_child(Some(&make_pane())); let leaf = find_leaf_pane(&paned.upcast(), gtk4::Orientation::Horizontal, true); - assert!( - leaf.is_ancestor(&left_pane), + assert_eq!( + leaf, + left_pane.clone().upcast::(), "find_leaf_pane should descend through a Paned to its leaf" ); }