diff --git a/crates/ui/src/docking/model.rs b/crates/ui/src/docking/model.rs index babb3e2f..a7056677 100644 --- a/crates/ui/src/docking/model.rs +++ b/crates/ui/src/docking/model.rs @@ -287,6 +287,12 @@ impl Layout

{ self.panels().contains(&panel) } + /// The panels `panel` can join as a tab: every other panel whose permissions accept tabs. + /// A "Move to group" menu with an empty answer has nothing to offer and stays hidden. + pub fn move_targets(&self, panel: &P, permissions: impl Fn(&P) -> Permissions) -> Vec<&P> { + self.panels().into_iter().filter(|target| *target != panel && permissions(target).accepts_tabs).collect() + } + /// Capture a panel's nearest surviving group/split neighbor before hiding it. pub fn location(&self, panel: &P) -> Result, DockError> { self.validate()?; diff --git a/crates/ui/src/docking/surface.rs b/crates/ui/src/docking/surface.rs index 0cf54891..53b81372 100644 --- a/crates/ui/src/docking/surface.rs +++ b/crates/ui/src/docking/surface.rs @@ -712,12 +712,12 @@ impl Renderer<'_, P> { ui.close(); } } - if policy.movable { + if policy.movable && !self.layout.move_targets(panel, self.permissions).is_empty() { let text = self.content.action_label(ui, &self.style.move_label, &self.style.font, self.style.text); let response = ui.menu_button(text, |ui| { egui::ScrollArea::vertical().max_height(360.0).show(ui, |ui| { - for target in self.layout.panels() { - if target != panel && (self.permissions)(target).accepts_tabs && self.menu_label(ui, target).clicked() { + for target in self.layout.move_targets(panel, self.permissions) { + if self.menu_label(ui, target).clicked() { self.output.actions.push(Action::Move { panel: panel.clone(), anchor: target.clone(), diff --git a/crates/ui/src/docking/tests.rs b/crates/ui/src/docking/tests.rs index 38c9f9fc..941220db 100644 --- a/crates/ui/src/docking/tests.rs +++ b/crates/ui/src/docking/tests.rs @@ -51,6 +51,22 @@ fn detach_redock_merge_and_collapse_empty_source_containers() { assert_eq!(layout.panels().len(), 6); } +#[test] +fn move_targets_skip_the_panel_itself_and_protected_anchors() { + let layout = sample(); + let all = |_: &u32| Permissions::default(); + let targets = layout.move_targets(&1, all); + assert_eq!(targets.len(), 5); + assert!(!targets.contains(&&1)); + let guarded = |p: &u32| if *p == 3 { Permissions::PROTECTED } else { Permissions::default() }; + assert!(!layout.move_targets(&1, guarded).contains(&&3)); + + // A lone panel has nowhere to go — its "Move to group" menu hides + // (storytold/lightcraft#1768). + let single = Layout { root: Some(tabs(&[1], 0)), floating: Vec::new() }; + assert!(single.move_targets(&1, all).is_empty()); +} + #[test] fn invalid_moves_are_atomic_including_after_a_source_would_have_been_removed() { let layout = sample();