mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-15 06:52:23 +00:00
docking: narrow the outer edge band to the canvas column and add DockStyle::drop_edges
This commit is contained in:
1 parent
02d7d92a3e
commit
889f0367f2
9 files changed
+290
-54
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)?));
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in new issue
Block a user