From cef258a356b7fa475af8418ee3d73699a3e52ccb Mon Sep 17 00:00:00 2001 From: aiongg Date: Tue, 20 Oct 2026 03:42:36 +0000 Subject: [PATCH] docking: a column's first tab docks beside its own column A dock target named by the dragged panel itself anchors on another panel of the same group or column, or leaves the panel in place when it is alone there, instead of failing with InvalidPlacement. --- crates/ui/src/docking/dock_tests.rs | 23 +++++++++++++++++++++++ crates/ui/src/docking/render/dock.rs | 12 +++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/crates/ui/src/docking/dock_tests.rs b/crates/ui/src/docking/dock_tests.rs index 9a17ed7e..8f950481 100644 --- a/crates/ui/src/docking/dock_tests.rs +++ b/crates/ui/src/docking/dock_tests.rs @@ -481,3 +481,26 @@ fn right_to_left_mirrors_docks_their_edges_and_new_docks() { d.settle(); assert_eq!(d.rect("column-collapse", 5).left(), 0.0); } + +#[test] +fn a_column_s_first_tab_dropped_on_its_own_column_edge_anchors_on_another_panel() { + // Two columns, 0..240 and 240..480; the dragged tab is the first panel of the last one. + let wide = |columns: &[&[&[u32]]]| { + let mut layout = docked(Side::Start, columns); + if let Some(Node::Split { size, .. }) = &mut layout.root { + *size = SplitSize::FixedFirst(480.0); + } + layout + }; + let mut d = Driver::new(wide(&[&[&[2]], &[&[3, 4]]])); + let out = d.drag(d.tab(3).center(), pos2(484.0, 300.0)); + assert_eq!(out.actions, vec![Action::Move { panel: 3, anchor: 4, placement: Placement::Column { after: true } }]); + assert_eq!(columns(&d.layout, Side::Start), vec![vec![vec![2]], vec![vec![4]], vec![vec![3]]]); + + // Alone in its column, the panel stays where it is. + let layout = wide(&[&[&[2]], &[&[3]]]); + let mut d = Driver::new(layout.clone()); + let out = d.drag(d.tab(3).center(), pos2(484.0, 300.0)); + assert_eq!(out.actions, vec![]); + assert_eq!(d.layout, layout); +} diff --git a/crates/ui/src/docking/render/dock.rs b/crates/ui/src/docking/render/dock.rs index 3709d169..96936f7a 100644 --- a/crates/ui/src/docking/render/dock.rs +++ b/crates/ui/src/docking/render/dock.rs @@ -10,7 +10,8 @@ use egui::{Context, Key, Pos2, Rect, Sense, Stroke, StrokeKind, Ui, UiBuilder, p use super::{GroupKind, Renderer}; use crate::docking::{ - Action, Column, DockEvent, DockStyle, Group, GroupDrag, GroupGesture, Node, Profile, Side, Target, + Action, Column, DockEvent, DockStyle, Group, GroupDrag, GroupGesture, Node, Placement, Profile, Side, Target, + dock_model::Unit, targets::{DockDrop, DockTarget, column_edge, horizontal_line, resolve_dock, usable, vertical_line}, }; use crate::layout::{ResizeHandle, ResizeHandleStyle, SplitAxis}; @@ -471,6 +472,15 @@ impl Renderer<'_, P> { let candidates = self.dock_candidates(); let drop = resolve_dock(pointer, &candidates)?; let action = match drop.target.to() { + // A target named by the dragged panel is its own group or column: another panel + // there names the same one. With none, the drop leaves the panel where it is. + Target::At { anchor, placement } if &anchor == panel => { + let unit = if matches!(placement, Placement::Column { .. }) { Unit::Column } else { Unit::Group }; + match self.layout.members(panel, unit).ok()?.into_iter().find(|p| p != panel) { + Some(anchor) => Action::Move { panel: panel.clone(), anchor, placement }, + None => return Some((None, drop.line)), + } + } Target::At { anchor, placement } => Action::Move { panel: panel.clone(), anchor, placement }, to @ Target::NewDock { .. } if self.layout.place(panel).and_then(|place| self.layout.group(&place)).is_some_and(|g| g.panels.len() == 1) =>