docking: narrow the outer edge band to the canvas column and add DockStyle::drop_edges

This commit is contained in:
aiongg committed 2026-10-25 05:18:09 +00:00
1 parent 02d7d92a3e
commit 889f0367f2
9 files changed
+290 -54

No files matched your search

+14 -6
View File
@@ -454,8 +454,10 @@ impl<P: Clone + Eq + Hash> Layout<P> {
None
}
/// The node an edge split wraps: the root, or the part between the docks.
pub(in crate::docking) fn edge_path(&self) -> Vec<bool> {
/// The node an edge split wraps: the root, or the part between the docks. Where neither side
/// of a side-by-side split is a dock and only one holds a `canvas` panel, that side: the
/// canvas column, without the side frames.
pub(in crate::docking) fn edge_path(&self, canvas: &dyn Fn(&P) -> bool) -> Vec<bool> {
let mut path = Vec::new();
let mut node = self.root.as_ref();
while let Some(Node::Split { axis: SplitAxis::Horizontal, first, second, .. }) = node {
@@ -465,7 +467,11 @@ impl<P: Clone + Eq + Hash> Layout<P> {
let second_side = match (&**first, &**second) {
(Node::Dock { .. }, _) => true,
(_, Node::Dock { .. }) => false,
_ => break,
_ => match (tree_panels(first).into_iter().any(canvas), tree_panels(second).into_iter().any(canvas)) {
(true, false) => false,
(false, true) => true,
_ => break,
},
};
path.push(second_side);
node = Some(if second_side { second } else { first });
@@ -666,7 +672,8 @@ impl<P: Clone + Eq + Hash> Layout<P> {
}
/// Insert groups taken by [`Self::take_unit`] (or a new single-panel group).
pub(super) fn insert_moving(&mut self, moving: Moving<P>, to: Target<P>) -> Result<(), DockError> {
/// `canvas` is the protected-panel test an outer edge drop narrows by ([`Self::edge_path`]).
pub(super) fn insert_moving(&mut self, moving: Moving<P>, to: Target<P>, canvas: &dyn Fn(&P) -> bool) -> Result<(), DockError> {
if moving.groups.is_empty() || moving.groups.iter().any(|g| g.panels.is_empty()) {
return Err(DockError::MissingPanel);
}
@@ -735,7 +742,7 @@ impl<P: Clone + Eq + Hash> Layout<P> {
// A floating window is a tree: a group above or below splits the anchor's group.
Placement::Group { below, .. } if matches!(place, Place::Floating { .. }) => {
let zone = if below { Zone::Bottom } else { Zone::Top };
self.insert_moving(moving, Target::At { anchor, placement: Placement::Split(zone) })
self.insert_moving(moving, Target::At { anchor, placement: Placement::Split(zone) }, canvas)
}
Placement::Group { below, height } => {
if !valid_height(height) {
@@ -757,7 +764,8 @@ impl<P: Clone + Eq + Hash> Layout<P> {
if zone == Zone::Center || matches!(place, Place::Floating { .. }) {
return Err(DockError::InvalidPlacement);
}
let path = self.edge_path();
// A protected anchor narrows the edge to the canvas column.
let path = if canvas(&anchor) { self.edge_path(canvas) } else { self.edge_path(&|_| false) };
let node = self.root.as_mut().and_then(|root| node_at_mut(root, &path)).ok_or(DockError::MissingTarget)?;
if matches!(node, Node::Dock { .. }) {
return Err(DockError::InvalidPlacement);
+128 -3
View File
@@ -133,8 +133,13 @@ fn 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 (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 mut d = driver(&[(1, canvas)]);
// By default only its top and bottom: left of center goes to the nearer of those.
for (zone, at, edges) in [
(Zone::Left, body.center() - vec2(10.0, 2.0), Sides::ALL),
(Zone::Top, body.center() - vec2(10.0, 2.0), DockStyle::default().drop_edges),
(Zone::Bottom, body.center() + vec2(-10.0, 2.0), DockStyle::default().drop_edges),
] {
let mut d = driver(&[(1, canvas)]).with_style(move |style| style.drop_edges = edges);
d.hold(tab(&d, 3), at);
assert!(d.filled(line(rect, zone)), "{zone:?}");
let out = d.release(at);
@@ -166,7 +171,7 @@ fn a_side_without_room_joins_as_a_tab_or_offers_nothing() {
let out = d.drag(tab(&d, 3), top);
assert_eq!(out.actions, vec![Action::Move { panel: 3, anchor: 1, placement: Placement::Split(Zone::Top) }]);
// A group that takes no tabs offers nothing there.
let mut d = narrow(&[(1, Permissions { accepts_tabs: false, ..Permissions::default() })]);
let mut d = narrow(&[(1, Permissions { accepts_tabs: false, ..Permissions::default() })]).with_style(|style| style.drop_edges = Sides::ALL);
d.hold(tab(&d, 3), left);
assert!(!d.outlined(rect.shrink(1.0)));
let out = d.release(left);
@@ -195,3 +200,123 @@ fn the_root_edge_band_is_28_pt_and_previews_a_line_along_the_layouts_edge() {
let out = d.drag(tab(&d, 3), pos2(x, 7.0));
assert!(matches!(&out.actions[..], [Action::Move { panel: 3, placement: Placement::Edge(Zone::Top), .. }]), "{:?}", out.actions);
}
fn tabs(panels: &[u32]) -> Node<u32> {
Node::Tabs { panels: panels.to_vec(), active: 0 }
}
fn split(axis: SplitAxis, size: SplitSize, first: Node<u32>, second: Node<u32>) -> Node<u32> {
Node::Split { axis, size, first: Box::new(first), second: Box::new(second) }
}
/// Side frame `[1]`, canvas 0 and side frame `[2, 3]` side by side, 200, 400 and 200 pt wide.
fn canvas_columns() -> Layout<u32> {
let sides = split(SplitAxis::Horizontal, SplitSize::FixedSecond(200.0), tabs(&[0]), Node::Tabs { panels: vec![2, 3], active: 1 });
Layout { root: Some(split(SplitAxis::Horizontal, SplitSize::FixedFirst(200.0), tabs(&[1]), sides)), floating: Vec::new() }
}
fn canvas_driver(canvas: Permissions, style: impl Fn(&mut DockStyle) + 'static) -> Driver {
DockDriver::new(canvas_columns(), Bodies { rules: [(0, canvas)].into(), ..Bodies::default() }).with_style(style)
}
fn moves(out: &DockOutput<u32>) -> bool {
out.actions.iter().any(|a| matches!(a, Action::Move { .. } | Action::MovePanel { .. }))
}
#[test]
fn between_side_frames_the_outer_edge_band_covers_only_the_canvas_column() {
let d = canvas_driver(Permissions::PROTECTED, |_| {});
let (canvas, _) = group(&d, 0);
let (side, _) = group(&d, 1);
let at = pos2(canvas.center().x, 590.0);
let mut d = canvas_driver(Permissions::PROTECTED, |_| {});
d.hold(tab(&d, 3), at);
assert!(d.filled(line(canvas, Zone::Bottom)), "a line along the canvas column's bottom");
assert!(!d.filled(Rect::from_x_y_ranges(0.0..=800.0, 596.0..=600.0)), "no line along the window's bottom");
let out = d.release(at);
assert_eq!(out.actions, vec![Action::Move { panel: 3, anchor: 0, placement: Placement::Edge(Zone::Bottom) }]);
// The side frames keep their full height.
let column = split(SplitAxis::Vertical, SplitSize::Ratio(0.75), tabs(&[0]), tabs(&[3]));
let sides = split(SplitAxis::Horizontal, SplitSize::FixedSecond(200.0), column, tabs(&[2]));
assert_eq!(d.layout.root, Some(split(SplitAxis::Horizontal, SplitSize::FixedFirst(200.0), tabs(&[1]), sides)));
// Under a side frame, its own bottom third.
let mut d = canvas_driver(Permissions::PROTECTED, |_| {});
let out = d.drag(tab(&d, 3), pos2(side.center().x, 590.0));
assert_eq!(out.actions, vec![Action::Move { panel: 3, anchor: 1, placement: Placement::Split(Zone::Bottom) }]);
}
#[test]
fn between_docks_the_outer_edge_band_covers_the_part_between_them() {
let dock = |side, panels: &[u32]| Node::Dock { side, columns: vec![Column::new(vec![Group::new(panels.to_vec(), 0)], 200.0)] };
let middle = split(SplitAxis::Vertical, SplitSize::Ratio(0.7), tabs(&[0]), tabs(&[4]));
let inner = split(SplitAxis::Horizontal, SplitSize::FixedSecond(200.0), middle.clone(), dock(Side::End, &[2, 3]));
let layout =
Layout { root: Some(split(SplitAxis::Horizontal, SplitSize::FixedFirst(200.0), dock(Side::Start, &[1]), inner)), floating: Vec::new() };
let driver = || DockDriver::new(layout.clone(), Bodies { rules: [(0, Permissions::PROTECTED)].into(), ..Bodies::default() });
let d = driver();
let (canvas, _) = group(&d, 0);
let (timeline, _) = group(&d, 4);
let area = Rect::from_x_y_ranges(canvas.x_range(), (canvas.top() - 26.0)..=timeline.bottom());
let at = pos2(canvas.center().x, 590.0);
let mut d = driver();
d.hold(tab(&d, 3), at);
assert!(d.filled(line(area, Zone::Bottom)), "a line along the bottom between the docks");
let out = d.release(at);
assert!(matches!(&out.actions[..], [Action::Move { panel: 3, placement: Placement::Edge(Zone::Bottom), .. }]), "{:?}", out.actions);
let Some(Node::Split { second, .. }) = &d.layout.root else { panic!("{:?}", d.layout) };
let Node::Split { first, .. } = &**second else { panic!("{:?}", d.layout) };
assert_eq!(**first, split(SplitAxis::Vertical, SplitSize::Ratio(0.75), middle, tabs(&[3])));
}
#[test]
fn drop_edges_limit_the_outer_edges_and_the_canvas_sides() {
let left_right = |style: &mut DockStyle| style.drop_edges = Sides::LEFT_RIGHT;
let d = canvas_driver(Permissions::PROTECTED, left_right);
// The canvas has no tab strip: its body is its frame.
let (canvas, _) = group(&d, 0);
let (side, _) = group(&d, 1);
// The bottom edge takes nothing.
let at = pos2(canvas.center().x, 590.0);
let mut d = canvas_driver(Permissions::PROTECTED, left_right);
d.hold(tab(&d, 3), at);
assert!(!d.filled(line(canvas, Zone::Bottom)));
assert!(!moves(&d.release(at)));
// The left edge splits around the canvas column.
let at = pos2(canvas.left() + 5.0, canvas.center().y);
let mut d = canvas_driver(Permissions::PROTECTED, left_right);
d.hold(tab(&d, 3), at);
assert!(d.filled(line(canvas, Zone::Left)), "a line along the canvas column's left edge");
let out = d.release(at);
assert_eq!(out.actions, vec![Action::Move { panel: 3, anchor: 0, placement: Placement::Edge(Zone::Left) }]);
let column = split(SplitAxis::Horizontal, SplitSize::Ratio(0.25), tabs(&[3]), tabs(&[0]));
let sides = split(SplitAxis::Horizontal, SplitSize::FixedSecond(200.0), column, tabs(&[2]));
assert_eq!(d.layout.root, Some(split(SplitAxis::Horizontal, SplitSize::FixedFirst(200.0), tabs(&[1]), sides)));
// A side frame keeps all four sides.
for (zone, y) in [(Zone::Bottom, 590.0), (Zone::Top, side.top() + side.height() * 0.2)] {
let mut d = canvas_driver(Permissions::PROTECTED, left_right);
let out = d.drag(tab(&d, 3), pos2(side.center().x, y));
assert_eq!(out.actions, vec![Action::Move { panel: 3, anchor: 1, placement: Placement::Split(zone) }], "{zone:?}");
}
// No drop edges: no outer edge takes a drop.
for at in [pos2(canvas.center().x, 590.0), pos2(canvas.center().x, 5.0), pos2(canvas.left() + 5.0, 300.0), pos2(canvas.right() - 5.0, 300.0)] {
let mut d = canvas_driver(Permissions::PROTECTED, |style| style.drop_edges = Sides::NONE);
let out = d.drag(tab(&d, 3), at);
assert!(!moves(&out), "{at:?}: {:?}", out.actions);
}
}
#[test]
fn 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);
let at = pos2(body.left() + body.width() * 0.3, body.bottom() - 40.0);
for (edges, zone) in [(DockStyle::default().drop_edges, Some(Zone::Bottom)), (Sides::LEFT_RIGHT, Some(Zone::Left)), (Sides::NONE, None)] {
let mut d = canvas_driver(beside, move |style| style.drop_edges = edges);
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), "{:?}", out.actions),
}
}
}
+2 -2
View File
@@ -35,8 +35,8 @@ pub use surface::{
WindowFiles, WindowKind, begin_drag, cancel_drag, float_space, is_dragging, route_app_drag,
};
pub use targets::{
DropProposal, DropTarget, SIDE_EDGE_ZONE_DEPTH, ZoneRule, body_zone, drop_zone, resolve_drop, resolve_drop_by, tab_insertion_marker, zone_rect,
zone_shape,
DropProposal, DropTarget, SIDE_EDGE_ZONE_DEPTH, Sides, ZoneRule, body_zone, drop_zone, resolve_drop, resolve_drop_by, tab_insertion_marker,
zone_rect, zone_shape,
};
pub use workspace::Workspace;
+25 -19
View File
@@ -318,7 +318,10 @@ pub enum Placement<P> {
Column { after: bool },
/// A new frame along one edge of the docked layout, a quarter of its width or height: the
/// root is split, or in a layout with docks, the part between them. The anchor names any
/// docked panel. `Center` is invalid.
/// docked panel. A protected anchor ([`Permissions::is_protected`], applied through
/// [`Layout::apply_with_permissions`]) narrows the part to the canvas column: where neither
/// side of a side-by-side split is a dock and only one holds a protected panel, that side.
/// `Center` is invalid.
Edge(Zone),
}
@@ -819,14 +822,14 @@ impl<P: Clone + Eq + Hash> Layout<P> {
let window_open = location.anchor.as_ref().is_some_and(|anchor| proposed.floating.iter().any(|w| w.contains(anchor)));
if let Some(rect) = location.floating.filter(|_| !window_open) {
// A window that closed as a whole reopens at its last rect.
proposed.insert_moving(moving(), Target::Float { rect: Some(rect) })?;
proposed.insert_moving(moving(), Target::Float { rect: Some(rect) }, &|_| false)?;
} else if let Some(anchor) = &location.anchor
&& matches!(location.placement, Placement::Group { .. } | Placement::Column { .. })
{
if !proposed.contains(anchor) {
return Err(DockError::MissingTarget);
}
proposed.insert_moving(moving(), Target::At { anchor: anchor.clone(), placement: location.placement.clone() })?;
proposed.insert_moving(moving(), Target::At { anchor: anchor.clone(), placement: location.placement.clone() }, &|_| false)?;
} else if let Some(column) = state.and_then(|s| s.column).filter(|_| location.anchor.is_none()) {
// Alone in its dock: back at that edge, as the outermost column if a dock exists.
match proposed.dock_path(column.side) {
@@ -835,7 +838,7 @@ impl<P: Clone + Eq + Hash> Layout<P> {
let Moving { groups, .. } = moving();
columns.insert(0, Column { groups, width: column.width, iconic: column.iconic, iconic_width: column.iconic_width });
}
None => proposed.insert_moving(moving(), Target::NewDock { side: column.side })?,
None => proposed.insert_moving(moving(), Target::NewDock { side: column.side }, &|_| false)?,
}
} else if let Some(anchor) = &location.anchor {
if !proposed.contains(anchor) {
@@ -862,7 +865,7 @@ impl<P: Clone + Eq + Hash> Layout<P> {
Placement::Tab { before: Some(before) } if !proposed.same_group(before, anchor) => Placement::Tab { before: None },
placement => placement.clone(),
};
proposed.apply_inner(Action::OpenAt { panel: panel.clone(), anchor: anchor.clone(), placement })?;
proposed.apply_inner(Action::OpenAt { panel: panel.clone(), anchor: anchor.clone(), placement }, &|_| false)?;
if let Some(size) = location.size {
proposed.trees_mut().any(|root| restore_parent_size(root, &panel, size));
}
@@ -899,7 +902,7 @@ impl<P: Clone + Eq + Hash> Layout<P> {
.find(|p| matches!(self.place(p), Some(Place::Dock { .. } | Place::Node(_))))
.cloned()
.ok_or(DockError::MissingTarget)?;
proposed.apply_inner(Action::OpenAt { panel: panel.clone(), anchor, placement: Placement::Edge(Zone::Right) })?;
proposed.apply_inner(Action::OpenAt { panel: panel.clone(), anchor, placement: Placement::Edge(Zone::Right) }, &|_| false)?;
if let Some(root) = proposed.root.as_mut() {
restore_parent_size(root, &panel, SplitSize::Ratio(0.25));
}
@@ -987,7 +990,7 @@ impl<P: Clone + Eq + Hash> Layout<P> {
}
let closed = self.closed_by(&action)?;
let mut proposed = self.clone();
proposed.apply_inner(action)?;
proposed.apply_inner(action, &|p| permissions(p).is_protected())?;
proposed.validate()?;
*self = proposed;
Ok(closed)
@@ -1017,7 +1020,8 @@ impl<P: Clone + Eq + Hash> Layout<P> {
})
}
fn apply_inner(&mut self, action: Action<P>) -> Result<(), DockError> {
/// `canvas` is the protected-panel test an outer edge drop narrows by.
fn apply_inner(&mut self, action: Action<P>, canvas: &dyn Fn(&P) -> bool) -> Result<(), DockError> {
match action {
Action::Move { panel, anchor, placement } => {
if panel == anchor {
@@ -1036,7 +1040,7 @@ impl<P: Clone + Eq + Hash> Layout<P> {
}
}
let entry = self.take(&panel).ok_or(DockError::MissingPanel)?;
self.insert(entry, &anchor, placement)
self.insert(entry, &anchor, placement, canvas)
}
Action::Float { panel, rect } => {
if !valid_rect(rect) {
@@ -1070,11 +1074,11 @@ impl<P: Clone + Eq + Hash> Layout<P> {
}
Action::Open { panel, anchor } => {
if self.contains(&panel) {
return self.apply_inner(Action::Activate { panel });
return self.apply_inner(Action::Activate { panel }, canvas);
}
let entry = StackEntry { panel, open: true, height: None };
if let Some(anchor) = anchor {
self.insert(entry, &anchor, Placement::Tab { before: None })
self.insert(entry, &anchor, Placement::Tab { before: None }, canvas)
} else if self.root.is_none() {
self.root = Some(Node::Tabs { panels: vec![entry.panel], active: 0 });
Ok(())
@@ -1084,14 +1088,14 @@ impl<P: Clone + Eq + Hash> Layout<P> {
}
Action::OpenAt { panel, anchor, placement } => {
if self.contains(&panel) {
return self.apply_inner(Action::Move { panel, anchor, placement });
return self.apply_inner(Action::Move { panel, anchor, placement }, canvas);
}
if let Placement::Tab { before: Some(before) } = &placement
&& !self.same_group(before, &anchor)
{
return Err(DockError::MissingTarget);
}
self.insert(StackEntry { panel, open: true, height: None }, &anchor, placement)
self.insert(StackEntry { panel, open: true, height: None }, &anchor, placement, canvas)
}
Action::MoveFloating { panel, rect } => {
if !valid_rect(rect) {
@@ -1142,13 +1146,13 @@ impl<P: Clone + Eq + Hash> Layout<P> {
return Ok(());
}
let moving = self.take_unit(panel, unit)?;
self.insert_moving(moving, to.clone())
self.insert_moving(moving, to.clone(), canvas)
}
Action::MovePanel { panel, to } => match to {
Target::At { anchor, placement } => self.apply_inner(Action::Move { panel, anchor, placement }),
Target::At { anchor, placement } => self.apply_inner(Action::Move { panel, anchor, placement }, canvas),
to => {
let entry = self.take(&panel).ok_or(DockError::MissingPanel)?;
self.insert_moving(Moving::group(Group::new(vec![entry.panel], 0)), to)
self.insert_moving(Moving::group(Group::new(vec![entry.panel], 0)), to, canvas)
}
},
Action::CloseGroup { ref panel } | Action::CloseWindow { ref panel } => self.take_unit(panel, unit_of(&action)).map(|_| ()),
@@ -1285,16 +1289,18 @@ impl<P: Clone + Eq + Hash> Layout<P> {
if let Some((panel, _)) = groups.clone().find(|(_, place)| side(place) == Some(Side::End)).or_else(|| groups.clone().next()) {
return Some(((*panel).clone(), Placement::Tab { before: None }));
}
docked.first().map(|(panel, _)| ((*panel).clone(), Placement::Edge(Zone::Right)))
// A canvas anchor would narrow the edge to the canvas column.
let edge = docked.iter().find(|(p, _)| !permissions(p).is_protected()).or(docked.first());
edge.map(|(panel, _)| ((*panel).clone(), Placement::Edge(Zone::Right)))
}
fn insert(&mut self, entry: StackEntry<P>, anchor: &P, placement: Placement<P>) -> Result<(), DockError> {
fn insert(&mut self, entry: StackEntry<P>, anchor: &P, placement: Placement<P>, canvas: &dyn Fn(&P) -> bool) -> Result<(), DockError> {
let place = self.place(anchor).ok_or(DockError::MissingTarget)?;
let tree = place.tree_path().map(|(window, path)| (window, path.to_vec()));
let Some((window, path)) = tree.filter(|_| !matches!(placement, Placement::Group { .. } | Placement::Column { .. } | Placement::Edge(_)))
else {
let group = Group::new(vec![entry.panel], 0);
return self.insert_moving(Moving::group(group), Target::At { anchor: anchor.clone(), placement });
return self.insert_moving(Moving::group(group), Target::At { anchor: anchor.clone(), placement }, canvas);
};
let root = self.tree_mut(window).ok_or(DockError::MissingTarget)?;
if let Placement::Split(zone) = placement
+31 -14
View File
@@ -28,14 +28,18 @@ pub(in crate::docking) struct FrameState<P> {
pub(super) windows: Vec<WindowHit<P>>,
}
/// The layout as shown: a maximized frame alone, and hidden panels left out.
/// The layout as shown: a maximized frame alone, and hidden panels left out. `narrow` narrows
/// the outer edge area to the canvas column ([`Layout::edge_path`]).
pub(in crate::docking) fn view<'a, P: Clone + Eq + Hash>(
layout: &'a Layout<P>,
maximized: Option<&P>,
hide: HidePanels<'_, P>,
content: &dyn DockContent<P>,
narrow: bool,
) -> (Cow<'a, Layout<P>>, FrameState<P>) {
let protected = |p: &P| content.permissions(p).is_protected();
// The edge area narrows to the canvas column without side edge zones.
let canvas = |p: &P| narrow && protected(p);
let hidden = |p: &P| match hide {
HidePanels::None => false,
HidePanels::All => !protected(p),
@@ -52,7 +56,7 @@ pub(in crate::docking) fn view<'a, P: Clone + Eq + Hash>(
});
let mut state = FrameState { paths: None, window_paths: HashMap::new(), edge_path: None, edge: None, maximized: None, windows: Vec::new() };
if maximized.is_none() && matches!(hide, HidePanels::None) {
state.edge_path = Some(layout.edge_path());
state.edge_path = Some(layout.edge_path(&canvas));
return (Cow::Borrowed(layout), state);
}
let mut paths = HashMap::new();
@@ -77,7 +81,7 @@ pub(in crate::docking) fn view<'a, P: Clone + Eq + Hash>(
}
};
if state.maximized.is_none() {
state.edge_path = Some(view.edge_path());
state.edge_path = Some(view.edge_path(&canvas));
}
state.paths = Some(paths);
(Cow::Owned(view), state)
@@ -166,21 +170,25 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
paths.and_then(|paths| paths.get(path).cloned()).unwrap_or_else(|| path.to_vec())
}
/// The outer edge band under the pointer (P8): top and bottom, and left and right with
/// The outer edge band under the pointer (P8): the sides of
/// [`crate::docking::DockStyle::drop_edges`], all four with
/// [`crate::docking::DockStyle::side_edge_zones`]. Returns a docked anchor that is not
/// moving, the zone and the preview: a line along the layout's whole edge, or with side edge
/// zones the quarter the new frame takes. Without them, on a tab strip that takes tabs, the band reaches
/// only [`crate::docking::targets::STRIP_REACH`] from the line.
/// moving, the zone and the preview: a line along the edge area's whole edge, or with side
/// edge zones the quarter the new frame takes. Without them, on a tab strip that takes tabs,
/// the band reaches only [`crate::docking::targets::STRIP_REACH`] from the line.
pub(super) fn edge_proposal(&self, members: &[P], pointer: Pos2) -> Option<(P, Zone, Rect)> {
let area = self.frame.edge.filter(|area| area.contains(pointer))?;
let band = if self.style.side_edge_zones { crate::docking::targets::SIDE_EDGE_BAND } else { self.style.edge_band };
let band = band.min(area.width() / 4.0).min(area.height() / 4.0);
let line = |zone| crate::docking::targets::edge_line(area, zone, self.style.drop_line_width);
let mut edges = vec![(pointer.y - area.top(), Zone::Top), (area.bottom() - pointer.y, Zone::Bottom)];
if self.style.side_edge_zones {
edges.extend([(pointer.x - area.left(), Zone::Left), (area.right() - pointer.x, Zone::Right)]);
}
let (distance, zone) = edges.into_iter().min_by(|a, b| a.0.total_cmp(&b.0))?;
let sides = self.style.drop_sides();
let edges = [
(pointer.y - area.top(), Zone::Top),
(area.bottom() - pointer.y, Zone::Bottom),
(pointer.x - area.left(), Zone::Left),
(area.right() - pointer.x, Zone::Right),
];
let (distance, zone) = edges.into_iter().filter(|(_, zone)| sides.contains(*zone)).min_by(|a, b| a.0.total_cmp(&b.0))?;
if distance > band {
return None;
}
@@ -188,8 +196,17 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
if !self.style.side_edge_zones && self.in_tab_strip(pointer) && !line(zone).is_some_and(reach) {
return None;
}
let anchor =
self.layout.panels().into_iter().find(|p| !members.contains(p) && !matches!(self.place(p), Some(Place::Floating { .. }) | None))?.clone();
// A protected anchor narrows the model's edge to the canvas column, as `edge_path`
// narrows the band without side edge zones.
let narrow = !self.style.side_edge_zones;
let docked: Vec<&P> = self
.layout
.panels()
.into_iter()
.filter(|p| !members.contains(p) && !matches!(self.place(p), Some(Place::Floating { .. }) | None))
.collect();
let anchor = docked.iter().find(|p| self.permissions(p).is_protected() == narrow).or(docked.first())?;
let anchor = (*anchor).clone();
if !self.style.side_edge_zones {
return Some((anchor, zone, line(zone)?));
}
+11 -4
View File
@@ -8,7 +8,7 @@ use crate::docking::{
Action, Drag, DropProposal, DropRequest, DropTarget, GroupDrag, GroupGesture, Layout, Node, Placement, Target, Zone, cancel_drag,
dock_model::Place,
surface::TAB_PADDING,
targets::{SIDE_EDGE_ZONE_DEPTH, resolve_drop_by},
targets::{SIDE_EDGE_ZONE_DEPTH, resolve_drop_on},
};
use crate::layout::SplitAxis;
@@ -186,11 +186,12 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
(proposal.noop || self.layout.clone().apply_with_permissions(group_action(drag, to), |p| self.permissions(p)).is_ok()).then_some(proposal)
}
/// [`crate::docking::resolve_drop_by`] with [`crate::docking::DockStyle::zone_rule`], where a split that would leave
/// [`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.
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_by(panel, pointer, targets, self.style.zone_rule())?;
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);
}
@@ -198,7 +199,13 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
return Some(proposal);
}
let target = targets.iter().rev().find(|t| crate::docking::targets::usable(t.rect) && t.rect.contains(pointer))?;
resolve_drop_by(panel, pointer, std::slice::from_ref(&DropTarget { allow_splits: false, ..target.clone() }), self.style.zone_rule())
resolve_drop_on(
panel,
pointer,
std::slice::from_ref(&DropTarget { allow_splits: false, ..target.clone() }),
self.style.zone_rule(),
self.style.drop_sides(),
)
}
/// Whether `action`, a drop that splits a group or the outer edge, leaves room: every group
+14 -1
View File
@@ -64,6 +64,12 @@ 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,
/// The layout's outer edges that take a drop, a new frame along that edge, and the sides of
/// a group that takes no tabs (a canvas) that take a split (top and bottom; the left and
/// right window edges are new docks). With [`crate::docking::Sides::NONE`], palettes dock
/// only through the dock bands. Not used with [`Self::side_edge_zones`], which takes all
/// four. 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
/// (one third, at most one half). The middle joins the group as a tab. Not used with
@@ -190,6 +196,7 @@ impl DockStyle {
title_height: super::chrome::TITLE_HEIGHT,
edge_band: 28.0,
side_edge_zones: false,
drop_edges: super::Sides::TOP_BOTTOM,
side_zone_fraction: 1.0 / 3.0,
drop_line_width: 4.0,
new_dock_zones: true,
@@ -227,6 +234,12 @@ impl DockStyle {
}
}
/// 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 }
}
/// How a group's body divides into drop zones: [`crate::docking::SIDE_EDGE_ZONE_DEPTH`] deep
/// with [`Self::side_edge_zones`], else [`Self::side_zone_fraction`] of it, with
/// [`Self::drop_line_width`] previews. Pass it to [`crate::docking::resolve_drop_by`].
@@ -1177,7 +1190,7 @@ impl<'a, P: Clone + Eq + Hash + Debug + Send + Sync + 'static> DockArea<'a, P> {
ui.ctx().data_mut(|d| d.insert_temp(blocked_key, self.input_blocked));
cancel_drag_if_replaced(ui.ctx(), self.id, layout);
release_moved_tab_focus(ui.ctx(), self.id, layout);
let (view, frame) = super::render::view(layout, self.maximized.as_ref(), self.hide, &*content);
let (view, frame) = super::render::view(layout, self.maximized.as_ref(), self.hide, &*content, !style.side_edge_zones);
let places = layout.places();
// A panel keeps the content height it last drew while hidden or minimized.
let contents: Vec<(P, Option<f32>)> = float_space::<P>(ui.ctx(), self.id)
+53 -4
View File
@@ -78,6 +78,12 @@ pub fn drop_zone(rect: Rect, tab_height: f32, depth: f32, pointer: Pos2) -> Opti
/// 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)
}
/// [`body_zone`] where a body without `center` maps only to the nearest of `sides`, and to no
/// target without any.
pub(super) fn body_zone_on(rect: Rect, tab_height: f32, fraction: f32, center: bool, sides: Sides, pointer: Pos2) -> Option<Zone> {
if !usable(rect) || !pointer.is_finite() || !rect.contains(pointer) || !tab_height.is_finite() || tab_height < 0.0 {
return None;
}
@@ -87,13 +93,16 @@ pub fn body_zone(rect: Rect, tab_height: f32, fraction: f32, center: bool, point
}
let fraction = fraction.min(0.5);
let (w, h) = (body.width(), body.height());
let sides = [
let allowed = |zone: &Zone| center || sides.contains(*zone);
let (distance, zone) = [
((pointer.x - body.left()) / w, Zone::Left),
((body.right() - pointer.x) / w, Zone::Right),
((pointer.y - body.top()) / h, Zone::Top),
((body.bottom() - pointer.y) / h, Zone::Bottom),
];
let (distance, zone) = sides.into_iter().min_by(|a, b| a.0.total_cmp(&b.0))?;
]
.into_iter()
.filter(|(_, zone)| allowed(zone))
.min_by(|a, b| a.0.total_cmp(&b.0))?;
Some(if center && distance > fraction { Zone::Center } else { zone })
}
@@ -216,6 +225,34 @@ pub fn resolve_drop<P: Clone + Eq>(panel: &P, pointer: Pos2, targets: &[DropTarg
resolve_drop_by(panel, pointer, targets, ZoneRule::Depth(zone_depth))
}
/// A set of sides: of the layout's outer edge, or of a group's body
/// ([`crate::docking::DockStyle::drop_edges`]).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Sides {
pub left: bool,
pub right: bool,
pub top: bool,
pub bottom: bool,
}
impl Sides {
pub const ALL: Self = Self { left: true, right: true, top: true, bottom: true };
pub const NONE: Self = Self { left: false, right: false, top: false, bottom: false };
pub const LEFT_RIGHT: Self = Self { left: true, right: true, top: false, bottom: false };
pub const TOP_BOTTOM: Self = Self { left: false, right: false, top: true, bottom: true };
/// Whether `zone`'s side is in the set; never the center.
pub fn contains(self, zone: Zone) -> bool {
match zone {
Zone::Left => self.left,
Zone::Right => self.right,
Zone::Top => self.top,
Zone::Bottom => self.bottom,
Zone::Center => false,
}
}
}
/// How [`resolve_drop_by`] divides a group's body.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum ZoneRule {
@@ -229,6 +266,18 @@ pub enum ZoneRule {
/// [`resolve_drop`] with the body zones of `rule`: [`crate::docking::DockStyle::zone_rule`]
/// gives the ones the dock area uses.
pub fn resolve_drop_by<P: Clone + Eq>(panel: &P, pointer: Pos2, targets: &[DropTarget<P>], rule: ZoneRule) -> Option<DropProposal<P>> {
resolve_drop_on(panel, pointer, targets, rule, Sides::ALL)
}
/// [`resolve_drop_by`] where a group that takes no tabs splits only on `sides` with
/// [`ZoneRule::Fraction`].
pub(super) fn resolve_drop_on<P: Clone + Eq>(
panel: &P,
pointer: Pos2,
targets: &[DropTarget<P>],
rule: ZoneRule,
sides: Sides,
) -> Option<DropProposal<P>> {
if !pointer.is_finite() || targets.len() > super::MAX_PANELS {
return None;
}
@@ -270,7 +319,7 @@ pub fn resolve_drop_by<P: Clone + Eq>(panel: &P, pointer: Pos2, targets: &[DropT
let zone = match rule {
_ if !target.allow_splits => Zone::Center,
ZoneRule::Depth(depth) => drop_zone(target.rect, header, depth, pointer)?,
ZoneRule::Fraction { fraction, .. } => body_zone(target.rect, header, fraction, target.accepts_tabs, pointer)?,
ZoneRule::Fraction { fraction, .. } => body_zone_on(target.rect, header, fraction, target.accepts_tabs, sides, pointer)?,
};
if zone == Zone::Center && target.accepts_tabs && target.tabs.iter().any(|(p, _)| p == panel) {
return Some(DropProposal {
+12 -1
View File
@@ -250,12 +250,23 @@ A flyout fits its content the same way, without the title bar.
| The seam between two stacked groups of a dock column | A new group between them | `seam_band` (28 pt) |
| Each edge of a dock column | A new column on that side | `column_edge_band` (28 pt) |
| The docked area's left or right edge without a dock, and an empty dock | A new dock | `new_dock_band` (28 pt) |
| The layout's top and bottom edges (and left and right with `side_edge_zones`) | A new frame across the whole window side, a quarter of the window | `edge_band` (28 pt, 8 pt with `side_edge_zones`; at most a quarter of the layout) |
| The edge area's sides in `drop_edges` (top and bottom; all four with `side_edge_zones`) | A new frame along that whole side, a quarter of the edge area | `edge_band` (28 pt, 8 pt with `side_edge_zones`; at most a quarter of the edge area) |
Where a band overlaps a group's outer third, both mean the same side. Without
`side_edge_zones`, a band reaches only 4 pt past its line into a tab strip that takes tabs;
the rest of the strip joins. The outer
edge band is offered only when every frame keeps its minimum.
- The edge area is the root, or the part between the docks. Without `side_edge_zones` it also
narrows to the canvas column: where neither side of a side-by-side split is a dock and only
one side holds a protected panel, that side. Side panels in split frames then get no band
across the whole window, and the drop wraps only the canvas column. A drop on the band
anchors on the canvas, so `Placement::Edge` narrows the same way when applied.
- `DockStyle::drop_edges` (a `Sides`) lists the edge area's sides that take a drop, and the
sides of a group that takes no tabs (a canvas) that take a split; there, the whole body goes
to the nearest listed side, and with none listed the canvas takes nothing. Groups that take
tabs keep all four sides. The default is top and bottom. An app whose palettes dock only
through the dock bands sets `Sides::NONE`; one whose palettes sit left and right of the
canvas sets `Sides::LEFT_RIGHT`. `side_edge_zones` uses all four and ignores it.
- With `side_edge_zones` (the split-tree arrangement), a frame's zones are a picture frame
instead: trapezoids along the four edges of the body below the tab strip, all four the same
depth, `docking::SIDE_EDGE_ZONE_DEPTH` (40 pt), or a quarter of the body's smaller side when