From 63b3e4e2e85ae1716651b4240ea966bec3b947fb Mon Sep 17 00:00:00 2001 From: aiongg Date: Sun, 25 Oct 2026 05:19:39 +0000 Subject: [PATCH] docking: a drop that changes only sizes is a no-op; a lone floating group's header drag looks through its window --- crates/ui/src/docking/dock_model.rs | 60 ++++++++++++ crates/ui/src/docking/drop_tests.rs | 117 ++++++++++++++++++++++++ crates/ui/src/docking/group_tests.rs | 5 +- crates/ui/src/docking/render/dock.rs | 30 +++--- crates/ui/src/docking/render/preview.rs | 80 ++++++++++------ crates/ui/src/docking/surface.rs | 41 +-------- crates/ui/src/docking/targets.rs | 3 +- docs/ui-docking.md | 11 ++- 8 files changed, 268 insertions(+), 79 deletions(-) diff --git a/crates/ui/src/docking/dock_model.rs b/crates/ui/src/docking/dock_model.rs index 3c0918e6..27390e09 100644 --- a/crates/ui/src/docking/dock_model.rs +++ b/crates/ui/src/docking/dock_model.rs @@ -335,6 +335,66 @@ fn splice

(panels: &mut Vec

, active: &mut usize, index: usize, new: Vec

, } impl Layout

{ + /// The layout without its sizes and rectangles: which panels are where, and their state. + /// A run of splits along one axis nests one way (`a | (b | c)`), since nesting it the + /// other way only moves sizes between its members. + pub(super) fn shape(&self) -> Layout

{ + fn run<'a, P>(node: &'a Node

, axis: SplitAxis, out: &mut Vec<&'a Node

>) { + match node { + Node::Split { axis: own, first, second, .. } if *own == axis => { + run(first, axis, out); + run(second, axis, out); + } + node => out.push(node), + } + } + fn node(n: &Node

) -> Node

{ + match n { + Node::Split { axis, .. } => { + let mut members = Vec::new(); + run(n, *axis, &mut members); + let mut shaped = members.into_iter().rev().map(node); + let last = shaped.next().unwrap_or(Node::Tabs { panels: Vec::new(), active: 0 }); + shaped.fold(last, |second, first| Node::Split { + axis: *axis, + size: SplitSize::Ratio(0.5), + first: Box::new(first), + second: Box::new(second), + }) + } + Node::Tabs { panels, active } => Node::Tabs { panels: panels.clone(), active: *active }, + Node::Stack { entries } => Node::Stack { entries: entries.iter().map(|e| super::StackEntry { height: None, ..e.clone() }).collect() }, + Node::Dock { side, columns } => Node::Dock { + side: *side, + columns: columns + .iter() + .map(|c| Column { + groups: c.groups.iter().map(|g| Group { height: None, ..g.clone() }).collect(), + width: 0.0, + iconic: c.iconic, + iconic_width: None, + }) + .collect(), + }, + } + } + Layout { + root: self.root.as_ref().map(node), + floating: self + .floating + .iter() + .map(|w| Floating { root: node(&w.root), rect: [0.0; 4], native: None, monitor: None, monitor_size: None, ..w.clone() }) + .collect(), + } + } + + /// Whether `other` arranges the panels as this layout does: equal apart from split sizes, + /// column widths, group heights and window rectangles. A drop that leads from one to the + /// other changes nothing the user would see as a move. + pub(super) fn same_arrangement(&self, other: &Self) -> bool { + self.shape() == other.shape() + } + pub(super) fn place(&self, panel: &P) -> Option { for (w, window) in self.floating.iter().enumerate() { for (leaf, path) in leaves(&window.root) { diff --git a/crates/ui/src/docking/drop_tests.rs b/crates/ui/src/docking/drop_tests.rs index b6006aa5..0098aeb5 100644 --- a/crates/ui/src/docking/drop_tests.rs +++ b/crates/ui/src/docking/drop_tests.rs @@ -315,3 +315,120 @@ fn a_canvas_that_takes_splits_takes_them_only_on_its_drop_edges() { } } } + +/// Whether the last frame painted a drop preview: an accent line, outline or fill. +fn previews(d: &Driver) -> bool { + let accent = accent(); + d.shapes().iter().any(|clipped| match &clipped.shape { + egui::Shape::Rect(shape) => shape.fill == accent || shape.fill == accent.gamma_multiply(0.08) || shape.stroke.color == accent, + egui::Shape::Path(path) => path.fill != Color32::TRANSPARENT && path.fill == accent.gamma_multiply(0.3), + _ => false, + }) +} + +fn ratio_split(axis: SplitAxis, ratio: f32, first: Node, second: Node) -> Node { + Node::Split { axis, size: SplitSize::Ratio(ratio), first: Box::new(first), second: Box::new(second) } +} + +#[test] +fn a_drop_that_changes_only_sizes_shows_nothing_and_does_nothing() { + // CADCraft's sidebar: Layers (2) alone above Properties (3) and a third group (4), beside + // the canvas (1); and the same with the column as one two-group split. + let nested = ratio_split(SplitAxis::Vertical, 0.3, tabs(&[2]), ratio_split(SplitAxis::Vertical, 0.5, tabs(&[3]), tabs(&[4]))); + let flat = ratio_split(SplitAxis::Vertical, 0.3, tabs(&[2]), tabs(&[3])); + for column in [nested, flat] { + let layout = Layout { root: Some(ratio_split(SplitAxis::Horizontal, 0.6, tabs(&[1]), column)), floating: Vec::new() }; + let start = || DockDriver::new(layout.clone(), Bodies::default()); + let d = start(); + let (body, rect) = group(&d, 3); + let top = pos2(body.center().x, body.top() + body.height() * 0.2); + // Properties' bottom third moves Layers, with a line. + let mut d = start(); + d.hold(tab(&d, 2), pos2(body.center().x, body.bottom() - body.height() * 0.2)); + assert!(previews(&d) && d.filled(line(rect, Zone::Bottom))); + // Its top third would put Layers back where it is. + let mut d = start(); + d.hold(tab(&d, 2), top); + assert!(!previews(&d), "no line, outline or fill"); + let out = d.release(top); + assert!(out.actions.is_empty(), "{:?}", out.actions); + assert_eq!(d.layout, layout); + } + // A lone frame's tab on the layout edge its frame already holds. + let layout = Layout { root: Some(ratio_split(SplitAxis::Vertical, 0.75, tabs(&[1]), tabs(&[4]))), floating: Vec::new() }; + let mut d = DockDriver::new(layout.clone(), Bodies::default()); + let at = pos2(400.0, 590.0); + d.hold(tab(&d, 4), at); + assert!(!previews(&d)); + let out = d.release(at); + assert!(out.actions.is_empty(), "{:?}", out.actions); + assert_eq!(d.layout, layout); +} + +#[test] +fn a_dock_drop_that_changes_only_heights_shows_nothing_and_does_nothing() { + // A dock column of a sized lone group (2) above another (3), beside the canvas (1). + let column = Column::new(vec![Group { height: Some(150.0), ..Group::new(vec![2], 0) }, Group::new(vec![3], 0)], 240.0); + let dock = Node::Dock { side: Side::End, columns: vec![column] }; + let root = Node::Split { axis: SplitAxis::Horizontal, size: SplitSize::FixedSecond(240.0), first: Box::new(tabs(&[1])), second: Box::new(dock) }; + let layout = Layout { root: Some(root), floating: Vec::new() }; + let start = || DockDriver::new(layout.clone(), Bodies::default()); + let d = start(); + let (body, rect) = group(&d, 3); + let (upper, _) = group(&d, 2); + // Below the lower group moves the upper one, with a line. + let mut d = start(); + d.hold(tab(&d, 2), pos2(body.center().x, body.bottom() - body.height() * 0.2)); + assert!(previews(&d)); + // The lower group's top third and the seam between them put the group back where it is. + for at in [pos2(body.center().x, body.top() + body.height() * 0.2), pos2(body.center().x, (upper.bottom() + rect.top()) * 0.5)] { + let mut d = start(); + d.hold(tab(&d, 2), at); + assert!(!previews(&d), "{at:?}: no line"); + let out = d.release(at); + assert!(out.actions.is_empty(), "{at:?}: {:?}", out.actions); + assert_eq!(d.layout, layout); + } +} + +#[test] +fn a_header_drag_of_a_windows_only_group_looks_through_its_window() { + // Group 4 floats alone over frame 1's left part. + let mut layout = frames(); + layout.floating.push(Floating::single(vec![4], 0, [40.0, 150.0, 300.0, 260.0])); + let start = || DockDriver::new(layout.clone(), Bodies::default()); + let d = start(); + let (body, rect) = group(&d, 1); + let window = Rect::from_min_size(pos2(40.0, 150.0), vec2(300.0, 260.0)); + let at = pos2(body.left() + body.width() * 0.25, body.center().y); + assert!(window.contains(at)); + let mut d = start(); + let bar = d.hit(&Hit::DragBar(4)).unwrap().center(); + d.hold(bar, at); + assert!(d.filled(line(rect, Zone::Left)), "frame 1's left third, behind the window"); + let out = d.release(at); + assert_eq!(out.actions, vec![Action::MoveGroup { panel: 4, to: Target::At { anchor: 1, placement: Placement::Split(Zone::Left) } }]); +} + +#[test] +fn a_header_drag_in_a_window_of_two_groups_still_offers_the_other_group() { + let mut layout = frames(); + let tree = ratio_split(SplitAxis::Vertical, 0.5, tabs(&[4]), tabs(&[5])); + layout.floating.push(Floating::new(tree, [40.0, 100.0, 300.0, 400.0])); + let start = || DockDriver::new(layout.clone(), Bodies::default()); + let d = start(); + let (body, rect) = group(&d, 5); + let mut d = start(); + let bar = d.hit(&Hit::DragBar(4)).unwrap().center(); + d.hold(bar, body.center()); + assert!(d.outlined(rect.shrink(1.0)), "group 5 joins"); + let out = d.release(body.center()); + assert!( + matches!( + &out.actions[..], + [Action::MoveGroup { panel: 4, to: Target::At { anchor: 5, placement: Placement::Tab { .. } | Placement::Split(Zone::Center) } }] + ), + "{:?}", + out.actions + ); +} diff --git a/crates/ui/src/docking/group_tests.rs b/crates/ui/src/docking/group_tests.rs index fc071e66..8406ec5e 100644 --- a/crates/ui/src/docking/group_tests.rs +++ b/crates/ui/src/docking/group_tests.rs @@ -503,7 +503,8 @@ fn release_over_own_position_is_a_noop() { #[test] fn floating_border_and_drag_bar_move_and_cancel_as_a_group() { for name in ["drag-border", "drag-bar"] { - let layout = Layout { root: Some(tabs(&[1], 0)), floating: vec![Floating::single(vec![5, 6], 1, [80.0, 70.0, 300.0, 220.0])] }; + // Nothing docked: a header drag of a window's only group looks through the window. + let layout = Layout { root: None, floating: vec![Floating::single(vec![5, 6], 1, [80.0, 70.0, 300.0, 220.0])] }; let mut d = Driver::new(layout.clone()); let from = d.rect(name, 5).center(); d.hold(from, from + vec2(50.0, 40.0)); @@ -522,7 +523,7 @@ fn floating_border_and_drag_bar_move_and_cancel_as_a_group() { #[test] fn floating_border_movement_does_not_accumulate_fractional_step_rounding() { - let layout = Layout { root: Some(tabs(&[1], 0)), floating: vec![Floating::single(vec![5, 6], 1, [80.0, 70.0, 300.0, 220.0])] }; + let layout = Layout { root: None, floating: vec![Floating::single(vec![5, 6], 1, [80.0, 70.0, 300.0, 220.0])] }; let mut d = Driver::new(layout); let from = d.rect("drag-border", 5).center(); d.step(vec![Event::PointerMoved(from), button(from, true)]); diff --git a/crates/ui/src/docking/render/dock.rs b/crates/ui/src/docking/render/dock.rs index be0aeee4..19c97607 100644 --- a/crates/ui/src/docking/render/dock.rs +++ b/crates/ui/src/docking/render/dock.rs @@ -10,7 +10,7 @@ use egui::{Context, Key, Pos2, Rect, Sense, Stroke, StrokeKind, Ui, UiBuilder, p use super::{GroupKind, Renderer, RowItem, row_drag, row_range, stripless}; use crate::docking::{ - Action, Column, DockEvent, DockHeights, DockStyle, Group, GroupDrag, GroupGesture, Node, Placement, Side, Target, Zone, + Action, Column, DockEvent, DockHeights, DockStyle, DropTarget, Group, GroupDrag, GroupGesture, Node, Placement, Side, Target, Zone, dock_model::Unit, targets::{ DockDrop, DockTarget, SIDE_EDGE_ZONE_DEPTH, STRIP_REACH, body_zone, column_edge, drop_zone, horizontal_line, resolve_dock, usable, @@ -636,18 +636,25 @@ impl Renderer<'_, P> { /// takes tabs: the strip joins the group, and border bands reach into it only /// [`STRIP_REACH`] from their lines. Not with [`DockStyle::side_edge_zones`]. pub(super) fn in_tab_strip(&self, pointer: Pos2) -> bool { + self.in_tab_strip_past(pointer, &[]) + } + + /// [`Self::in_tab_strip`] past the groups holding any of `through`. + fn in_tab_strip_past(&self, pointer: Pos2, through: &[P]) -> bool { + let own = |t: &DropTarget

| t.tabs.iter().any(|(p, _)| through.contains(p)); !self.style.side_edge_zones - && self.targets.iter().rev().find(|t| usable(t.rect) && t.rect.contains(pointer)).is_some_and(|t| { + && self.targets.iter().rev().find(|t| usable(t.rect) && t.rect.contains(pointer) && !own(t)).is_some_and(|t| { t.accepts_tabs && t.tab_strip.map(|strip| strip.intersect(t.rect)).is_some_and(|strip| usable(strip) && strip.contains(pointer)) }) } /// The dock target at `pointer`: a band at a border, else a side zone of a dock group's /// body ([`SIDE_EDGE_ZONE_DEPTH`] deep with [`DockStyle::side_edge_zones`]). The middle of a - /// group that takes tabs is none: the group's own target joins it. - fn dock_drop(&self, pointer: Pos2) -> Option> { + /// group that takes tabs is none: the group's own target joins it. The groups holding + /// `through` (a floating window the drag looks through) hide no strip. + fn dock_drop(&self, pointer: Pos2, through: &[P]) -> Option> { let mut candidates = self.dock_candidates(); - let strip = self.in_tab_strip(pointer); + let strip = self.in_tab_strip_past(pointer, through); if strip { candidates.retain(|t| t.line.distance_to_pos(pointer) <= STRIP_REACH); } @@ -670,11 +677,11 @@ impl Renderer<'_, P> { /// The dock target a whole group or column drag would take at `pointer`, with the action /// and its line. A target that leaves the layout unchanged is a no-op: `None` action. pub(super) fn dock_group_proposal(&self, drag: &GroupDrag

, pointer: Pos2) -> Option<(Option>, Rect)> { - let moving = (drag.gesture == GroupGesture::Window).then_some(&drag.panel); - if self.dock_covered(pointer, moving) { + let through = self.looks_through(drag); + if self.dock_covered(pointer, through.then_some(&drag.panel)) { return None; } - let drop = self.dock_drop(pointer)?; + let drop = self.dock_drop(pointer, if through { &drag.members } else { &[] })?; let action = group_action(drag, drop.target.to()); self.checked(action).map(|action| (action, drop.line)) } @@ -686,7 +693,7 @@ impl Renderer<'_, P> { if self.dock_covered(pointer, None) { return None; } - let drop = self.dock_drop(pointer)?; + let drop = self.dock_drop(pointer, &[])?; 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. @@ -707,11 +714,12 @@ impl Renderer<'_, P> { self.checked(action).map(|action| (action, drop.line)) } - /// `Some(None)` when the action is valid but changes nothing, `None` when it is invalid. + /// `Some(None)` when the action is valid but changes nothing apart from sizes + /// ([`crate::docking::Layout::same_arrangement`]), `None` when it is invalid. fn checked(&self, action: Action

) -> Option>> { let mut candidate = self.layout.clone(); candidate.apply_with_permissions(action.clone(), |p| self.permissions(p)).ok()?; - Some((candidate != *self.layout).then_some(action)) + Some((!candidate.same_arrangement(self.layout)).then_some(action)) } /// The line a drop beside a group, column, dock or the layout's edge previews with. diff --git a/crates/ui/src/docking/render/preview.rs b/crates/ui/src/docking/render/preview.rs index a8cd1e6f..75b9365f 100644 --- a/crates/ui/src/docking/render/preview.rs +++ b/crates/ui/src/docking/render/preview.rs @@ -127,24 +127,35 @@ impl Renderer<'_, P> { } /// The valid target under the pointer for a whole group. A target holding the group - /// itself is a no-op proposal; a target the model would reject is no target. A window - /// moving live looks through itself and does not split frames or, moved by its bar, join - /// a body's center. + /// itself is a no-op proposal, and so is a drop that would change only sizes; a target + /// the model would reject is no target. A drag that takes a whole floating window along + /// looks through it ([`Self::looks_through`]). A window moving live does not split frames + /// or, moved by its bar, join a body's center. pub(super) fn group_proposal(&self, drag: &GroupDrag

, pointer: Pos2) -> Option> { - let window = drag.gesture == GroupGesture::Window; - if let Some(proposal) = self.extra_proposal(&drag.members, pointer, window) { - return self.valid_group(drag, proposal); + if let Some(proposal) = self.extra_proposal(&drag.members, pointer, self.looks_through(drag)) { + return self.valid_group(drag, self.settled(proposal, |to| group_action(drag, to))); } let target = self.group_target(drag, pointer)?; self.group_proposal_at(drag, pointer, target) } - /// The group under the pointer that a group drag addresses. A window moving live looks - /// through itself. + /// Whether a group drag looks through the floating window it comes from: the window's + /// title bar moves it, or the drag holds the window's only group and so takes the window + /// along. The window then covers nothing behind it. + pub(super) fn looks_through(&self, drag: &GroupDrag

) -> bool { + match drag.gesture { + GroupGesture::Window => true, + GroupGesture::Group => self.whole_window(&drag.panel), + GroupGesture::Column => false, + } + } + + /// The group under the pointer that a group drag addresses, past the drag's own groups + /// when it looks through its window. fn group_target(&self, drag: &GroupDrag

, pointer: Pos2) -> Option<&DropTarget

> { - let window = drag.gesture == GroupGesture::Window; + let through = self.looks_through(drag); let own = |t: &DropTarget

| t.tabs.iter().any(|(p, _)| drag.members.contains(p)); - self.targets.iter().rev().find(|t| crate::docking::targets::usable(t.rect) && t.rect.contains(pointer) && !(window && own(t))) + self.targets.iter().rev().find(|t| crate::docking::targets::usable(t.rect) && t.rect.contains(pointer) && !(through && own(t))) } /// [`Self::group_proposal`] on `target` alone. @@ -189,23 +200,37 @@ impl Renderer<'_, P> { /// [`crate::docking::resolve_drop_by`] with [`crate::docking::DockStyle::zone_rule`] and the /// canvas sides of [`crate::docking::DockStyle::drop_edges`], where a split that would leave /// a group without room becomes a join of the target group, or no target when the group - /// takes no tabs. + /// takes no tabs. A drop that would change only sizes is a no-op ([`Self::settled`]). fn resolve_roomy(&self, panel: &P, pointer: Pos2, targets: &[DropTarget

], action: impl Fn(Target

) -> Action

) -> Option> { let proposal = resolve_drop_on(panel, pointer, targets, self.style.zone_rule(), self.style.drop_sides())?; if !matches!(proposal.placement, Placement::Split(zone) if zone != Zone::Center) { - return Some(proposal); + return Some(self.settled(proposal, action)); } if self.leaves_room(action(Target::At { anchor: proposal.anchor.clone(), placement: proposal.placement.clone() })) { - return Some(proposal); + return Some(self.settled(proposal, action)); } let target = targets.iter().rev().find(|t| crate::docking::targets::usable(t.rect) && t.rect.contains(pointer))?; - resolve_drop_on( + let proposal = resolve_drop_on( panel, pointer, std::slice::from_ref(&DropTarget { allow_splits: false, ..target.clone() }), self.style.zone_rule(), self.style.drop_sides(), - ) + )?; + Some(self.settled(proposal, action)) + } + + /// `proposal`, marked a no-op when `action` at it would leave every panel where it is, + /// changing only sizes ([`Layout::same_arrangement`]): a lone group dropped beside the + /// group it already borders, or a tab on the edge its frame already holds. Such a drop + /// shows no target and changes nothing. + fn settled(&self, mut proposal: DropProposal

, action: impl Fn(Target

) -> Action

) -> DropProposal

{ + if !proposal.noop { + let mut candidate = self.layout.clone(); + let to = Target::At { anchor: proposal.anchor.clone(), placement: proposal.placement.clone() }; + proposal.noop = candidate.apply_with_permissions(action(to), |p| self.permissions(p)).is_ok() && candidate.same_arrangement(self.layout); + } + proposal } /// Whether `action`, a drop that splits a group or the outer edge, leaves room: every group @@ -245,8 +270,9 @@ impl Renderer<'_, P> { } /// Release of a group drag: join or split at the target, or float at the ghost with the - /// group's size. Over its own docked position nothing changes; a floating group released - /// over itself moves there. + /// group's size. Over its own docked position, or where the drop would change only sizes, + /// nothing changes; a floating group released over itself or a window's title bar moves + /// there. fn drop_group(&mut self, drag: &GroupDrag

, pointer: Pos2, float_only: bool) { if pointer.distance(drag.origin) < 2.0 { return; @@ -262,7 +288,8 @@ impl Renderer<'_, P> { self.output.actions.push(group_action(drag, to)); return; } - if proposal.is_some() && !floating { + // A no-op anchored on another group would change only sizes. + if proposal.is_some_and(|p| !floating || !drag.members.contains(&p.anchor)) { return; } if !float_only && !self.style.float_on_empty_drop { @@ -416,12 +443,14 @@ impl Renderer<'_, P> { /// The group target a single tab drag would take at `pointer`. pub(super) fn tab_proposal(&self, panel: &P, pointer: Pos2) -> Option> { - self.extra_proposal(std::slice::from_ref(panel), pointer, false).or_else(|| { - self.resolve_roomy(panel, pointer, &self.targets, |to| match to { - Target::At { anchor, placement } => Action::Move { panel: panel.clone(), anchor, placement }, - to => Action::MovePanel { panel: panel.clone(), to }, - }) - }) + let action = |to| match to { + Target::At { anchor, placement } => Action::Move { panel: panel.clone(), anchor, placement }, + to => Action::MovePanel { panel: panel.clone(), to }, + }; + match self.extra_proposal(std::slice::from_ref(panel), pointer, false) { + Some(proposal) => Some(self.settled(proposal, action)), + None => self.resolve_roomy(panel, pointer, &self.targets, action), + } } /// The preview of a tab drag's group target (with @@ -500,8 +529,7 @@ impl Renderer<'_, P> { /// [`Self::paint_zones`] for a group drag. pub(super) fn paint_group_zones(&self, painter: &egui::Painter, drag: &GroupDrag

, pointer: Pos2, proposal: Option<&DropProposal

>) -> bool { - let window = drag.gesture == GroupGesture::Window; - if !self.style.side_edge_zones || self.extra_proposal(&drag.members, pointer, window).is_some() { + if !self.style.side_edge_zones || self.extra_proposal(&drag.members, pointer, self.looks_through(drag)).is_some() { return false; } let Some(target) = self.group_target(drag, pointer) else { return false }; diff --git a/crates/ui/src/docking/surface.rs b/crates/ui/src/docking/surface.rs index 01e9a838..885303a6 100644 --- a/crates/ui/src/docking/surface.rs +++ b/crates/ui/src/docking/surface.rs @@ -496,54 +496,19 @@ fn cancel_drag_if_replaced }) else { return; }; - let now = shape(layout); - if now == shape(&last.layout) { + let now = layout.shape(); + if now == last.layout.shape() { return; } let mut applied = last.layout; for action in last.actions { let _ = applied.apply(action); } - if now != shape(&applied) { + if now != applied.shape() { cancel_drag::

(ctx, area); } } -/// `layout` without its sizes and rectangles: which panels are where, and their state. -fn shape(layout: &Layout

) -> Layout

{ - use super::{Column, Floating, Group, Node, StackEntry}; - use crate::layout::SplitSize; - fn node(n: &Node

) -> Node

{ - match n { - Node::Split { axis, first, second, .. } => { - Node::Split { axis: *axis, size: SplitSize::Ratio(0.5), first: Box::new(node(first)), second: Box::new(node(second)) } - } - Node::Tabs { panels, active } => Node::Tabs { panels: panels.clone(), active: *active }, - Node::Stack { entries } => Node::Stack { entries: entries.iter().map(|e| StackEntry { height: None, ..e.clone() }).collect() }, - Node::Dock { side, columns } => Node::Dock { - side: *side, - columns: columns - .iter() - .map(|c| Column { - groups: c.groups.iter().map(|g| Group { height: None, ..g.clone() }).collect(), - width: 0.0, - iconic: c.iconic, - iconic_width: None, - }) - .collect(), - }, - } - } - Layout { - root: layout.root.as_ref().map(node), - floating: layout - .floating - .iter() - .map(|w| Floating { root: node(&w.root), rect: [0.0; 4], native: None, monitor: None, monitor_size: None, ..w.clone() }) - .collect(), - } -} - /// Keep what [`cancel_drag_if_replaced`] compares the next frame against, while a drag of this /// area goes on. fn record_drag_frame( diff --git a/crates/ui/src/docking/targets.rs b/crates/ui/src/docking/targets.rs index a3dc3b2a..0752cbb6 100644 --- a/crates/ui/src/docking/targets.rs +++ b/crates/ui/src/docking/targets.rs @@ -23,7 +23,8 @@ pub struct DropProposal

{ pub placement: Placement

, pub preview: Rect, pub insertion: Option, - /// The pointer addresses the tab's current insertion slot. + /// The pointer addresses the tab's current insertion slot. The renderer also marks a drop + /// that would leave every panel where it is, changing only sizes. pub noop: bool, } diff --git a/docs/ui-docking.md b/docs/ui-docking.md index 93a42930..4ad822a4 100644 --- a/docs/ui-docking.md +++ b/docs/ui-docking.md @@ -215,7 +215,10 @@ A flyout fits its content the same way, without the title bar. with `title_bar_docks` a release over a target docks the whole window there, else it only moves. - Inside a floating window, tabs and drag bars drag and dock as they do in the docked layout, - into any window. Its groups split, join and resize like frames, with dividers and four-way + into any window. A drag bar or border of a window's only group takes the whole window along, + so, like the title bar, it looks through the window: the groups, docks and windows behind it + take the drop while the pointer is still inside it. In a window of several groups, the other + groups stay targets. Its groups split, join and resize like frames, with dividers and four-way handles (`Divider::WindowSplit` and `Divider::WindowIntersection`, named by the window's first panel). Double-clicking the title bar minimizes the window to its title bar, or to the tab strip of its only group; so does double-clicking that group's strip. The group menu's float item @@ -308,6 +311,12 @@ A flyout fits its content the same way, without the title bar. - A drop that joins a group shows a 2 pt accent outline of the whole group, with a light fill, and on its strip the 2 pt insertion marker. + A drop that would leave every panel where it is, changing only sizes, shows nothing and + changes nothing: a lone group dropped beside the group it already borders, or a frame's tab + on the layout edge the frame already holds. Split sizes, column widths, group heights and + window rectangles are not compared, and a run of splits along one axis is the same however + it nests. + A group drag also shows its ghost at the pointer. With `side_edge_zones`, a drag over a group instead outlines each of its zones that a drop would change something in (the side trapezoids, the center rectangle and the tab strip; `zone_shape` gives the outlines) and