diff --git a/crates/ui/src/docking/floating_tests.rs b/crates/ui/src/docking/floating_tests.rs index 5e3c1ba7..da5ae98c 100644 --- a/crates/ui/src/docking/floating_tests.rs +++ b/crates/ui/src/docking/floating_tests.rs @@ -592,3 +592,20 @@ fn hidden_panels_leave_their_split_and_resizing_names_the_layout_path() { let Some(Node::Split { size: SplitSize::FixedFirst(200.0), second, .. }) = &d.layout.root else { panic!("{:?}", d.layout) }; assert!(matches!(**second, Node::Split { size, .. } if size != SplitSize::Ratio(0.5))); } + +#[test] +fn a_hidden_floating_window_does_not_cover_dock_targets() { + // Window 5 covers the column's seam at x 150..240 until it is hidden. + let mut layout = workspace(); + layout.floating = vec![Floating::single(vec![5], 0, [150.0, 250.0, 200.0, 150.0]), Floating::single(vec![8], 0, [560.0, 300.0, 200.0, 200.0])]; + let mut d = Driver::new(layout, Profile::Dock); + d.hide = Hide::Keep; + d.content.keep = vec![2, 3, 4, 8]; + d.step(vec![]); + d.step(vec![]); + d.drag(d.title(8).center(), pos2(200.0, 309.0)); + let Some(Node::Split { first, .. }) = &d.layout.root else { panic!("{:?}", d.layout.root) }; + let Node::Dock { columns, .. } = &**first else { panic!("{first:?}") }; + let groups: Vec> = columns[0].groups.iter().map(|g| g.panels.clone()).collect(); + assert_eq!(groups, vec![vec![2, 3], vec![8], vec![4]]); +} diff --git a/crates/ui/src/docking/hits.rs b/crates/ui/src/docking/hits.rs index 053732bb..c2eb05f4 100644 --- a/crates/ui/src/docking/hits.rs +++ b/crates/ui/src/docking/hits.rs @@ -112,7 +112,9 @@ impl DockArea<'_, P> { /// The egui layer of a floating window in the area `id`; `None` for an empty window. Of two /// layers, the later in `ctx.memory(|m| m.layer_ids())` is drawn on top, and - /// `ctx.memory(|m| m.area_rect(layer.id))` is the window's rect. + /// `ctx.memory(|m| m.area_rect(layer.id))` is the window's rect. A window the area does not + /// draw (all its panels hidden, or a frame maximized) keeps its last rect there: check + /// `ctx.memory(|m| m.areas().is_visible(&layer))` before treating it as covering anything. pub fn floating_layer(id: Id, window: &Floating

) -> Option { window.panels().next().map(|first| LayerId::new(Order::Middle, id.with(("floating", first)))) } diff --git a/crates/ui/src/docking/render/dock.rs b/crates/ui/src/docking/render/dock.rs index 96936f7a..c5e88e4b 100644 --- a/crates/ui/src/docking/render/dock.rs +++ b/crates/ui/src/docking/render/dock.rs @@ -412,15 +412,13 @@ impl Renderer<'_, P> { }); } - /// Whether a floating window (other than the one following the pointer) or a host overlay - /// covers `pointer`. Covered dock targets take nothing. - fn dock_covered(&self, ctx: &Context, pointer: Pos2, moving: Option<&P>) -> bool { + /// Whether a floating window drawn this frame (other than the one following the pointer) + /// or a host overlay covers `pointer`. Covered dock targets take nothing. + fn dock_covered(&self, pointer: Pos2, moving: Option<&P>) -> bool { self.blockers.iter().any(|r| r.contains(pointer)) || self.iconic.covers(pointer) - || self.layout.floating.iter().any(|window| { - let Some(first) = window.panels().next() else { return false }; - !moving.is_some_and(|p| window.contains(p)) - && ctx.memory(|m| m.area_rect(self.id.with(("floating", first)))).is_some_and(|r| r.contains(pointer)) + || self.frame.stacks.iter().any(|window| { + window.window.contains(pointer) && !moving.is_some_and(|p| window.groups.iter().any(|(panels, ..)| panels.contains(p))) }) } @@ -451,9 +449,9 @@ 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, ctx: &Context, drag: &GroupDrag

, pointer: Pos2) -> Option<(Option>, Rect)> { + 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(ctx, pointer, moving) { + if self.dock_covered(pointer, moving) { return None; } let candidates = self.dock_candidates(); @@ -465,8 +463,8 @@ impl Renderer<'_, P> { /// The dock target a single tab drag would take: a new group, column or dock of that panel. /// A panel alone in its group takes the group's state to a new dock; one that shares its /// group leaves the other tabs behind (P6). - pub(super) fn dock_tab_proposal(&self, ctx: &Context, panel: &P, pointer: Pos2) -> Option<(Option>, Rect)> { - if self.dock_covered(ctx, pointer, None) { + pub(super) fn dock_tab_proposal(&self, panel: &P, pointer: Pos2) -> Option<(Option>, Rect)> { + if self.dock_covered(pointer, None) { return None; } let candidates = self.dock_candidates(); diff --git a/crates/ui/src/docking/render/floating.rs b/crates/ui/src/docking/render/floating.rs index 1e46077f..f0045e23 100644 --- a/crates/ui/src/docking/render/floating.rs +++ b/crates/ui/src/docking/render/floating.rs @@ -310,8 +310,8 @@ impl Renderer<'_, P> { /// Release of a title bar drag. The window already sits where it was dragged; a dock or /// group target docks or merges every group in it, else hosts get the release in /// `group_drop`. - pub(super) fn drop_window(&mut self, ctx: &Context, drag: &GroupDrag

, pointer: Pos2, float_only: bool) { - if !float_only && let Some((action, _)) = self.dock_group_proposal(ctx, drag, pointer) { + pub(super) fn drop_window(&mut self, drag: &GroupDrag

, pointer: Pos2, float_only: bool) { + if !float_only && let Some((action, _)) = self.dock_group_proposal(drag, pointer) { self.output.actions.extend(action); return; } diff --git a/crates/ui/src/docking/render/preview.rs b/crates/ui/src/docking/render/preview.rs index aff38e95..4feed8b9 100644 --- a/crates/ui/src/docking/render/preview.rs +++ b/crates/ui/src/docking/render/preview.rs @@ -66,7 +66,7 @@ impl Renderer<'_, P> { } let Some(pointer) = pointer.filter(|p| !self.blockers.iter().any(|r| r.contains(*p))) else { return }; match drag.gesture { - GroupGesture::Window => self.drop_window(ctx, &drag, pointer, float_only), + GroupGesture::Window => self.drop_window(&drag, pointer, float_only), GroupGesture::Group | GroupGesture::Column => self.drop_group(ctx, &drag, pointer, float_only), } } else if let Some(pointer) = pointer.filter(|p| !self.blockers.iter().any(|r| r.contains(*p))) { @@ -113,7 +113,7 @@ impl Renderer<'_, P> { if pointer.distance(drag.origin) < 2.0 { return; } - if !float_only && let Some((action, _)) = self.dock_group_proposal(ctx, drag, pointer) { + if !float_only && let Some((action, _)) = self.dock_group_proposal(drag, pointer) { self.output.actions.extend(action); return; } @@ -162,7 +162,7 @@ impl Renderer<'_, P> { if float_only { return; } - if let Some((action, line)) = self.dock_group_proposal(ctx, drag, pointer) { + if let Some((action, line)) = self.dock_group_proposal(drag, pointer) { if action.is_some() { self.paint_dock_line(ctx, line); } @@ -214,7 +214,7 @@ impl Renderer<'_, P> { } return; } - let dock = if float_only || gone { None } else { self.dock_tab_proposal(ctx, &drag.panel, pointer) }; + let dock = if float_only || gone { None } else { self.dock_tab_proposal(&drag.panel, pointer) }; if let Some((action, line)) = dock { if released { cancel_drag::

(ctx, self.id); diff --git a/docs/ui-docking.md b/docs/ui-docking.md index 122f0bae..df9f3f6d 100644 --- a/docs/ui-docking.md +++ b/docs/ui-docking.md @@ -274,7 +274,9 @@ grips, resize edges and dividers. It is always compiled, so an app can return it `DockArea::hit_id(id, &hit)` is the egui widget id of a control, for tests and automation that focus it or read its response. `DockArea::floating_layer(id, &window)` is a floating window's egui layer: compare its place in `memory.layer_ids()` with another layer to tell whether the -window covers it, and read its rect with `memory.area_rect(layer.id)`. +window covers it, and read its rect with `memory.area_rect(layer.id)`. A window the area does not +draw (its panels hidden, or a frame maximized) keeps its last rect there, so check +`memory.areas().is_visible(&layer)` first. The `testing` feature adds `docking::testing::DockDriver`. It runs a `DockArea` in a headless egui context with real pointer and key events, and applies the actions each frame emits.