docking: a canvas body takes no drop in the docking profile (#323)

With new_dock_zones (and without side_edge_zones), the body of a frame or
floating group that takes no tabs is no drop target, whatever its
accepts_splits. A floating window released there stays floating and a
docked panel floats as over empty space; the outer edge band, dock columns,
new docks and drops beside other groups still dock. Without new_dock_zones
the whole canvas body still goes to the nearest of drop_edges.
This commit is contained in:
aiongg authored and GitHub committed 2026-10-25 12:07:15 -04:00
1 parent 7010c5e93e
commit bab74e1f4c
6 files changed
+115 -30

No files matched your search

+15 -6
View File
@@ -122,16 +122,17 @@ fn a_drop_on_a_groups_outer_third_docks_beside_it_with_its_rect_and_its_middle_j
}
#[test]
fn a_group_that_takes_no_tabs_maps_its_whole_body_to_its_edges() {
fn without_new_dock_zones_a_group_that_takes_no_tabs_maps_its_whole_body_to_its_edges() {
let canvas = Permissions { accepts_tabs: false, ..Permissions::default() };
let d = driver(&[(1, canvas)]);
let start = || driver(&[(1, canvas)]).with_style(|style| style.new_dock_zones = false);
let d = start();
let (body, rect) = group(&d, 1);
for (zone, at) in [(Zone::Left, body.center() - vec2(10.0, 0.0)), (Zone::Top, body.center() - vec2(0.0, 10.0))] {
let out = previews_result(driver(&[(1, canvas)]), tab(&d, 3), at, |d| group(d, 3).1);
let out = previews_result(start(), tab(&d, 3), at, |d| group(d, 3).1);
assert_eq!(out, vec![Action::Move { panel: 3, anchor: 1, placement: Placement::Split(zone) }], "{zone:?}");
}
// Its strip joins nothing.
let mut d = driver(&[(1, canvas)]);
let mut d = start();
let out = d.drag(tab(&d, 3), pos2(rect.center().x, rect.top() + 13.0));
assert!(!out.actions.iter().any(|a| matches!(a, Action::Move { anchor: 1, placement: Placement::Tab { .. }, .. })), "{:?}", out.actions);
}
@@ -345,7 +346,7 @@ fn drop_edges_limit_the_outer_edges_and_the_canvas_sides() {
}
#[test]
fn a_canvas_that_takes_splits_takes_them_only_on_its_drop_edges() {
fn without_new_dock_zones_a_canvas_that_takes_splits_takes_them_only_on_its_drop_edges() {
let beside = Permissions { accepts_splits: true, ..Permissions::PROTECTED };
let d = canvas_driver(beside, |_| {});
let (body, _) = group(&d, 0);
@@ -358,12 +359,20 @@ fn a_canvas_that_takes_splits_takes_them_only_on_its_drop_edges() {
(Sides::NONE, [None, None]),
] {
for (at, zone) in [low, high].into_iter().zip(zones) {
let mut d = canvas_driver(beside, move |style| style.drop_edges = edges);
let split_frames = move |style: &mut DockStyle| {
style.new_dock_zones = false;
style.drop_edges = edges;
};
let mut d = canvas_driver(beside, split_frames);
let out = d.drag(tab(&d, 3), at);
match zone {
Some(zone) => assert_eq!(out.actions, vec![Action::Move { panel: 3, anchor: 0, placement: Placement::Split(zone) }], "{edges:?}"),
None => assert!(!moves(&out), "{edges:?}: {:?}", out.actions),
}
// With new dock zones the canvas body takes no drop on any side.
let mut d = canvas_driver(beside, move |style| style.drop_edges = edges);
let out = d.drag(tab(&d, 3), at);
assert!(!moves(&out), "{edges:?}: {:?}", out.actions);
}
}
}
+54
View File
@@ -17,6 +17,8 @@ const ORIGIN: Pos2 = pos2(100.0, 50.0);
struct Content {
/// Panels that may not close.
fixed: Vec<u32>,
/// The protected canvas that takes splits beside it, as PhotoCraft's does.
canvas: Option<u32>,
/// Panels that stay in-window.
in_window: Vec<u32>,
/// Viewer panels: their windows are normal windows.
@@ -121,6 +123,9 @@ impl DockContent<u32> for Content {
}
fn permissions(&self, panel: &u32) -> Permissions {
if self.canvas == Some(*panel) {
return Permissions { accepts_splits: true, ..Permissions::PROTECTED };
}
Permissions { closable: !self.fixed.contains(panel), ..Permissions::default() }
}
@@ -982,6 +987,55 @@ fn a_native_windows_bar_docks_it_at_frame_edges_and_dock_lines_but_not_a_body_ce
assert!(in_root(&d, 5) && in_root(&d, 6));
}
#[test]
fn over_a_canvas_body_a_window_stays_floating_and_a_tab_floats_while_the_edge_bands_dock() {
// PhotoCraft: a dock column of [2] above [3, 4] beside canvas 0, which takes splits, and
// window [5, 6] floating.
let accent = Color32::from_rgb(250, 10, 200);
let dock = Node::Dock {
side: super::Side::Start,
columns: vec![super::Column::new(vec![super::Group::new(vec![2], 0), super::Group::new(vec![3, 4], 0)], 240.0)],
};
let root = Node::Split { axis: SplitAxis::Horizontal, size: SplitSize::FixedFirst(240.0), first: Box::new(dock), second: Box::new(tabs(&[0])) };
let layout = Layout { root: Some(root), floating: vec![Floating::single(vec![5, 6], 0, [300.0, 80.0, 240.0, 200.0])] };
let start = || {
DockDriver::new(layout.clone(), Content { canvas: Some(0), ..Content::default() })
.with_style(move |s| {
s.accent = accent;
s.float_hint = false;
})
.with_native_host(ORIGIN)
};
// The canvas spans x = 244 to 800 and the whole height: no tab strip.
for at in [pos2(520.0, 150.0), pos2(520.0, 450.0)] {
let mut d = start();
let before = accented(d.shapes(), accent);
let id = window(&d, 5).id;
d.window_step(id, vec![Event::PointerMoved(pos2(40.0, 9.0)), button(pos2(40.0, 9.0), true)]);
d.window_step(id, vec![Event::PointerMoved(pos2(60.0, 19.0))]);
let local = d.window_local(id, ORIGIN + at.to_vec2()).unwrap();
d.window_step(id, vec![Event::PointerMoved(local)]);
d.step(vec![]);
assert_eq!(accented(d.shapes(), accent), before, "{at:?}: no preview");
let out = d.window_step(id, vec![button(pos2(40.0, 9.0), false)]);
assert_eq!(window_move(&out), None, "{at:?}: {:?}", out.actions);
assert!(window(&d, 5).contains(&6), "{at:?}");
}
// A docked tab dropped there floats.
let mut d = start();
let tab = d.hit(&Hit::Tab(3)).unwrap().center();
let out = d.drag(tab, pos2(520.0, 450.0));
assert!(matches!(out.actions.as_slice(), [Action::Float { panel: 3, .. }]), "{:?}", out.actions);
// The window's right edge, where no dock is yet, and the bottom edge band beside the canvas
// still dock the window.
let mut d = start();
let out = drag_bar_to(&mut d, 5, pos2(795.0, 300.0));
assert_eq!(window_move(&out), Some(&Target::NewDock { side: super::Side::End }), "{:?}", out.actions);
let mut d = start();
let out = drag_bar_to(&mut d, 5, pos2(520.0, 590.0));
assert!(matches!(window_move(&out), Some(Target::At { placement: Placement::Edge(super::Zone::Bottom), .. })), "{:?}", out.actions);
}
#[test]
fn escape_during_a_bar_drag_puts_the_window_back() {
let mut d = driver(layout(true));
+5 -5
View File
@@ -198,11 +198,11 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> 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. A drop that would change only sizes is a no-op ([`Self::settled`]).
/// canvas sides of [`crate::docking::DockStyle::drop_edges`] (none in the docking profile),
/// 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. A drop that would change only sizes is a no-op ([`Self::settled`]).
fn resolve_roomy(&self, panel: &P, pointer: Pos2, targets: &[DropTarget<P>], action: impl Fn(Target<P>) -> Action<P>) -> Option<DropProposal<P>> {
let proposal = resolve_drop_on(panel, pointer, targets, self.style.zone_rule(), self.style.drop_sides())?;
let proposal = resolve_drop_on(panel, pointer, targets, self.style.zone_rule(), self.style.canvas_sides())?;
if !matches!(proposal.placement, Placement::Split(zone) if zone != Zone::Center) {
return Some(self.settled(proposal, action));
}
@@ -220,7 +220,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
pointer,
std::slice::from_ref(&DropTarget { allow_splits: false, ..target.clone() }),
self.style.zone_rule(),
self.style.drop_sides(),
self.style.canvas_sides(),
)?;
Some(self.settled(proposal, action))
}
+20 -8
View File
@@ -64,11 +64,13 @@ pub struct DockStyle {
/// bottom edges add a full-width row, a group's zones are [`Self::side_zone_fraction`] of its
/// body, and every preview is a line or a tab outline.
pub side_edge_zones: bool,
/// Which sides of a group that takes no tabs (a canvas), and of the layout's outer edge, take
/// a drop (all four). Without [`Self::side_edge_zones`] the outer left and right edges are
/// new docks, so only top and bottom apply there. A dock-column app whose palettes dock only
/// at the left and right sets [`crate::docking::Sides::NONE`] with [`Self::new_dock_zones`]
/// on. Not used with [`Self::side_edge_zones`]. Groups that take tabs keep all four sides.
/// Which sides of the layout's outer edge, and of a group that takes no tabs (a canvas)
/// without [`Self::new_dock_zones`], take a drop (all four). With [`Self::new_dock_zones`]
/// a canvas body takes no drop, and without [`Self::side_edge_zones`] the outer left and
/// right edges are new docks, so only top and bottom apply there. A dock-column app whose
/// palettes dock only at the left and right sets [`crate::docking::Sides::NONE`] with
/// [`Self::new_dock_zones`] on. Not used with [`Self::side_edge_zones`]. Groups that take
/// tabs keep all four sides.
pub drop_edges: super::Sides,
/// The share of a group's body, below the tab strip, that means "beside" on each side: of
/// its width from the left and right edges, of its height from the top and bottom edges
@@ -81,8 +83,10 @@ pub struct DockStyle {
/// zero). Other drops beside a group, column, dock or the layout's edge preview the rect the
/// moved panels take. Border bands reach 4 pt past this line into a tab strip.
pub drop_line_width: f32,
/// [D] The docked area's left and right edges, where no dock is yet, take a drop as a new
/// dock (see [`Self::new_dock_band`]).
/// [D] The docking profile. The docked area's left and right edges, where no dock is yet,
/// take a drop as a new dock (see [`Self::new_dock_band`]). Without
/// [`Self::side_edge_zones`], the body of a frame or floating group that takes no tabs (a
/// canvas) takes no drop: a release there floats as over no target.
pub new_dock_zones: bool,
/// [D] Height of the bar above each dock column. It drags the column and holds its »/« button.
pub column_bar_height: f32,
@@ -237,12 +241,20 @@ impl DockStyle {
}
}
/// The canvas sides [`Self::drop_edges`] gives in this profile: all four with
/// The sides [`Self::drop_edges`] gives in this profile: all four with
/// [`Self::side_edge_zones`].
pub(super) fn drop_sides(&self) -> super::Sides {
if self.side_edge_zones { super::Sides::ALL } else { self.drop_edges }
}
/// The sides of a frame or floating group that takes no tabs (a canvas) whose body takes a
/// drop: none in the docking profile ([`Self::new_dock_zones`] without
/// [`Self::side_edge_zones`]), where only the bands along borders dock beside it, else
/// [`Self::drop_sides`].
pub(super) fn canvas_sides(&self) -> super::Sides {
if self.side_edge_zones || !self.new_dock_zones { self.drop_sides() } else { super::Sides::NONE }
}
/// The outer edges that take a drop: all four with [`Self::side_edge_zones`], else the top
/// and bottom of [`Self::drop_edges`].
pub(super) fn outer_edges(&self) -> super::Sides {
+7 -4
View File
@@ -4,6 +4,8 @@ use super::{Placement, Side, Target, Zone};
/// One visible group, supplied in back-to-front order to [`resolve_drop`]. An empty tab strip
/// or a group that rejects tabs may still accept side splits, e.g. a protected document canvas.
/// The dock area's docking profile ([`crate::docking::DockStyle::new_dock_zones`]) gives the
/// body of a frame or floating group that rejects tabs no target at all.
#[derive(Clone, Debug)]
pub struct DropTarget<P> {
pub anchor: P,
@@ -75,9 +77,10 @@ pub fn drop_zone(rect: Rect, tab_height: f32, depth: f32, pointer: Pos2) -> Opti
/// In the body below it, the outer `fraction` of its width from the left or right edge, or of
/// its height from the top or bottom edge, is that side's zone; where two sides' zones meet, the
/// side the pointer is relatively nearer wins. The rest is the center. Without `center` (a group
/// that takes no tabs) the whole body goes to the relatively nearest side. A `fraction` of zero
/// or less leaves only the center; more than one half acts as one half. Outside, non-finite,
/// empty and inverted rectangles have no target.
/// that takes no tabs) the whole body goes to the relatively nearest side, as the dock area has
/// it without [`crate::docking::DockStyle::new_dock_zones`]; with it, such a body is no target.
/// A `fraction` of zero or less leaves only the center; more than one half acts as one half.
/// Outside, non-finite, empty and inverted rectangles have no target.
pub fn body_zone(rect: Rect, tab_height: f32, fraction: f32, center: bool, pointer: Pos2) -> Option<Zone> {
body_zone_on(rect, tab_height, fraction, center, Sides::ALL, pointer)
}
@@ -271,7 +274,7 @@ pub fn resolve_drop_by<P: Clone + Eq>(panel: &P, pointer: Pos2, targets: &[DropT
}
/// [`resolve_drop_by`] where a group that takes no tabs splits only on `sides` with
/// [`ZoneRule::Fraction`].
/// [`ZoneRule::Fraction`], and with none is no target.
pub(super) fn resolve_drop_on<P: Clone + Eq>(
panel: &P,
pointer: Pos2,
+14 -7
View File
@@ -239,8 +239,14 @@ A flyout fits its content the same way, without the title bar.
beside the group on that side. Where two sides meet, at a corner, the side the pointer is
relatively nearer wins.
- The middle third joins the group's tabs.
- A group that takes no tabs, such as a protected canvas, has no middle: every point of its
body docks beside it on the relatively nearest side. Its strip takes nothing.
- A group that takes no tabs, such as a protected canvas, takes nothing on its strip, and its
body has no middle: every point docks beside it on the relatively nearest side.
- In the docking profile (`new_dock_zones` without `side_edge_zones`), the body of a frame or
floating group that takes no tabs takes nothing, whatever its `accepts_splits`: a release
there is a release over no target, so a floating window stays floating and a docked panel
or group floats as `float_on_empty_drop` says. Only the bands along borders (below) dock
beside it, as in Adobe apps, whose document area is no drop zone. A dock column's group
keeps its sides.
- Beside a frame is a split of that frame. Beside a group in a dock column, the top and bottom
stack a new group above or below it, and the left and right make a new column on that side.
- A frame too narrow or short for two halves of `DockStyle::min_pane` still splits when the
@@ -276,11 +282,12 @@ A flyout fits its content the same way, without the title bar.
`window.panel.dock` to an outer edge picks its anchor the same way, from the permissions
`apply` (or `to_actions`) gets and the area's `FloatSpace` (`with_side_edge_zones` before it
has drawn).
- `DockStyle::drop_edges` (a `Sides`, all four by default) lists which sides of a group that
takes no tabs (a canvas), and of the layout's outer edge, take a drop. A canvas's whole body
goes to the nearest listed side; with none listed it takes nothing. In the docking profile
the outer left and right edges are new docks, so only top and bottom apply there. Groups
that take tabs keep all four sides. A dock-column app whose palettes dock only at the left
- `DockStyle::drop_edges` (a `Sides`, all four by default) lists which sides of the layout's
outer edge, and of a group that takes no tabs (a canvas) without `new_dock_zones`, take a
drop. There a canvas's whole body goes to the nearest listed side; with none listed it takes
nothing. In the docking profile the canvas body takes no drop and the outer left and right
edges are new docks, so only the outer top and bottom apply. Groups that take tabs keep all
four sides. A dock-column app whose palettes dock only at the left
and right, as in AutoCAD, sets `Sides::NONE` with `new_dock_zones` on. Not used with
`side_edge_zones`.
- With `side_edge_zones` (the split-tree arrangement), a frame's zones are a picture frame