docking: hidden floating windows do not cover dock targets

Dock targets check the floating windows drawn this frame instead of every
window's last egui area rect. floating_layer says that an undrawn window
keeps its last area rect.
This commit is contained in:
aiongg committed 2026-10-20 03:44:25 +00:00
1 parent a55cbe870e
commit ae0c19b49b
6 files changed
+38 -19

No files matched your search

+17
View File
@@ -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<Vec<u32>> = columns[0].groups.iter().map(|g| g.panels.clone()).collect();
assert_eq!(groups, vec![vec![2, 3], vec![8], vec![4]]);
}
+3 -1
View File
@@ -112,7 +112,9 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> 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<P>) -> Option<LayerId> {
window.panels().next().map(|first| LayerId::new(Order::Middle, id.with(("floating", first))))
}
+9 -11
View File
@@ -412,15 +412,13 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> 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<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> 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<P>, pointer: Pos2) -> Option<(Option<Action<P>>, Rect)> {
pub(super) fn dock_group_proposal(&self, drag: &GroupDrag<P>, pointer: Pos2) -> Option<(Option<Action<P>>, 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<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> 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<Action<P>>, Rect)> {
if self.dock_covered(ctx, pointer, None) {
pub(super) fn dock_tab_proposal(&self, panel: &P, pointer: Pos2) -> Option<(Option<Action<P>>, Rect)> {
if self.dock_covered(pointer, None) {
return None;
}
let candidates = self.dock_candidates();
+2 -2
View File
@@ -310,8 +310,8 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> 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<P>, 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<P>, pointer: Pos2, float_only: bool) {
if !float_only && let Some((action, _)) = self.dock_group_proposal(drag, pointer) {
self.output.actions.extend(action);
return;
}
+4 -4
View File
@@ -66,7 +66,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> 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<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> 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<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> 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<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> 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::<P>(ctx, self.id);
+3 -1
View File
@@ -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.