From 5d5d11f8546990ff508eae1442e894151660d8b7 Mon Sep 17 00:00:00 2001 From: aiongg Date: Mon, 19 Oct 2026 13:30:02 +0000 Subject: [PATCH] docking: window.panel.dock to a side without a dock moves one panel of a shared group Action::MovePanel moves one panel to a Target; NewDock and Float give it a group of its own. DockCommand::Dock with a side that has no dock used to fail with InvalidPlacement for a panel that shares its group. --- crates/ui/src/docking/command.rs | 2 +- crates/ui/src/docking/command_tests.rs | 10 ++++++++++ crates/ui/src/docking/model.rs | 25 +++++++++++++++++++++++++ docs/ui-docking.md | 4 ++-- 4 files changed, 38 insertions(+), 3 deletions(-) diff --git a/crates/ui/src/docking/command.rs b/crates/ui/src/docking/command.rs index 3ae6d2ae..7a146f95 100644 --- a/crates/ui/src/docking/command.rs +++ b/crates/ui/src/docking/command.rs @@ -284,7 +284,7 @@ impl Unit { let panel = panel.clone(); Ok(match (self, to) { (Self::Panel, Target::At { anchor, placement }) => Action::Move { panel, anchor, placement }, - (Self::Panel, _) => return Err(DockError::InvalidPlacement), + (Self::Panel, to) => Action::MovePanel { panel, to }, (Self::Group, to) => Action::MoveGroup { panel, to }, (Self::Column, to) if floating => Action::MoveWindow { panel, to }, (Self::Column, to) => Action::MoveColumn { panel, to }, diff --git a/crates/ui/src/docking/command_tests.rs b/crates/ui/src/docking/command_tests.rs index e109a08c..905e1452 100644 --- a/crates/ui/src/docking/command_tests.rs +++ b/crates/ui/src/docking/command_tests.rs @@ -371,3 +371,13 @@ fn float_without_a_rect_takes_the_drag_size_inside_the_dock_area() { fn float_with(space: &FloatSpace

, panel: &str) -> Vec> { parse(json!({"command": "window.panel.float", "panel": panel})).unwrap().to_actions(&workspace(), false, space).unwrap() } + +#[test] +fn dock_to_a_side_without_a_dock_takes_one_panel_out_of_a_shared_group() { + let params = json!({"command": "window.panel.dock", "panel": "layers", "side": "End"}); + assert_eq!(actions(params.clone()), vec![Action::MovePanel { panel: s("layers"), to: Target::NewDock { side: Side::End } }]); + let (workspace, _) = applied(params); + let side = |panel: &str| workspace.layout.location(&s(panel)).unwrap().group.and_then(|g| g.column).map(|c| c.side); + assert_eq!(side("layers"), Some(Side::End)); + assert_eq!(side("channels"), Some(Side::Start)); +} diff --git a/crates/ui/src/docking/model.rs b/crates/ui/src/docking/model.rs index 75391f6e..690137bb 100644 --- a/crates/ui/src/docking/model.rs +++ b/crates/ui/src/docking/model.rs @@ -310,6 +310,12 @@ pub enum Action

{ panel: P, rect: Option<[f32; 4]>, }, + /// Move one panel, without the rest of its group. `Target::At` is [`Action::Move`]; + /// `NewDock` and `Float` give the panel a group of its own. + MovePanel { + panel: P, + to: Target

, + }, } /// What may happen to a panel. [`Layout::apply_with_permissions`] enforces these for every @@ -726,6 +732,18 @@ impl Layout

{ Action::Close { panel } if !permissions(panel).closable => return Err(DockError::NotAllowed), Action::MoveFloating { panel, .. } if !permissions(panel).movable => return Err(DockError::NotAllowed), Action::Open { anchor: Some(anchor), .. } if !permissions(anchor).accepts_tabs => return Err(DockError::NotAllowed), + Action::MovePanel { panel, to } => { + let policy = permissions(panel); + let allowed = policy.movable + && match to { + Target::Float { .. } => policy.floatable, + Target::At { anchor, placement } => placement_allowed(placement, permissions(anchor)), + Target::NewDock { .. } => true, + }; + if !allowed { + return Err(DockError::NotAllowed); + } + } Action::MoveGroup { panel, to } | Action::MoveColumn { panel, to } | Action::MoveWindow { panel, to } => { let members = self.members(panel, unit_of(&action))?; if members.iter().any(|p| !permissions(p).movable) { @@ -899,6 +917,13 @@ impl Layout

{ let moving = self.take_unit(panel, unit)?; self.insert_moving(moving, to.clone()) } + Action::MovePanel { panel, to } => match to { + Target::At { anchor, placement } => self.apply_inner(Action::Move { panel, anchor, placement }), + to => { + let entry = self.take(&panel).ok_or(DockError::MissingPanel)?; + self.insert_moving(Moving::group(Group::new(vec![entry.panel], 0)), to) + } + }, Action::CloseGroup { ref panel } | Action::CloseWindow { ref panel } => self.take_unit(panel, unit_of(&action)).map(|_| ()), Action::CloseOthers { panel } => { for other in self.members(&panel, Unit::Group)? { diff --git a/docs/ui-docking.md b/docs/ui-docking.md index 9705a620..336367e7 100644 --- a/docs/ui-docking.md +++ b/docs/ui-docking.md @@ -57,8 +57,8 @@ layout.apply(Action::Move { # Ok::<(), craft_ui::docking::DockError>(()) ``` -- `Move`, `Float` and `Close` act on one panel. `MoveGroup`, `MoveColumn` and `MoveWindow` move a - group, a dock column or a floating window, named by any panel in it. +- `Move`, `MovePanel`, `Float` and `Close` act on one panel. `MoveGroup`, `MoveColumn` and + `MoveWindow` move a group, a dock column or a floating window, named by any panel in it. - A `Target` is `At { anchor, placement }`, `Float { rect }` or `NewDock { side }`. - A `Placement` is a tab position, a split zone, a new group above or below (with an optional preferred `height`), a new column, or a new frame on an outer edge.