mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-01 08:16:43 +00:00
Merge #187: narrow-group splits
This commit is contained in:
commit
42c87e7e60
4 files changed
+103
-19
No files matched your search
@@ -8,6 +8,7 @@ use crate::docking::{
|
||||
Action, Drag, DropProposal, DropRequest, DropTarget, GroupDrag, GroupGesture, Layout, Node, Placement, Target, Zone, cancel_drag,
|
||||
dock_model::Place, resolve_drop, surface::TAB_PADDING,
|
||||
};
|
||||
use crate::layout::SplitAxis;
|
||||
|
||||
/// Ctrl (Windows, Linux) or Cmd (macOS) held: the drop floats and no target is offered.
|
||||
fn float_only(ctx: &Context) -> bool {
|
||||
@@ -183,31 +184,41 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
}
|
||||
|
||||
/// Whether `action`, a drop that splits a group or the outer edge, leaves room: every group
|
||||
/// it makes smaller stays [`DROP_MARGIN`] above its minimum, so each divider can still
|
||||
/// travel both ways. An action the model rejects is left to the model's check.
|
||||
/// it makes smaller keeps its minimum, and each divider can still travel both ways. A group
|
||||
/// within [`DROP_MARGIN`] of its minimum along an axis passes when the dividers along its
|
||||
/// sides can travel, the groups beyond it giving way as they do for a dragged divider, so a
|
||||
/// narrow group still splits when its neighbours have room for both halves. An action the
|
||||
/// model rejects is left to the model's check.
|
||||
pub(super) fn leaves_room(&self, action: Action<P>) -> bool {
|
||||
let mut candidate = self.layout.clone();
|
||||
if candidate.apply_with_permissions(action, |p| self.permissions(p)).is_err() {
|
||||
return true;
|
||||
}
|
||||
let sizer = self.sizer();
|
||||
let drawn = |layout: &Layout<P>| {
|
||||
let drawn = |root: &Node<P>, area: Rect| {
|
||||
let mut out: Vec<Drawn<'_, P>> = Vec::new();
|
||||
if let Some(root) = &layout.root {
|
||||
sizer.groups(root, self.workspace, &mut out);
|
||||
}
|
||||
for window in layout.floating.iter().filter(|w| !w.minimized) {
|
||||
sizer.groups(&window.root, self.window_body(window), &mut out);
|
||||
}
|
||||
out.into_iter().filter_map(|d| Some((lead(d.node)?.clone(), d.rect.size(), d.limits.min))).collect::<Vec<_>>()
|
||||
sizer.groups(root, area, &mut out);
|
||||
out.into_iter().filter_map(|d| Some((lead(d.node)?.clone(), d.rect, d.limits.min))).collect::<Vec<_>>()
|
||||
};
|
||||
let before: HashMap<P, Vec2> = drawn(self.layout).into_iter().map(|(panel, size, _)| (panel, size)).collect();
|
||||
drawn(&candidate).into_iter().all(|(panel, size, min)| {
|
||||
let room = size - min;
|
||||
(room.x >= DROP_MARGIN && room.y >= DROP_MARGIN) || before.get(&panel).is_some_and(|old| size.x >= old.x - 0.5 && size.y >= old.y - 0.5)
|
||||
let before: HashMap<P, Vec2> =
|
||||
self.trees(self.layout).into_iter().flat_map(|(root, area)| drawn(root, area)).map(|(panel, rect, _)| (panel, rect.size())).collect();
|
||||
self.trees(&candidate).into_iter().all(|(root, area)| {
|
||||
drawn(root, area).into_iter().all(|(panel, rect, min)| {
|
||||
let (size, room) = (rect.size(), rect.size() - min);
|
||||
let fits = |room: f32, axis| room >= DROP_MARGIN || sizer.frees((root, area), rect, axis);
|
||||
before.get(&panel).is_some_and(|old| size.x >= old.x - 0.5 && size.y >= old.y - 0.5)
|
||||
|| (room.min_elem() > -0.01 && fits(room.x, SplitAxis::Horizontal) && fits(room.y, SplitAxis::Vertical))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// The trees of `layout` as drawn: the docked root in the workspace and each open floating
|
||||
/// window's tree in its body.
|
||||
fn trees<'l>(&self, layout: &'l Layout<P>) -> Vec<(&'l Node<P>, Rect)> {
|
||||
let windows = layout.floating.iter().filter(|w| !w.minimized).map(|w| (&w.root, self.window_body(w)));
|
||||
layout.root.iter().map(|root| (root, self.workspace)).chain(windows).collect()
|
||||
}
|
||||
|
||||
/// Release of a group drag: join or split at the target, or float at the ghost with the
|
||||
/// group's size. Over its own docked position nothing changes; a floating group released
|
||||
/// over itself moves there.
|
||||
|
||||
@@ -18,8 +18,8 @@ use crate::layout::{SplitAxis, SplitLayout, SplitSize, split_rect};
|
||||
/// A split's two children with the rects they are drawn in.
|
||||
type Children<'n, P> = [(&'n Node<P>, Rect); 2];
|
||||
|
||||
/// Room a group keeps above its minimum after a drop, so each of its dividers can still
|
||||
/// travel both ways.
|
||||
/// How far each divider beside a group that a drop makes smaller can still travel both ways:
|
||||
/// the group keeps this much above its minimum, or the groups beyond it give the room.
|
||||
pub(in crate::docking) const DROP_MARGIN: f32 = 1.0;
|
||||
|
||||
pub(in crate::docking) struct Sizer<'a, P> {
|
||||
@@ -277,6 +277,34 @@ impl<P> Sizer<'_, P> {
|
||||
}
|
||||
(-first_in.min(second_out), first_out.min(second_in))
|
||||
}
|
||||
|
||||
/// Whether every divider of `root` across `axis` that runs along a side of the group drawn
|
||||
/// at `group` can travel [`DROP_MARGIN`] both ways, by [`Self::range`]: a group at its
|
||||
/// minimum holds no divider still while the groups beyond it can give way.
|
||||
pub(in crate::docking) fn frees(&self, root: (&Node<P>, Rect), group: Rect, axis: SplitAxis) -> bool {
|
||||
let edges = |rect: Rect| match axis {
|
||||
SplitAxis::Horizontal => (rect.left(), rect.right()),
|
||||
SplitAxis::Vertical => (rect.top(), rect.bottom()),
|
||||
};
|
||||
let (start, end) = edges(group);
|
||||
let beside = |child: Rect, edge: f32, own: f32| child.contains(group.center()) && (edge - own).abs() < 0.5;
|
||||
let mut nodes = vec![(root.0, root.1, Vec::new())];
|
||||
while let Some((node, rect, path)) = nodes.pop() {
|
||||
let (Node::Split { axis: split_axis, .. }, Some([a, b])) = (node, self.children(node, rect)) else { continue };
|
||||
if *split_axis == axis && (beside(a.1, edges(a.1).1, end) || beside(b.1, edges(b.1).0, start)) {
|
||||
let (lo, hi) = self.range(root, &path);
|
||||
if -lo < DROP_MARGIN || hi < DROP_MARGIN {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (second, (child, rect)) in [(false, a), (true, b)] {
|
||||
let mut path = path.clone();
|
||||
path.push(second);
|
||||
nodes.push((child, rect, path));
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// The size of a split at `path` whose first child is `first` of `available` points.
|
||||
|
||||
@@ -193,8 +193,10 @@ fn groups_have_one_small_minimum_and_no_maximum() {
|
||||
drag_hit(&mut d, divider(&[true, true]), vec2(-2000.0, 0.0));
|
||||
assert_eq!(group(&d, TOOLS).width(), 40.0, "and shrinks to the minimum, no further");
|
||||
|
||||
// A group too narrow for two halves takes a drop on its edge as a tab.
|
||||
// At its minimum, with Project left of it at its own, it has no room for two halves and
|
||||
// takes a drop on its edge as a tab.
|
||||
let tools = group(&d, TOOLS);
|
||||
assert_eq!(group(&d, PROJECT[0]).width(), 40.0);
|
||||
let out = d.drag(tab(&d, INFO), pos2(tools.right() - 4.0, tools.center().y + 30.0));
|
||||
assert!(
|
||||
matches!(&out.actions[..], [Action::Move { panel: INFO, anchor: TOOLS, placement: Placement::Split(Zone::Center) | Placement::Tab { .. } }]),
|
||||
@@ -801,3 +803,45 @@ fn zones_a_group_does_not_take_are_not_drawn() {
|
||||
d.hold(tab(&d, TIMELINE), timeline.center() + vec2(0.0, 13.0));
|
||||
assert!(zone_outlines(&d).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_narrow_group_splits_on_every_side_and_its_neighbours_give_the_room() {
|
||||
// Tools at 80 pt: two halves need 84, so a side split takes the rest from Timeline.
|
||||
let narrow = || {
|
||||
let mut d = film();
|
||||
let width = group(&d, TOOLS).width();
|
||||
drag_hit(&mut d, divider(&[true, true]), vec2(80.0 - width, 0.0));
|
||||
d
|
||||
};
|
||||
let d = narrow();
|
||||
let (tools, timeline) = (group(&d, TOOLS), group(&d, TIMELINE));
|
||||
assert_eq!(tools.width().round(), 80.0);
|
||||
let body_top = tools.top() + 26.0;
|
||||
for (zone, at) in [
|
||||
(Zone::Left, pos2(tools.left() + 5.0, tools.center().y)),
|
||||
(Zone::Right, pos2(tools.right() - 5.0, tools.center().y)),
|
||||
(Zone::Top, pos2(tools.center().x, body_top + 5.0)),
|
||||
(Zone::Bottom, pos2(tools.center().x, tools.bottom() - 15.0)),
|
||||
] {
|
||||
let mut d = narrow();
|
||||
d.hold(tab(&d, INFO), at);
|
||||
assert_eq!(zone_outlines(&d).len(), 6, "{zone:?}: four sides, the center and the strip");
|
||||
assert!(d.filled_polygon(&zone_shape(tools, 26.0, 40.0, zone).unwrap()), "{zone:?} is filled");
|
||||
let out = d.release(at);
|
||||
assert_eq!(out.actions, vec![Action::Move { panel: INFO, anchor: TOOLS, placement: Placement::Split(zone) }]);
|
||||
d.settle();
|
||||
let (info, split) = (group(&d, INFO), group(&d, TOOLS));
|
||||
let beside = match zone {
|
||||
Zone::Left => info.right() < split.left(),
|
||||
Zone::Right => info.left() > split.right(),
|
||||
Zone::Top => info.bottom() < split.top(),
|
||||
_ => info.top() > split.bottom(),
|
||||
};
|
||||
assert!(beside, "{zone:?}: Info {info:?}, Tools {split:?}");
|
||||
if matches!(zone, Zone::Left | Zone::Right) {
|
||||
assert_eq!((info.width(), split.width()), (40.0, 40.0), "{zone:?}");
|
||||
assert!((group(&d, TIMELINE).width() - (timeline.width() - 4.0)).abs() < 1.0, "{zone:?}: Timeline gives the room");
|
||||
}
|
||||
assert_sizes(&d, &format!("{zone:?}"));
|
||||
}
|
||||
}
|
||||
+3
-2
@@ -218,8 +218,9 @@ A flyout fits its content the same way, without the title bar.
|
||||
- Over a frame, the tab strip and the middle join its tabs, and the nearest side splits it. The
|
||||
side zones are trapezoids along the four edges of the body below the tab strip, like a picture
|
||||
frame, all four the same depth: `DockStyle::zone_depth` (40 pt), or a quarter of the body's
|
||||
smaller side when that is less, so every frame keeps a center. A split that would leave either half without
|
||||
`DockStyle::min_pane` joins the frame's tabs instead. The gap between two frames is no target.
|
||||
smaller side when that is less, so every frame keeps a center. A frame too narrow or short for two
|
||||
halves of `DockStyle::min_pane` still splits when the frames beyond it can give the room, as they
|
||||
do for a dragged divider; when they cannot, the drop joins the frame's tabs instead. The gap between two frames is no target.
|
||||
- The outer edge band (`DockStyle::edge_band`) takes a drop as a new frame across the whole
|
||||
window side: top and bottom, and left and right with `side_edge_zones`. It takes a quarter of
|
||||
the window and is offered only when every frame keeps its minimum. Past the band, a frame's own
|
||||
|
||||
Reference in new issue
Block a user