From 01ce7162a48dfca1026ea4e0da26afcb312564d6 Mon Sep 17 00:00:00 2001 From: aiongg Date: Fri, 23 Oct 2026 06:11:03 +0000 Subject: [PATCH] docking: Floating::stack is public, and four-way handles take the pointer only Floating::stack builds a window from groups stacked top to bottom, so an app's own stacked layouts migrate without building the split chain. The four-way handle no longer takes keyboard focus: it has no label or key action, and the two dividers it joins take arrow keys. --- crates/ui/src/docking/floating_tests.rs | 4 ++-- crates/ui/src/docking/group_tests.rs | 2 +- crates/ui/src/docking/iconic_tests.rs | 2 +- crates/ui/src/docking/model.rs | 8 ++++---- crates/ui/src/docking/model_tests.rs | 6 +++--- crates/ui/src/docking/render/mod.rs | 3 ++- docs/ui-docking.md | 5 +++-- 7 files changed, 16 insertions(+), 14 deletions(-) diff --git a/crates/ui/src/docking/floating_tests.rs b/crates/ui/src/docking/floating_tests.rs index f930aed4..9217965e 100644 --- a/crates/ui/src/docking/floating_tests.rs +++ b/crates/ui/src/docking/floating_tests.rs @@ -38,7 +38,7 @@ fn workspace() -> Layout { second: Box::new(tabs(&[0], 0)), }), floating: vec![ - Floating::stack(vec![Group::new(vec![5], 0), Group::new(vec![6, 7], 1)], [300.0, 80.0, 240.0, 300.0]), + Floating::stack(vec![Group::new(vec![5], 0), Group::new(vec![6, 7], 1)], [300.0, 80.0, 240.0, 300.0]).unwrap(), Floating::single(vec![8], 0, [560.0, 300.0, 200.0, 200.0]), ], } @@ -678,7 +678,7 @@ fn undock_panel_group_floats_one_group_of_a_floating_window() { use egui_kittest::{Harness, kittest::Queryable}; let stacked = || Layout { root: Some(tabs(&[1], 0)), - floating: vec![Floating::stack(vec![Group::new(vec![5], 0), Group::new(vec![6, 7], 0)], [300.0, 80.0, 300.0, 400.0])], + floating: vec![Floating::stack(vec![Group::new(vec![5], 0), Group::new(vec![6, 7], 0)], [300.0, 80.0, 300.0, 400.0]).unwrap()], }; type State = (Content, Layout, Vec>); let mut h = Harness::builder().with_size(vec2(800.0, 600.0)).build_ui_state( diff --git a/crates/ui/src/docking/group_tests.rs b/crates/ui/src/docking/group_tests.rs index a11e2796..f6027aed 100644 --- a/crates/ui/src/docking/group_tests.rs +++ b/crates/ui/src/docking/group_tests.rs @@ -551,7 +551,7 @@ fn drag_bar_menu_has_the_group_items_and_opens_from_the_keyboard() { #[test] fn a_group_dragged_out_of_a_floating_stack_tears_out_alone() { - let stack = Floating::stack(vec![Group::new(vec![5], 0), Group::new(vec![6, 7], 1)], [80.0, 70.0, 300.0, 400.0]); + let stack = Floating::stack(vec![Group::new(vec![5], 0), Group::new(vec![6, 7], 1)], [80.0, 70.0, 300.0, 400.0]).unwrap(); let layout = Layout { root: Some(tabs(&[1], 0)), floating: vec![stack] }; let mut d = Driver::new(layout); let from = d.rect("drag-bar", 6).center(); diff --git a/crates/ui/src/docking/iconic_tests.rs b/crates/ui/src/docking/iconic_tests.rs index bc946daf..c648e43e 100644 --- a/crates/ui/src/docking/iconic_tests.rs +++ b/crates/ui/src/docking/iconic_tests.rs @@ -283,7 +283,7 @@ fn hit_rects_cover_every_gesture_source() { first: Box::new(Node::Tabs { panels: vec![0], active: 0 }), second: Box::new(stack), }; - let window = Floating::stack(vec![Group::new(vec![12], 0), Group::new(vec![13], 0)], [500.0, 60.0, 220.0, 300.0]); + let window = Floating::stack(vec![Group::new(vec![12], 0), Group::new(vec![13], 0)], [500.0, 60.0, 220.0, 300.0]).unwrap(); let layout = Layout { root: Some(Node::Split { axis: SplitAxis::Horizontal, size: SplitSize::FixedFirst(200.0), first: Box::new(dock), second: Box::new(frames) }), floating: vec![window], diff --git a/crates/ui/src/docking/model.rs b/crates/ui/src/docking/model.rs index e11a0417..ec4cb137 100644 --- a/crates/ui/src/docking/model.rs +++ b/crates/ui/src/docking/model.rs @@ -87,10 +87,10 @@ impl

Floating

{ tree_panels(&self.root).into_iter() } - /// Groups stacked top to bottom, as a v1 layout stored them. - #[cfg(test)] - pub(crate) fn stack(groups: Vec>, rect: [f32; 4]) -> Self { - Self::new(super::dock_model::stack_tree(groups).expect("a group"), rect) + /// Groups stacked top to bottom, as a v1 layout stored them: each group over a vertical + /// split of the ones below it, sized by their heights. `None` without groups. + pub fn stack(groups: Vec>, rect: [f32; 4]) -> Option { + super::dock_model::stack_tree(groups).map(|root| Self::new(root, rect)) } /// The window's groups, first to last. diff --git a/crates/ui/src/docking/model_tests.rs b/crates/ui/src/docking/model_tests.rs index 583a2dd9..11f3fcde 100644 --- a/crates/ui/src/docking/model_tests.rs +++ b/crates/ui/src/docking/model_tests.rs @@ -23,7 +23,7 @@ pub(super) fn docked() -> Layout { Node::Dock { side: Side::End, columns: vec![Column::new(vec![g(&[2, 3], 1), g(&[4], 0)], 180.0), Column::new(vec![g(&[5], 0)], 120.0)] }; Layout { root: Some(hsplit(SplitSize::FixedSecond(300.0), middle, dock)), - floating: vec![Floating::stack(vec![g(&[6, 7], 1), g(&[8], 0)], [100.0, 100.0, 300.0, 400.0])], + floating: vec![Floating::stack(vec![g(&[6, 7], 1), g(&[8], 0)], [100.0, 100.0, 300.0, 400.0]).unwrap()], } } @@ -213,7 +213,7 @@ fn restore_into_a_column_group_seam_and_new_column_round_trips() { let start = Node::Dock { side: Side::Start, columns: vec![Column::new(vec![g(&[10], 0)], 200.0)] }; let original = Layout { root: Some(hsplit(SplitSize::FixedFirst(200.0), start, hsplit(SplitSize::FixedSecond(300.0), tabs(&[1], 0), end))), - floating: vec![Floating::stack(vec![g(&[6], 0), Group { height: Some(90.0), ..g(&[7], 0) }], [10.0, 10.0, 200.0, 300.0])], + floating: vec![Floating::stack(vec![g(&[6], 0), Group { height: Some(90.0), ..g(&[7], 0) }], [10.0, 10.0, 200.0, 300.0]).unwrap()], }; original.validate().unwrap(); // 4: the seam between [2, 3] and [9]; 9: below [4]; 5: a new column after A; 10: alone in @@ -415,7 +415,7 @@ mod serde_tests { let plain = Floating::single(vec![4u32, 5], 1, rect); assert_eq!(serde_json::to_value(&plain).unwrap(), json!({"panels": [4, 5], "active": 1, "rect": rect})); let minimized = Floating { minimized: true, ..Floating::single(vec![4u32], 0, rect) }; - let stacked = Floating::stack(vec![g(&[4], 0), g(&[5], 0)], rect); + let stacked = Floating::stack(vec![g(&[4], 0), g(&[5], 0)], rect).unwrap(); let native = Floating { native: Some(rect), ..Floating::single(vec![4u32], 0, rect) }; for window in [minimized, stacked, native] { let value = serde_json::to_value(&window).unwrap(); diff --git a/crates/ui/src/docking/render/mod.rs b/crates/ui/src/docking/render/mod.rs index fc8fd3d4..f4fbf37e 100644 --- a/crates/ui/src/docking/render/mod.rs +++ b/crates/ui/src/docking/render/mod.rs @@ -295,7 +295,8 @@ impl Renderer<'_, P> { None => self.id.with(("intersection", outer, inner_layout)), Some((panel, _)) => self.id.with(("window-intersection", panel, outer, inner_layout)), }; - let response = ui.interact(hit, id, Sense::drag()); + // Pointer only: each of the two dividers it joins already takes keyboard focus. + let response = ui.interact(hit, id, Sense::DRAG); self.register_interaction(ui.ctx(), id); if response.hovered() || response.dragged() { ui.ctx().set_cursor_icon(CursorIcon::Move); diff --git a/docs/ui-docking.md b/docs/ui-docking.md index adaec4ab..177c1c2d 100644 --- a/docs/ui-docking.md +++ b/docs/ui-docking.md @@ -39,8 +39,9 @@ their sides. A `Floating` window holds a tree like the docked layout's (`root`: splits, tab groups and accordions, no docks), a rect, an optional native-window rect and `minimized`. -`Floating::single` makes a window with one group and `Floating::new` one with a tree; `panels()` -and `contains()` read it. Saved layouts keep loading: a window stored as one group (v1) or as a +`Floating::single` makes a window with one group, `Floating::new` one with a tree, and +`Floating::stack` one with groups stacked top to bottom (a vertical split sized by the groups' +heights, as an app's own stacked layouts migrate); `panels()` and `contains()` read it. Saved layouts keep loading: a window stored as one group (v1) or as a stack of groups loads as that group or as a vertical split of the stacked groups, sized by their heights.