mirror of
https://github.com/storytold/craft-libs.git
synced 2026-10-31 07:39:23 +00:00
docking: drop targets follow the pointer over the whole group (#310)
* docking: drop targets follow the pointer over the whole group Outside side_edge_zones, a group body's outer third on each side docks beside it and the middle joins as a tab; at a corner the relatively nearer side wins, and a group that takes no tabs maps its whole body to its edges. Dock-column groups get the same zones: top and bottom stack, left and right make a column. Border bands (seam, column edge, new dock, root edge) are 28 pt and reach only 4 pt into a tab strip that takes tabs. Previews are a drop_line_width line along the edge a drop docks to, or the whole group's outline with a light fill for a tab join, for tab and group drags alike. side_edge_zones keeps its 40 pt picture-frame zones, overlay and previews (SIDE_EDGE_ZONE_DEPTH). DockStyle: side_zone_fraction replaces zone_depth; new column_edge_band and drop_line_width; edge_band, seam_band and new_dock_band default to 28. * docking: document the drop zones, bands and previews * docking: dock groups and the outer edge band keep the split-tree sizes with side edge zones * docking: drop zones for hosts that resolve drops, lines kept in view, stripless dock groups resolve_drop_by and DockStyle::zone_rule give hosts the dock area's zones and previews. A stack line on an edge scrolled out of a dock column shows at the view's edge, and a dock group without a tab strip takes its zones from the top of its body. A style needs a drop line wider than zero and a side zone fraction of at most one half.
This commit is contained in:
1 parent
865c9af7fd
commit
8a503cfe79
18 files changed
+783
-124
No files matched your search
@@ -254,7 +254,9 @@ impl<'a> FloatingFrame<'a> {
|
||||
}
|
||||
|
||||
/// A drop preview: a 2 pt accent outline inside `rect`, filled with 15% accent when `filled`.
|
||||
/// Docking and the pane host draw every destination and drag ghost with it.
|
||||
/// Docking draws its drag ghosts with it, and with
|
||||
/// [`crate::docking::DockStyle::side_edge_zones`] the region a drop takes; the pane host draws
|
||||
/// every destination with it.
|
||||
pub fn drop_preview(painter: &egui::Painter, rect: Rect, accent: Color32, filled: bool) {
|
||||
let fill = if filled { accent.gamma_multiply(0.15) } else { Color32::TRANSPARENT };
|
||||
painter.rect(rect, 0.0, fill, Stroke::new(2.0, accent), StrokeKind::Inside);
|
||||
|
||||
@@ -259,6 +259,108 @@ fn group_dropped_on_a_top_or_bottom_band_stacks_above_or_below() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_dock_groups_top_and_bottom_thirds_stack_its_sides_make_columns_and_its_middle_joins() {
|
||||
// Group 3's body is 0..240 across and 333..600 down.
|
||||
for (at, expected, line) in [
|
||||
(pos2(120.0, 360.0), vec![vec![vec![2], vec![5], vec![3]]], Some(hline(307.0, 0.0..=240.0))),
|
||||
(pos2(120.0, 580.0), vec![vec![vec![2], vec![3], vec![5]]], Some(hline(598.0, 0.0..=240.0))),
|
||||
(pos2(30.0, 466.0), vec![vec![vec![5]], vec![vec![2], vec![3]]], Some(vline(0.0, 0.0..=600.0))),
|
||||
(pos2(200.0, 466.0), vec![vec![vec![2], vec![3]], vec![vec![5]]], Some(vline(240.0, 0.0..=600.0))),
|
||||
(pos2(120.0, 466.0), vec![vec![vec![2], vec![3, 5]]], None),
|
||||
] {
|
||||
let layout = with_floating(docked(Side::Start, &[&[&[2], &[3]]]), &[(5, [400.0, 300.0, 150.0, 150.0])]);
|
||||
let mut d = Driver::new(layout);
|
||||
d.hold(bar(&d, 5), at);
|
||||
if let Some(line) = line {
|
||||
assert!(d.painted(line), "{at:?}: the line of the edge it docks to");
|
||||
}
|
||||
d.step(vec![button(at, false)]);
|
||||
assert_eq!(columns(&d.layout, Side::Start), expected, "{at:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_side_edge_zones_a_dock_groups_side_zones_are_40_pt_deep() {
|
||||
// Group 3's body is 0..240 across and 333..600 down. 60 pt in from its left edge and 57
|
||||
// below its top are in its outer thirds but past 40 pt: the middle, which joins it.
|
||||
for (at, expected) in [
|
||||
(pos2(30.0, 466.0), vec![vec![vec![5]], vec![vec![2], vec![3]]]),
|
||||
(pos2(120.0, 360.0), vec![vec![vec![2], vec![5], vec![3]]]),
|
||||
(pos2(60.0, 466.0), vec![vec![vec![2], vec![3, 5]]]),
|
||||
(pos2(120.0, 390.0), vec![vec![vec![2], vec![3, 5]]]),
|
||||
] {
|
||||
let layout = with_floating(docked(Side::Start, &[&[&[2], &[3]]]), &[(5, [400.0, 300.0, 150.0, 150.0])]);
|
||||
let mut d = testing::DockDriver::new(layout, TestContent::default())
|
||||
.with_area(Rect::from_min_size(Pos2::ZERO, vec2(600.0, 600.0)))
|
||||
.with_style(|s| s.side_edge_zones = true);
|
||||
let tab = d.hit(&Hit::Tab(5)).unwrap_or_else(|| panic!("tab 5")).center();
|
||||
d.drag(tab, at);
|
||||
assert_eq!(columns(&d.layout, Side::Start), expected, "{at:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_group_edge_scrolled_out_of_view_previews_its_line_at_the_views_edge() {
|
||||
// Groups 306 pt high from y = 14: group 3 spans 320..626 and its body's bottom third starts
|
||||
// at 533, above the window's bottom.
|
||||
let layout = with_floating(docked(Side::Start, &[&[&[2], &[3], &[4]]]), &[(5, [400.0, 300.0, 150.0, 150.0])]);
|
||||
let mut d = Driver::with(layout, TestContent { min_height: 280.0 }, false);
|
||||
let at = pos2(120.0, 580.0);
|
||||
d.hold(bar(&d, 5), at);
|
||||
assert!(d.painted(hline(598.0, 0.0..=240.0)), "the line at the view's bottom");
|
||||
d.step(vec![button(at, false)]);
|
||||
assert_eq!(columns(&d.layout, Side::Start), vec![vec![vec![2], vec![3], vec![5], vec![4]]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_stripless_dock_groups_top_third_stacks_above_it() {
|
||||
// Canvas 9 in the middle; protected panel 0 alone in the lower group draws no tab strip, so
|
||||
// its body is 307..600.
|
||||
let mut layout = with_floating(docked(Side::Start, &[&[&[2], &[0]]]), &[(5, [400.0, 300.0, 150.0, 150.0])]);
|
||||
if let Some(Node::Split { second, .. }) = &mut layout.root {
|
||||
**second = Node::Tabs { panels: vec![9], active: 0 };
|
||||
}
|
||||
let mut d = Driver::new(layout);
|
||||
d.drag(bar(&d, 5), pos2(120.0, 330.0));
|
||||
assert_eq!(columns(&d.layout, Side::Start), vec![vec![vec![2], vec![5], vec![0]]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dock_bands_are_28_pt_wide() {
|
||||
let window = || with_floating(docked(Side::Start, &[&[&[2], &[3]]]), &[(5, [400.0, 300.0, 150.0, 150.0])]);
|
||||
let target = |layout: Layout<u32>, at: Pos2| {
|
||||
let mut d = Driver::new(layout);
|
||||
let out = d.drag(bar(&d, 5), at);
|
||||
moved(&out.actions).map(|(_, to)| to.clone())
|
||||
};
|
||||
let column_after = Target::At { anchor: 2, placement: Placement::Column { after: true } };
|
||||
// 14 pt either side of the dock's inner edge at 240; past it, the protected canvas.
|
||||
assert_eq!(target(window(), pos2(253.0, 300.0)), Some(column_after.clone()));
|
||||
assert_ne!(target(window(), pos2(255.0, 300.0)), Some(column_after));
|
||||
// 28 pt along the window edge without a dock.
|
||||
assert_eq!(target(window(), pos2(573.0, 300.0)), Some(Target::NewDock { side: Side::End }));
|
||||
assert_ne!(target(window(), pos2(571.0, 300.0)), Some(Target::NewDock { side: Side::End }));
|
||||
// 28 pt along the bottom of the part between the docks, which splits at the root.
|
||||
assert!(matches!(target(window(), pos2(420.0, 573.0)), Some(Target::At { placement: Placement::Edge(Zone::Bottom), .. })));
|
||||
assert!(!matches!(target(window(), pos2(420.0, 571.0)), Some(Target::At { placement: Placement::Edge(_), .. })));
|
||||
// 14 pt either side of a seam. In a 400 pt column, 16 pt in from the window edge and 13
|
||||
// above the seam at 307 is relatively nearer group 2's left than its bottom, but the seam
|
||||
// takes it; 15 above, the left third makes a column.
|
||||
let wide = || {
|
||||
let mut layout = with_floating(docked(Side::Start, &[&[&[2], &[3]]]), &[(5, [450.0, 300.0, 100.0, 150.0])]);
|
||||
if let Some(Node::Split { size, first, .. }) = &mut layout.root {
|
||||
*size = SplitSize::FixedFirst(400.0);
|
||||
if let Node::Dock { columns, .. } = &mut **first {
|
||||
columns.iter_mut().for_each(|c| c.width = 400.0);
|
||||
}
|
||||
}
|
||||
layout
|
||||
};
|
||||
assert_eq!(target(wide(), pos2(16.0, 294.0)), Some(Target::At { anchor: 2, placement: Placement::Group { below: true, height: None } }));
|
||||
assert_eq!(target(wide(), pos2(16.0, 292.0)), Some(Target::At { anchor: 2, placement: Placement::Column { after: false } }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn group_dropped_on_a_column_edge_makes_a_new_column() {
|
||||
// The dock's inner edge: a column after the existing one, previewed full height.
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
//! Drop targets of the default profile over frames: a group body's thirds, the tab outline,
|
||||
//! the fallback where a side has no room, and the root edge band.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::testing::DockDriver;
|
||||
use super::*;
|
||||
use crate::layout::{SplitAxis, SplitSize};
|
||||
use egui::{Color32, Pos2, Rect, pos2, vec2};
|
||||
|
||||
/// Panel bodies by their rects; `rules` are the permissions of panels that differ from the
|
||||
/// default, `min_width` every panel's least width.
|
||||
#[derive(Default)]
|
||||
struct Bodies {
|
||||
bodies: HashMap<u32, Rect>,
|
||||
rules: HashMap<u32, Permissions>,
|
||||
min_width: f32,
|
||||
}
|
||||
|
||||
impl DockContent<u32> for Bodies {
|
||||
fn body(&mut self, ui: &mut egui::Ui, panel: &u32) {
|
||||
self.bodies.insert(*panel, ui.max_rect());
|
||||
}
|
||||
fn permissions(&self, panel: &u32) -> Permissions {
|
||||
self.rules.get(panel).copied().unwrap_or_default()
|
||||
}
|
||||
fn limits(&self, _: &u32) -> PanelLimits {
|
||||
PanelLimits { min: vec2(self.min_width, 0.0), ..PanelLimits::default() }
|
||||
}
|
||||
}
|
||||
|
||||
type Driver = DockDriver<u32, Bodies>;
|
||||
|
||||
/// Frames `[1]` and `[2, 3]` side by side, each half of the area.
|
||||
fn frames() -> Layout<u32> {
|
||||
Layout {
|
||||
root: Some(Node::Split {
|
||||
axis: SplitAxis::Horizontal,
|
||||
size: SplitSize::Ratio(0.5),
|
||||
first: Box::new(Node::Tabs { panels: vec![1], active: 0 }),
|
||||
second: Box::new(Node::Tabs { panels: vec![2, 3], active: 1 }),
|
||||
}),
|
||||
floating: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn driver(rules: &[(u32, Permissions)]) -> Driver {
|
||||
DockDriver::new(frames(), Bodies { rules: rules.iter().copied().collect(), ..Bodies::default() })
|
||||
}
|
||||
|
||||
/// The body of `panel` and its group's rect, the 26 pt strip included.
|
||||
fn group(d: &Driver, panel: u32) -> (Rect, Rect) {
|
||||
let body = d.content.bodies.get(&panel).copied().unwrap_or_else(|| panic!("panel {panel} not drawn"));
|
||||
(body, Rect::from_min_max(pos2(body.left(), body.top() - 26.0), body.max))
|
||||
}
|
||||
|
||||
fn tab(d: &Driver, panel: u32) -> Pos2 {
|
||||
d.hit(&Hit::Tab(panel)).unwrap_or_else(|| panic!("tab {panel}")).center()
|
||||
}
|
||||
|
||||
/// The fill of the last frame's rect shape at `rect` that has no stroke.
|
||||
fn fill_at(d: &Driver, rect: Rect) -> Option<Color32> {
|
||||
d.shapes().iter().find_map(|clipped| match &clipped.shape {
|
||||
egui::Shape::Rect(shape) if shape.rect == rect && shape.stroke.width == 0.0 && shape.fill != Color32::TRANSPARENT => Some(shape.fill),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// The 4 pt line along `rect`'s edge on `zone`'s side, inside it.
|
||||
fn line(rect: Rect, zone: Zone) -> Rect {
|
||||
match zone {
|
||||
Zone::Left => Rect::from_x_y_ranges(rect.left()..=(rect.left() + 4.0), rect.y_range()),
|
||||
Zone::Right => Rect::from_x_y_ranges((rect.right() - 4.0)..=rect.right(), rect.y_range()),
|
||||
Zone::Top => Rect::from_x_y_ranges(rect.x_range(), rect.top()..=(rect.top() + 4.0)),
|
||||
Zone::Bottom | Zone::Center => Rect::from_x_y_ranges(rect.x_range(), (rect.bottom() - 4.0)..=rect.bottom()),
|
||||
}
|
||||
}
|
||||
|
||||
fn accent() -> Color32 {
|
||||
DockStyle::default().accent
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_drop_on_a_groups_outer_third_docks_beside_it_with_a_line_and_its_middle_joins_with_an_outline() {
|
||||
let d = driver(&[]);
|
||||
let (body, rect) = group(&d, 1);
|
||||
let (w, h) = (body.width(), body.height());
|
||||
let line = |zone| line(rect, zone);
|
||||
for (zone, at) in [
|
||||
(Zone::Left, pos2(body.left() + w * 0.3, body.center().y)),
|
||||
(Zone::Right, pos2(body.right() - w * 0.3, body.center().y)),
|
||||
(Zone::Top, pos2(body.center().x, body.top() + h * 0.3)),
|
||||
(Zone::Bottom, pos2(body.center().x, body.bottom() - h * 0.3)),
|
||||
// In the top left corner, the top is relatively nearer.
|
||||
(Zone::Top, pos2(body.left() + w * 0.25, body.top() + h * 0.2)),
|
||||
] {
|
||||
let mut d = driver(&[]);
|
||||
d.hold(tab(&d, 3), at);
|
||||
assert!(d.filled(line(zone)), "{zone:?}: a line along the group's whole edge");
|
||||
assert!(!d.filled(zone_rect(rect, zone).unwrap().shrink(1.0)), "{zone:?}: no filled half");
|
||||
assert!(!d.outlined(rect.shrink(1.0)), "{zone:?}");
|
||||
let out = d.release(at);
|
||||
assert_eq!(out.actions, vec![Action::Move { panel: 3, anchor: 1, placement: Placement::Split(zone) }], "{zone:?}");
|
||||
}
|
||||
// The middle and the tab strip join: the group's outline with a light fill, no line.
|
||||
for at in [body.center(), pos2(rect.center().x, rect.top() + 13.0)] {
|
||||
let mut d = driver(&[]);
|
||||
d.hold(tab(&d, 3), at);
|
||||
assert!(d.outlined(rect.shrink(1.0)), "{at:?}");
|
||||
assert_eq!(fill_at(&d, rect.shrink(1.0)), Some(accent().gamma_multiply(0.08)), "{at:?}: a light fill");
|
||||
assert!([Zone::Left, Zone::Right, Zone::Top, Zone::Bottom].into_iter().all(|z| !d.filled(line(z))), "{at:?}");
|
||||
let out = d.release(at);
|
||||
assert!(
|
||||
matches!(&out.actions[..], [Action::Move { panel: 3, anchor: 1, placement: Placement::Tab { .. } | Placement::Split(Zone::Center) }]),
|
||||
"{:?}",
|
||||
out.actions
|
||||
);
|
||||
}
|
||||
// A group's drag bar previews with the same painter.
|
||||
let mut d = driver(&[]);
|
||||
let bar = d.hit(&Hit::DragBar(2)).unwrap().center();
|
||||
let at = pos2(body.left() + w * 0.3, body.center().y);
|
||||
d.hold(bar, at);
|
||||
assert!(d.filled(line(Zone::Left)));
|
||||
assert!(!d.filled(zone_rect(rect, Zone::Left).unwrap().shrink(1.0)));
|
||||
let mut d = driver(&[]);
|
||||
d.hold(bar, body.center());
|
||||
assert!(d.outlined(rect.shrink(1.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
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)]);
|
||||
d.hold(tab(&d, 3), at);
|
||||
assert!(d.filled(line(rect, zone)), "{zone:?}");
|
||||
let out = d.release(at);
|
||||
assert_eq!(out.actions, vec![Action::Move { panel: 3, anchor: 1, placement: Placement::Split(zone) }], "{zone:?}");
|
||||
}
|
||||
// Its strip joins nothing.
|
||||
let mut d = driver(&[(1, canvas)]);
|
||||
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);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_side_without_room_joins_as_a_tab_or_offers_nothing() {
|
||||
// Two 400 pt frames of panels at least 300 wide: neither has room for two halves side by
|
||||
// side, but both have room for two halves stacked.
|
||||
let narrow = |rules: &[(u32, Permissions)]| {
|
||||
DockDriver::new(frames(), Bodies { rules: rules.iter().copied().collect(), min_width: 300.0, ..Bodies::default() })
|
||||
};
|
||||
let d = narrow(&[]);
|
||||
let (body, rect) = group(&d, 1);
|
||||
let left = pos2(body.left() + body.width() * 0.25, body.center().y);
|
||||
let mut d = narrow(&[]);
|
||||
d.hold(tab(&d, 3), left);
|
||||
assert!(d.outlined(rect.shrink(1.0)), "the tab outline");
|
||||
let out = d.release(left);
|
||||
assert_eq!(out.actions, vec![Action::Move { panel: 3, anchor: 1, placement: Placement::Split(Zone::Center) }]);
|
||||
let top = pos2(body.center().x, body.top() + body.height() * 0.2);
|
||||
let mut d = narrow(&[]);
|
||||
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() })]);
|
||||
d.hold(tab(&d, 3), left);
|
||||
assert!(!d.outlined(rect.shrink(1.0)));
|
||||
let out = d.release(left);
|
||||
assert!(!out.actions.iter().any(|a| matches!(a, Action::Move { anchor: 1, .. })), "{:?}", out.actions);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_root_edge_band_is_28_pt_and_previews_a_line_along_the_layouts_edge() {
|
||||
let d = driver(&[]);
|
||||
let (body, _) = group(&d, 1);
|
||||
let x = body.center().x;
|
||||
let mut d = driver(&[]);
|
||||
d.hold(tab(&d, 3), pos2(x, 573.0));
|
||||
assert!(d.filled(Rect::from_x_y_ranges(0.0..=800.0, 596.0..=600.0)), "a line along the layout's whole bottom edge");
|
||||
let out = d.release(pos2(x, 573.0));
|
||||
assert!(matches!(&out.actions[..], [Action::Move { panel: 3, placement: Placement::Edge(Zone::Bottom), .. }]), "{:?}", out.actions);
|
||||
// Past the band, the group's own bottom third.
|
||||
let mut d = driver(&[]);
|
||||
let out = d.drag(tab(&d, 3), pos2(x, 571.0));
|
||||
assert_eq!(out.actions, vec![Action::Move { panel: 3, anchor: 1, placement: Placement::Split(Zone::Bottom) }]);
|
||||
// The band reaches only 4 pt past its line into a tab strip, which joins the group.
|
||||
let mut d = driver(&[]);
|
||||
let out = d.drag(tab(&d, 3), pos2(x, 12.0));
|
||||
assert!(matches!(&out.actions[..], [Action::Move { panel: 3, anchor: 1, placement: Placement::Tab { .. } }]), "{:?}", out.actions);
|
||||
let mut d = driver(&[]);
|
||||
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);
|
||||
}
|
||||
@@ -391,6 +391,18 @@ fn outer_edge_band_splits_at_the_root() {
|
||||
assert!(!out.actions.iter().any(|a| matches!(a, Action::Move { placement: Placement::Edge(_), .. })), "{:?}", out.actions);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_split_tree_outer_edge_band_is_8_pt() {
|
||||
let mut d = Driver::new(frames(), true);
|
||||
let tab = d.tab(3).unwrap().center();
|
||||
let out = d.drag(tab, pos2(7.0, 300.0));
|
||||
assert!(out.actions.contains(&Action::Move { panel: 3, anchor: 1, placement: Placement::Edge(Zone::Left) }), "{:?}", out.actions);
|
||||
// Past it, frame [1]'s left side zone.
|
||||
let mut d = Driver::new(frames(), true);
|
||||
let out = d.drag(tab, pos2(10.0, 300.0));
|
||||
assert!(out.actions.contains(&Action::Move { panel: 3, anchor: 1, placement: Placement::Split(Zone::Left) }), "{:?}", out.actions);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maximized_frame_fills_the_area_and_other_frames_do_not_render_or_take_input() {
|
||||
let mut layout = frames();
|
||||
|
||||
@@ -354,7 +354,7 @@ fn group_drop_on_a_frame_edge_splits_and_on_a_protected_canvas_floats() {
|
||||
let canvas = || TestContent { protected: vec![1], ..Default::default() };
|
||||
let area = Rect::from_min_size(pos2(0.0, 0.0), vec2(600.0, 600.0));
|
||||
let mut d = Driver::with(frames(), TestContent::default(), area);
|
||||
let out = d.drag(d.rect("drag-bar", 2).center(), pos2(20.0, 300.0));
|
||||
let out = d.drag(d.rect("drag-bar", 2).center(), pos2(40.0, 300.0));
|
||||
assert!(matches!(moved_group(&out.actions), Some(Target::At { anchor: 1, placement: Placement::Split(Zone::Left) })), "{:?}", out.actions);
|
||||
let Some(Node::Split { first, second, .. }) = &d.layout.root else { panic!() };
|
||||
assert_eq!(**first, tabs(&[2, 3], 1));
|
||||
@@ -362,7 +362,7 @@ fn group_drop_on_a_frame_edge_splits_and_on_a_protected_canvas_floats() {
|
||||
|
||||
// A protected canvas takes nothing, at its center or its edge: no join, no split, no
|
||||
// preview; the group floats there.
|
||||
for at in [pos2(150.0, 300.0), pos2(20.0, 300.0)] {
|
||||
for at in [pos2(150.0, 300.0), pos2(40.0, 300.0)] {
|
||||
let mut d = Driver::with(frames(), canvas(), area);
|
||||
let out = d.drag(d.rect("drag-bar", 2).center(), at);
|
||||
assert!(matches!(moved_group(&out.actions), Some(Target::Float { .. })), "{:?}", out.actions);
|
||||
|
||||
@@ -34,7 +34,10 @@ pub use surface::{
|
||||
DockArea, DockContent, DockEvent, DockHeights, DockOutput, DockStyle, DropFeedback, DropRequest, FloatSpace, HidePanels, PanelLimits,
|
||||
WindowFiles, WindowKind, begin_drag, cancel_drag, float_space, is_dragging, route_app_drag,
|
||||
};
|
||||
pub use targets::{DropProposal, DropTarget, drop_zone, resolve_drop, tab_insertion_marker, zone_rect, zone_shape};
|
||||
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,
|
||||
};
|
||||
pub use workspace::Workspace;
|
||||
|
||||
use surface::{Drag, GroupDrag, GroupGesture};
|
||||
@@ -44,6 +47,8 @@ mod command_tests;
|
||||
#[cfg(test)]
|
||||
mod dock_tests;
|
||||
#[cfg(test)]
|
||||
mod drop_tests;
|
||||
#[cfg(test)]
|
||||
mod floating_tests;
|
||||
#[cfg(test)]
|
||||
mod group_tests;
|
||||
|
||||
@@ -895,9 +895,9 @@ fn window_move(out: &super::DockOutput<u32>) -> Option<&Target<u32>> {
|
||||
|
||||
#[test]
|
||||
fn a_native_windows_bar_docks_it_at_frame_edges_and_dock_lines_but_not_a_body_center() {
|
||||
// The right edge zone of frame [3, 4], 25 pt in, clear of the new-dock band along the window's edge.
|
||||
// The right edge zone of frame [3, 4], 40 pt in, clear of the new-dock band along the window's edge.
|
||||
let mut d = driver(layout(true));
|
||||
let out = drag_bar_to(&mut d, 5, pos2(775.0, 300.0));
|
||||
let out = drag_bar_to(&mut d, 5, pos2(760.0, 300.0));
|
||||
assert!(matches!(window_move(&out), Some(Target::At { anchor: 3 | 4, placement: Placement::Split(super::Zone::Right) })), "{:?}", out.actions);
|
||||
assert!(d.layout.floating.is_empty());
|
||||
assert!(in_root(&d, 5) && in_root(&d, 6));
|
||||
@@ -1459,10 +1459,10 @@ fn the_bars_close_button_wins_over_the_resize_bands() {
|
||||
assert_eq!(resize(pos2(237.0, 100.0)), Some(D::East), "the right edge");
|
||||
assert_eq!(resize(pos2(120.0, 1.0)), Some(D::North), "the top edge above the bar");
|
||||
assert_eq!(resize(pos2(2.0, 298.0)), Some(D::SouthWest), "the bottom-left corner");
|
||||
// Inside a corner's L, the bar moves the window.
|
||||
// Inside a corner's L, the bar moves the window, here to the middle of frame [1, 2].
|
||||
let rect = |x0: f32, y0: f32, x1: f32, y1: f32| Rect::from_min_max(pos2(x0, y0), pos2(x1, y1));
|
||||
let (moved, resize, _) = press_and_drag(pos2(10.0, 10.0), vec2(20.0, 20.0));
|
||||
assert_eq!((moved, resize), (rect(420.0, 150.0, 660.0, 450.0), None));
|
||||
let (moved, resize, _) = press_and_drag(pos2(10.0, 10.0), vec2(-110.0, 150.0));
|
||||
assert_eq!((moved, resize), (rect(290.0, 280.0, 530.0, 580.0), None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -184,7 +184,10 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
std::mem::swap(&mut self.frame.windows, &mut surface.windows);
|
||||
let bounds = std::mem::replace(&mut self.float_bounds, surface.bounds);
|
||||
let blockers = std::mem::take(&mut self.blockers);
|
||||
// A native window has no docks.
|
||||
let groups = std::mem::take(&mut self.dock_groups);
|
||||
let result = f(self);
|
||||
self.dock_groups = groups;
|
||||
std::mem::swap(&mut self.targets, &mut surface.targets);
|
||||
std::mem::swap(&mut self.dock_targets, &mut surface.dock_targets);
|
||||
std::mem::swap(&mut self.frame.windows, &mut surface.windows);
|
||||
|
||||
@@ -8,17 +8,44 @@ use std::{fmt::Debug, hash::Hash};
|
||||
|
||||
use egui::{Context, Key, Pos2, Rect, Sense, Stroke, StrokeKind, Ui, UiBuilder, pos2, vec2};
|
||||
|
||||
use super::{GroupKind, Renderer, RowItem, row_drag, row_range};
|
||||
use super::{GroupKind, Renderer, RowItem, row_drag, row_range, stripless};
|
||||
use crate::docking::{
|
||||
Action, Column, DockEvent, DockHeights, DockStyle, Group, GroupDrag, GroupGesture, Node, Placement, Side, Target,
|
||||
Action, Column, DockEvent, DockHeights, DockStyle, Group, GroupDrag, GroupGesture, Node, Placement, Side, Target, Zone,
|
||||
dock_model::Unit,
|
||||
targets::{DockDrop, DockTarget, column_edge, horizontal_line, resolve_dock, usable, vertical_line},
|
||||
targets::{
|
||||
DockDrop, DockTarget, SIDE_EDGE_ZONE_DEPTH, STRIP_REACH, body_zone, column_edge, drop_zone, horizontal_line, resolve_dock, usable,
|
||||
vertical_line,
|
||||
},
|
||||
};
|
||||
use crate::layout::{ResizeHandle, ResizeHandleStyle, SplitAxis};
|
||||
|
||||
/// Width of the »/« button at the outer end of a column bar.
|
||||
const COLLAPSE_WIDTH: f32 = 18.0;
|
||||
|
||||
/// [D] An open group of a dock column: its visible body, and the target and line of its left,
|
||||
/// right, top and bottom zones (see [`DockStyle::side_zone_fraction`]). The top and bottom
|
||||
/// stack a group above or below it; the left and right make a column on that side.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(in crate::docking) struct DockGroup<P> {
|
||||
body: Rect,
|
||||
/// The group takes tabs, so its body has a middle that joins it.
|
||||
center: bool,
|
||||
sides: [Option<(DockTarget<P>, Rect)>; 4],
|
||||
}
|
||||
|
||||
impl<P> DockGroup<P> {
|
||||
fn side(&self, zone: Zone) -> Option<&(DockTarget<P>, Rect)> {
|
||||
let index = match zone {
|
||||
Zone::Left => 0,
|
||||
Zone::Right => 1,
|
||||
Zone::Top => 2,
|
||||
Zone::Bottom => 3,
|
||||
Zone::Center => return None,
|
||||
};
|
||||
self.sides.get(index)?.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
/// The physical window edge a dock sits on.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(in crate::docking) enum Edge {
|
||||
@@ -230,8 +257,9 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
Edge::Left => (rect.left(), 1.0),
|
||||
Edge::Right => (rect.right(), -1.0),
|
||||
};
|
||||
let (band_width, line_width) = (self.style.column_edge_band, self.style.drop_line_width);
|
||||
if columns.is_empty() {
|
||||
let (band, line) = column_edge(outer, inward, rect.y_range());
|
||||
let (band, line) = column_edge(outer, rect.y_range(), band_width, line_width);
|
||||
let band = band.union(Rect::from_x_y_ranges(
|
||||
outer.min(outer + inward * self.style.new_dock_band)..=outer.max(outer + inward * self.style.new_dock_band),
|
||||
rect.y_range(),
|
||||
@@ -257,13 +285,24 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
let width = fitted.min((rect.width() - (x - outer).abs()).max(0.0));
|
||||
let inner = x + inward * width;
|
||||
let column_rect = Rect::from_x_y_ranges(x.min(inner)..=x.max(inner), rect.y_range());
|
||||
let groups = self.dock_groups.len();
|
||||
self.column(ui, column, first, column_rect, edge);
|
||||
if index == 0 {
|
||||
let (band, line) = column_edge(x, inward, rect.y_range());
|
||||
self.dock_targets.push(DockDrop { target: DockTarget::ColumnEdge { anchor: first.clone(), after: false }, band, line });
|
||||
let (outer_band, outer_line) = column_edge(x, rect.y_range(), band_width, line_width);
|
||||
let (inner_band, inner_line) = column_edge(inner, rect.y_range(), band_width, line_width);
|
||||
let before = (DockTarget::ColumnEdge { anchor: first.clone(), after: false }, outer_line);
|
||||
let after = (DockTarget::ColumnEdge { anchor: first.clone(), after: true }, inner_line);
|
||||
let (left, right) = match edge {
|
||||
Edge::Left => (before.clone(), after.clone()),
|
||||
Edge::Right => (after.clone(), before.clone()),
|
||||
};
|
||||
for group in self.dock_groups.iter_mut().skip(groups) {
|
||||
group.sides[0] = Some(left.clone());
|
||||
group.sides[1] = Some(right.clone());
|
||||
}
|
||||
let (band, line) = column_edge(inner, -inward, rect.y_range());
|
||||
self.dock_targets.push(DockDrop { target: DockTarget::ColumnEdge { anchor: first.clone(), after: true }, band, line });
|
||||
if index == 0 {
|
||||
self.dock_targets.push(DockDrop { target: before.0, band: outer_band, line: before.1 });
|
||||
}
|
||||
self.dock_targets.push(DockDrop { target: after.0, band: inner_band, line: after.1 });
|
||||
drawn.push((index, column, first, width, inner));
|
||||
x = inner;
|
||||
}
|
||||
@@ -408,10 +447,13 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
}
|
||||
let xs = view.x_range();
|
||||
let half = self.style.seam_band * 0.5;
|
||||
let width = self.style.drop_line_width;
|
||||
// A line on an edge scrolled out of the view shows at the view's edge.
|
||||
let line = |y: f32| horizontal_line(y.clamp(view.top() + width * 0.5, view.bottom() - width * 0.5), xs, width);
|
||||
let push = |targets: &mut Vec<DockDrop<P>>, target: DockTarget<P>, band: Rect, line: Rect| {
|
||||
let band = band.intersect(view);
|
||||
if usable(band) {
|
||||
targets.push(DockDrop { target, band, line: line.intersect(view.expand(2.0)) });
|
||||
targets.push(DockDrop { target, band, line });
|
||||
}
|
||||
};
|
||||
let mut previous: Option<&P> = None;
|
||||
@@ -419,20 +461,20 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
let Some(anchor) = group.panels.first() else { continue };
|
||||
if let Some(above) = previous {
|
||||
let band = Rect::from_x_y_ranges(xs, (rect.top() - half)..=(rect.top() + half));
|
||||
push(&mut self.dock_targets, DockTarget::Seam { anchor: above.clone() }, band, horizontal_line(rect.top(), xs));
|
||||
push(&mut self.dock_targets, DockTarget::Seam { anchor: above.clone() }, band, line(rect.top()));
|
||||
}
|
||||
if !group.minimized {
|
||||
let body = Rect::from_min_max(pos2(rect.left(), (rect.top() + self.style.tab_height).min(rect.bottom())), rect.max);
|
||||
let quarter = body.height() * 0.25;
|
||||
let upper = Rect::from_x_y_ranges(xs, body.top()..=(body.top() + quarter));
|
||||
let lower = Rect::from_x_y_ranges(xs, (body.bottom() - quarter)..=body.bottom());
|
||||
push(&mut self.dock_targets, DockTarget::GroupBand { anchor: anchor.clone(), below: false }, upper, horizontal_line(rect.top(), xs));
|
||||
push(
|
||||
&mut self.dock_targets,
|
||||
DockTarget::GroupBand { anchor: anchor.clone(), below: true },
|
||||
lower,
|
||||
horizontal_line(rect.bottom(), xs),
|
||||
);
|
||||
let header = if stripless(&group.panels, &*self.content) { 0.0 } else { self.style.tab_height };
|
||||
let body = Rect::from_min_max(pos2(rect.left(), (rect.top() + header).min(rect.bottom())), rect.max).intersect(view);
|
||||
if !group.minimized && usable(body) {
|
||||
let center = group.panels.get(group.active).is_some_and(|p| self.permissions(p).accepts_tabs);
|
||||
let (above, below) =
|
||||
(DockTarget::GroupBand { anchor: anchor.clone(), below: false }, DockTarget::GroupBand { anchor: anchor.clone(), below: true });
|
||||
// The left and right sides are the column's edges, set once the column is drawn.
|
||||
self.dock_groups.push(DockGroup {
|
||||
body,
|
||||
center,
|
||||
sides: [None, None, Some((above, line(rect.top()))), Some((below, line(rect.bottom())))],
|
||||
});
|
||||
}
|
||||
previous = Some(anchor);
|
||||
}
|
||||
@@ -441,7 +483,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
&& top < view.bottom()
|
||||
{
|
||||
let band = Rect::from_x_y_ranges(xs, top..=view.bottom());
|
||||
push(&mut self.dock_targets, DockTarget::GroupBand { anchor: last.clone(), below: true }, band, horizontal_line(top, xs));
|
||||
push(&mut self.dock_targets, DockTarget::GroupBand { anchor: last.clone(), below: true }, band, line(top));
|
||||
}
|
||||
// Height dividers between an open group and the next open one, after the groups so
|
||||
// their hit areas win at the boundary. The heights of the groups a drag changes are
|
||||
@@ -583,12 +625,48 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
Edge::Right => (self.workspace.right(), -self.style.new_dock_band),
|
||||
};
|
||||
let band = Rect::from_x_y_ranges(x.min(x + inward)..=x.max(x + inward), ys);
|
||||
let line = vertical_line(x + inward.signum() * 2.0, ys);
|
||||
let width = self.style.drop_line_width;
|
||||
let line = vertical_line(x + inward.signum() * width * 0.5, ys, width);
|
||||
candidates.push(DockDrop { target: DockTarget::NewDock { side }, band, line });
|
||||
}
|
||||
candidates
|
||||
}
|
||||
|
||||
/// Whether `pointer` is on the tab strip of the frontmost group under it, and that group
|
||||
/// takes tabs: the strip joins the group, and border bands reach into it only
|
||||
/// [`STRIP_REACH`] from their lines. Not with [`DockStyle::side_edge_zones`].
|
||||
pub(super) fn in_tab_strip(&self, pointer: Pos2) -> bool {
|
||||
!self.style.side_edge_zones
|
||||
&& self.targets.iter().rev().find(|t| usable(t.rect) && t.rect.contains(pointer)).is_some_and(|t| {
|
||||
t.accepts_tabs && t.tab_strip.map(|strip| strip.intersect(t.rect)).is_some_and(|strip| usable(strip) && strip.contains(pointer))
|
||||
})
|
||||
}
|
||||
|
||||
/// The dock target at `pointer`: a band at a border, else a side zone of a dock group's
|
||||
/// body ([`SIDE_EDGE_ZONE_DEPTH`] deep with [`DockStyle::side_edge_zones`]). The middle of a
|
||||
/// group that takes tabs is none: the group's own target joins it.
|
||||
fn dock_drop(&self, pointer: Pos2) -> Option<DockDrop<P>> {
|
||||
let mut candidates = self.dock_candidates();
|
||||
let strip = self.in_tab_strip(pointer);
|
||||
if strip {
|
||||
candidates.retain(|t| t.line.distance_to_pos(pointer) <= STRIP_REACH);
|
||||
}
|
||||
if let Some(drop) = resolve_dock(pointer, &candidates) {
|
||||
return Some(drop.clone());
|
||||
}
|
||||
if strip {
|
||||
return None;
|
||||
}
|
||||
let group = self.dock_groups.iter().find(|g| g.body.contains(pointer))?;
|
||||
let zone = if self.style.side_edge_zones {
|
||||
drop_zone(group.body, 0.0, SIDE_EDGE_ZONE_DEPTH, pointer)?
|
||||
} else {
|
||||
body_zone(group.body, 0.0, self.style.side_zone_fraction, group.center, pointer)?
|
||||
};
|
||||
let (target, line) = group.side(zone)?.clone();
|
||||
Some(DockDrop { target, band: group.body, line })
|
||||
}
|
||||
|
||||
/// The dock target a whole group or column drag would take at `pointer`, with the action
|
||||
/// and its line. A target that leaves the layout unchanged is a no-op: `None` action.
|
||||
pub(super) fn dock_group_proposal(&self, drag: &GroupDrag<P>, pointer: Pos2) -> Option<(Option<Action<P>>, Rect)> {
|
||||
@@ -596,8 +674,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
if self.dock_covered(pointer, moving) {
|
||||
return None;
|
||||
}
|
||||
let candidates = self.dock_candidates();
|
||||
let drop = resolve_dock(pointer, &candidates)?;
|
||||
let drop = self.dock_drop(pointer)?;
|
||||
let action = group_action(drag, drop.target.to());
|
||||
self.checked(action).map(|action| (action, drop.line))
|
||||
}
|
||||
@@ -609,8 +686,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
if self.dock_covered(pointer, None) {
|
||||
return None;
|
||||
}
|
||||
let candidates = self.dock_candidates();
|
||||
let drop = resolve_dock(pointer, &candidates)?;
|
||||
let drop = self.dock_drop(pointer)?;
|
||||
let action = match drop.target.to() {
|
||||
// A target named by the dragged panel is its own group or column: another panel
|
||||
// there names the same one. With none, the drop leaves the panel where it is.
|
||||
@@ -638,8 +714,8 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
Some((candidate != *self.layout).then_some(action))
|
||||
}
|
||||
|
||||
/// The insertion line of a dock target.
|
||||
pub(super) fn paint_dock_line(&self, ctx: &Context, line: Rect) {
|
||||
/// The line a drop beside a group, column, dock or the layout's edge previews with.
|
||||
pub(super) fn paint_drop_line(&self, ctx: &Context, line: Rect) {
|
||||
if usable(line) {
|
||||
let painter = self.preview_painter(ctx);
|
||||
self.iconic.outline(&painter, line, self.style);
|
||||
|
||||
@@ -168,10 +168,14 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
|
||||
/// The outer edge band under the pointer (P8): top and bottom, and left and right with
|
||||
/// [`crate::docking::DockStyle::side_edge_zones`]. Returns a docked anchor that is not
|
||||
/// moving, the zone and the preview of the new frame.
|
||||
/// 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.
|
||||
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 = self.style.edge_band.min(area.width() / 4.0).min(area.height() / 4.0);
|
||||
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)]);
|
||||
@@ -180,8 +184,15 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
if distance > band {
|
||||
return None;
|
||||
}
|
||||
let reach = |line: Rect| line.distance_to_pos(pointer) <= crate::docking::targets::STRIP_REACH;
|
||||
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();
|
||||
if !self.style.side_edge_zones {
|
||||
return Some((anchor, zone, line(zone)?));
|
||||
}
|
||||
let size = area.size() * 0.25;
|
||||
let preview = match zone {
|
||||
Zone::Left => Rect::from_min_size(area.min, vec2(size.x, area.height())),
|
||||
|
||||
@@ -181,7 +181,8 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
for (below, ys, line) in [(false, upper_top..=middle, rect.top()), (true, middle..=lower_bottom, rect.bottom())] {
|
||||
let band = Rect::from_x_y_ranges(xs, ys).intersect(view);
|
||||
if usable(band) {
|
||||
let line = horizontal_line(line.clamp(view.top() + 2.0, view.bottom() - 2.0), xs);
|
||||
let width = self.style.drop_line_width;
|
||||
let line = horizontal_line(line.clamp(view.top() + width * 0.5, view.bottom() - width * 0.5), xs, width);
|
||||
self.dock_targets.push(DockDrop { target: DockTarget::GroupBand { anchor: anchor.clone(), below }, band, line });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +102,8 @@ pub(super) struct Renderer<'a, P> {
|
||||
pub(super) targets: Vec<DropTarget<P>>,
|
||||
/// [D] Seams, group bands, column edges and new-dock bands, see [`crate::docking::targets::DockDrop`].
|
||||
pub(super) dock_targets: Vec<crate::docking::targets::DockDrop<P>>,
|
||||
/// [D] The open groups of dock columns, whose body zones stack or make columns.
|
||||
pub(super) dock_groups: Vec<dock::DockGroup<P>>,
|
||||
pub(super) owned_interactions: Vec<Id>,
|
||||
pub(super) output: DockOutput<P>,
|
||||
pub(super) frame: FrameState<P>,
|
||||
|
||||
@@ -6,7 +6,9 @@ use super::{DROP_MARGIN, Drawn, Renderer, clamp_float, dock::group_action};
|
||||
use crate::docking::chrome::drop_preview;
|
||||
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,
|
||||
dock_model::Place,
|
||||
surface::TAB_PADDING,
|
||||
targets::{SIDE_EDGE_ZONE_DEPTH, resolve_drop_by},
|
||||
};
|
||||
use crate::layout::SplitAxis;
|
||||
|
||||
@@ -15,6 +17,16 @@ fn float_only(ctx: &Context) -> bool {
|
||||
ctx.input(|i| i.modifiers.command || i.modifiers.ctrl || i.modifiers.mac_cmd)
|
||||
}
|
||||
|
||||
/// Share of the accent that fills a tab drop's outline: light, so it does not read as a line.
|
||||
const TAB_FILL: f32 = 0.08;
|
||||
|
||||
/// The preview of a drop that joins a group as a tab: a 2 pt accent outline inside `rect`, with
|
||||
/// a light fill.
|
||||
fn tab_outline(painter: &egui::Painter, rect: Rect, accent: egui::Color32) {
|
||||
painter.rect_filled(rect, 0.0, accent.gamma_multiply(TAB_FILL));
|
||||
painter.rect_stroke(rect, 0.0, egui::Stroke::new(2.0, accent), egui::StrokeKind::Inside);
|
||||
}
|
||||
|
||||
/// Height of a panel floated out of a dock column when the host gives no floating size.
|
||||
const DOCKED_FLOAT_HEIGHT: f32 = 320.0;
|
||||
|
||||
@@ -174,10 +186,11 @@ 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)
|
||||
}
|
||||
|
||||
/// [`resolve_drop`], where a split that would leave a group without room becomes a join
|
||||
/// of the target group.
|
||||
/// [`crate::docking::resolve_drop_by`] with [`crate::docking::DockStyle::zone_rule`], 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(panel, pointer, targets, self.style.zone_depth)?;
|
||||
let proposal = resolve_drop_by(panel, pointer, targets, self.style.zone_rule())?;
|
||||
if !matches!(proposal.placement, Placement::Split(zone) if zone != Zone::Center) {
|
||||
return Some(proposal);
|
||||
}
|
||||
@@ -185,7 +198,7 @@ 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(panel, pointer, std::slice::from_ref(&DropTarget { allow_splits: false, ..target.clone() }), self.style.zone_depth)
|
||||
resolve_drop_by(panel, pointer, std::slice::from_ref(&DropTarget { allow_splits: false, ..target.clone() }), self.style.zone_rule())
|
||||
}
|
||||
|
||||
/// Whether `action`, a drop that splits a group or the outer edge, leaves room: every group
|
||||
@@ -285,7 +298,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
}
|
||||
if let Some((action, line)) = self.dock_group_proposal(drag, pointer) {
|
||||
if action.is_some() {
|
||||
self.paint_dock_line(ctx, line);
|
||||
self.paint_drop_line(ctx, line);
|
||||
if drag.gesture != GroupGesture::Window {
|
||||
self.paint_float_hint(ctx, pointer);
|
||||
}
|
||||
@@ -295,12 +308,11 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
let proposal = self.group_proposal(drag, pointer).filter(|p| !p.noop);
|
||||
let zones = self.paint_group_zones(&painter, drag, pointer, proposal.as_ref());
|
||||
if let Some(proposal) = proposal {
|
||||
let split = matches!(proposal.placement, Placement::Split(zone) | Placement::Edge(zone) if zone != Zone::Center);
|
||||
if crate::docking::targets::usable(proposal.preview) && !zones {
|
||||
drop_preview(&painter, proposal.preview.shrink(1.0), self.style.accent, split);
|
||||
}
|
||||
if let Some(marker) = proposal.insertion {
|
||||
painter.rect_filled(marker, 0.0, self.style.accent);
|
||||
if zones {
|
||||
self.paint_marker(&painter, &proposal);
|
||||
} else {
|
||||
let split = matches!(proposal.placement, Placement::Split(zone) | Placement::Edge(zone) if zone != Zone::Center);
|
||||
self.paint_preview(ctx, &painter, &proposal, split);
|
||||
}
|
||||
if drag.gesture != GroupGesture::Window {
|
||||
self.paint_float_hint(ctx, pointer);
|
||||
@@ -357,7 +369,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
self.output.focused = Some(drag.panel);
|
||||
}
|
||||
} else if action.is_some() {
|
||||
self.paint_dock_line(ctx, line);
|
||||
self.paint_drop_line(ctx, line);
|
||||
self.paint_float_hint(ctx, pointer);
|
||||
}
|
||||
return;
|
||||
@@ -421,17 +433,41 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
// Reordering inside the source strip shows only the insertion line.
|
||||
let same_group = proposal.insertion.is_some()
|
||||
&& self.targets.iter().any(|target| target.rect == proposal.preview && target.tabs.iter().any(|(panel, _)| panel == &drag.panel));
|
||||
if !same_group && !proposal.noop && crate::docking::targets::usable(proposal.preview) && !zones {
|
||||
drop_preview(painter, proposal.preview.shrink(1.0), self.style.accent, true);
|
||||
}
|
||||
if let Some(marker) = proposal.insertion {
|
||||
painter.rect_filled(marker, 0.0, self.style.accent);
|
||||
if !same_group && !proposal.noop && !zones {
|
||||
self.paint_preview(ctx, painter, proposal, true);
|
||||
} else {
|
||||
self.paint_marker(painter, proposal);
|
||||
}
|
||||
if !proposal.noop {
|
||||
self.paint_float_hint(ctx, pointer);
|
||||
}
|
||||
}
|
||||
|
||||
/// The preview of a group target, for tab and group drags: a line along the edge of a drop
|
||||
/// beside the group or the layout, else the tab outline of the whole group, and the
|
||||
/// insertion marker on a strip. With [`crate::docking::DockStyle::side_edge_zones`], the
|
||||
/// region the drop takes, filled for a tab drag (`filled`) or a split.
|
||||
fn paint_preview(&self, ctx: &Context, painter: &egui::Painter, proposal: &DropProposal<P>, filled: bool) {
|
||||
if crate::docking::targets::usable(proposal.preview) {
|
||||
let beside = matches!(proposal.placement, Placement::Split(zone) | Placement::Edge(zone) if zone != Zone::Center);
|
||||
if self.style.side_edge_zones {
|
||||
drop_preview(painter, proposal.preview.shrink(1.0), self.style.accent, filled);
|
||||
} else if beside {
|
||||
self.paint_drop_line(ctx, proposal.preview);
|
||||
} else {
|
||||
tab_outline(painter, proposal.preview.shrink(1.0), self.style.accent);
|
||||
}
|
||||
}
|
||||
self.paint_marker(painter, proposal);
|
||||
}
|
||||
|
||||
/// The insertion marker of a drop on a tab strip.
|
||||
fn paint_marker(&self, painter: &egui::Painter, proposal: &DropProposal<P>) {
|
||||
if let Some(marker) = proposal.insertion {
|
||||
painter.rect_filled(marker, 0.0, self.style.accent);
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Self::paint_zones`] for a tab drag of `panel`. The center of the group holding the panel
|
||||
/// is not a target: a drop there reorders or changes nothing.
|
||||
pub(super) fn paint_tab_zones(&self, painter: &egui::Painter, panel: &P, pointer: Pos2, proposal: Option<&DropProposal<P>>) -> bool {
|
||||
@@ -489,7 +525,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
let takes = |zone: Zone, point: Pos2| at(point).as_ref().and_then(zone_of) == Some(zone);
|
||||
let mut shapes: Vec<(Zone, Vec<Pos2>)> = [Zone::Left, Zone::Right, Zone::Top, Zone::Bottom, Zone::Center]
|
||||
.into_iter()
|
||||
.filter_map(|zone| Some((zone, zone_shape(target.rect, header, self.style.zone_depth, zone)?)))
|
||||
.filter_map(|zone| Some((zone, zone_shape(target.rect, header, SIDE_EDGE_ZONE_DEPTH, zone)?)))
|
||||
.filter(|(zone, points)| takes(*zone, centroid(points).to_pos2()))
|
||||
.collect();
|
||||
let strip = target.tab_strip.map(|strip| strip.intersect(target.rect)).filter(|strip| usable(*strip) && takes(Zone::Center, strip.center()));
|
||||
|
||||
@@ -780,7 +780,7 @@ fn the_preview_fills_the_zone_under_the_pointer_and_no_result_rect() {
|
||||
assert_eq!(zone_outlines(&d).len(), 6);
|
||||
d.key(egui::Key::Escape);
|
||||
|
||||
// The dock-and-canvas arrangement keeps its filled half.
|
||||
// The dock-and-canvas arrangement shows a line along the group's left edge instead.
|
||||
let mut d = DockDriver::new(editing(), Film::default());
|
||||
d.screen = Rect::from_min_size(Pos2::ZERO, SCREEN);
|
||||
d.area = d.screen;
|
||||
@@ -788,7 +788,8 @@ fn the_preview_fills_the_zone_under_the_pointer_and_no_result_rect() {
|
||||
d.hold(tab(&d, INFO), pos2(timeline.left() + 8.0, timeline.center().y + 13.0));
|
||||
assert!(!d.filled_polygon(&zone_shape(timeline, 26.0, 40.0, Zone::Left).unwrap()));
|
||||
assert!(zone_outlines(&d).is_empty());
|
||||
assert!(d.filled(zone_rect(timeline, Zone::Left).unwrap().shrink(1.0)));
|
||||
assert!(!d.filled(zone_rect(timeline, Zone::Left).unwrap().shrink(1.0)));
|
||||
assert!(d.filled(Rect::from_x_y_ranges(timeline.left()..=(timeline.left() + 4.0), timeline.y_range())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -53,23 +53,38 @@ pub struct DockStyle {
|
||||
pub blocked_dim: Color32,
|
||||
/// Height of a floating window's title bar.
|
||||
pub title_height: f32,
|
||||
/// Width of the band along the layout's outer edge that splits at the root.
|
||||
/// Width of the band along the layout's outer edge that splits at the root (28 pt). It is
|
||||
/// at most a quarter of the layout's width and height. Not used with
|
||||
/// [`Self::side_edge_zones`], whose band is 8 pt.
|
||||
pub edge_band: f32,
|
||||
/// The outer edge band also runs along the left and right edges, where a drop adds a
|
||||
/// full-height column. Off: only the top and bottom edges add a full-width row.
|
||||
/// The split-tree profile. The outer edge band also runs along the left and right edges,
|
||||
/// where a drop adds a full-height column. A group's side zones are trapezoids
|
||||
/// [`crate::docking::SIDE_EDGE_ZONE_DEPTH`] deep (see [`crate::docking::drop_zone`]), a drag
|
||||
/// outlines them, and a split previews the half of the group it takes. Off: only the top and
|
||||
/// 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,
|
||||
/// How far a group's side drop zones reach into its body, below the tab strip. All four
|
||||
/// zones share one depth: this, or a quarter of the body's smaller side when that is less,
|
||||
/// so the body keeps a center.
|
||||
pub zone_depth: f32,
|
||||
/// 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
|
||||
/// [`Self::side_edge_zones`].
|
||||
pub side_zone_fraction: f32,
|
||||
/// Width of the line a drop beside a group, column, dock or the layout's edge previews with
|
||||
/// (4 pt, more than zero).
|
||||
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`]).
|
||||
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,
|
||||
/// [D] Height of the band around the seam between two stacked groups in a dock column.
|
||||
/// [D] Height of the band centred on the seam between two stacked groups in a dock column
|
||||
/// (28 pt).
|
||||
pub seam_band: f32,
|
||||
/// [D] Width of the band along the docked area's left and right edges that makes a new dock.
|
||||
/// [D] Width of the band centred on each edge of a dock column, where a drop makes a new
|
||||
/// column (28 pt).
|
||||
pub column_edge_band: f32,
|
||||
/// [D] Width of the band along the docked area's left and right edges, and inside an empty
|
||||
/// dock, that makes a new dock (28 pt).
|
||||
pub new_dock_band: f32,
|
||||
/// [D] Side of a panel icon in an iconic column; each icon row is 10 pt taller.
|
||||
pub icon_size: f32,
|
||||
@@ -173,13 +188,15 @@ impl DockStyle {
|
||||
floating_stroke: visuals.window_stroke,
|
||||
blocked_dim: visuals.panel_fill.gamma_multiply(0.6),
|
||||
title_height: super::chrome::TITLE_HEIGHT,
|
||||
edge_band: 8.0,
|
||||
edge_band: 28.0,
|
||||
side_edge_zones: false,
|
||||
zone_depth: 40.0,
|
||||
side_zone_fraction: 1.0 / 3.0,
|
||||
drop_line_width: 4.0,
|
||||
new_dock_zones: true,
|
||||
column_bar_height: 14.0,
|
||||
seam_band: 8.0,
|
||||
new_dock_band: 12.0,
|
||||
seam_band: 28.0,
|
||||
column_edge_band: 28.0,
|
||||
new_dock_band: 28.0,
|
||||
icon_size: 20.0,
|
||||
iconic_width: 36.0,
|
||||
iconic_max_width: 240.0,
|
||||
@@ -210,6 +227,17 @@ impl DockStyle {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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`].
|
||||
pub fn zone_rule(&self) -> super::ZoneRule {
|
||||
if self.side_edge_zones {
|
||||
super::ZoneRule::Depth(super::SIDE_EDGE_ZONE_DEPTH)
|
||||
} else {
|
||||
super::ZoneRule::Fraction { fraction: self.side_zone_fraction, line: self.drop_line_width }
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn valid(&self) -> bool {
|
||||
[
|
||||
self.tab_height,
|
||||
@@ -222,9 +250,11 @@ impl DockStyle {
|
||||
self.floating_stroke.width,
|
||||
self.title_height,
|
||||
self.edge_band,
|
||||
self.zone_depth,
|
||||
self.side_zone_fraction,
|
||||
self.drop_line_width,
|
||||
self.column_bar_height,
|
||||
self.seam_band,
|
||||
self.column_edge_band,
|
||||
self.new_dock_band,
|
||||
self.icon_size,
|
||||
self.iconic_width,
|
||||
@@ -236,6 +266,8 @@ impl DockStyle {
|
||||
&& self.iconic_width <= self.iconic_max_width
|
||||
&& self.tab_height > 0.0
|
||||
&& self.font.size > 0.0
|
||||
&& self.drop_line_width > 0.0
|
||||
&& self.side_zone_fraction <= 0.5
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1164,6 +1196,7 @@ impl<'a, P: Clone + Eq + Hash + Debug + Send + Sync + 'static> DockArea<'a, P> {
|
||||
float_bounds: self.float_bounds.map_or(ui.ctx().content_rect(), |bounds| bounds.intersect(ui.ctx().content_rect())),
|
||||
targets: Vec::new(),
|
||||
dock_targets: Vec::new(),
|
||||
dock_groups: Vec::new(),
|
||||
owned_interactions: Vec::new(),
|
||||
output: DockOutput::default(),
|
||||
frame,
|
||||
|
||||
@@ -31,8 +31,18 @@ pub(super) fn usable(rect: Rect) -> bool {
|
||||
rect.is_finite() && rect.width() > 0.0 && rect.height() > 0.0 && rect.width() <= 1_000_000.0 && rect.height() <= 1_000_000.0
|
||||
}
|
||||
|
||||
/// Tab header (the top `tab_height` of `rect`) is the center; otherwise the nearest side zone of
|
||||
/// the body below it wins. Every side zone reaches the same depth into the body: `depth`, or a
|
||||
/// Depth of a group's side drop zones with [`crate::docking::DockStyle::side_edge_zones`]: the
|
||||
/// `depth` that [`drop_zone`], [`zone_shape`] and [`resolve_drop`] take there.
|
||||
pub const SIDE_EDGE_ZONE_DEPTH: f32 = 40.0;
|
||||
|
||||
/// Width of the band along the layout's outer edge with
|
||||
/// [`crate::docking::DockStyle::side_edge_zones`], in place of
|
||||
/// [`crate::docking::DockStyle::edge_band`].
|
||||
pub(super) const SIDE_EDGE_BAND: f32 = 8.0;
|
||||
|
||||
/// The picture-frame zones of [`crate::docking::DockStyle::side_edge_zones`]. Tab header (the
|
||||
/// top `tab_height` of `rect`) is the center; otherwise the nearest side zone of the body below
|
||||
/// it wins. Every side zone reaches the same depth into the body: `depth`, or a
|
||||
/// quarter of the body's smaller side when that is less, so a body always keeps a center and a
|
||||
/// narrow body's top and bottom zones are no deeper than its left and right ones. A `depth` of
|
||||
/// zero or less leaves only the center. Outside, non-finite, empty and inverted rectangles have
|
||||
@@ -60,6 +70,49 @@ pub fn drop_zone(rect: Rect, tab_height: f32, depth: f32, pointer: Pos2) -> Opti
|
||||
})
|
||||
}
|
||||
|
||||
/// The zones of the default profile. Tab header (the top `tab_height` of `rect`) is the center.
|
||||
/// 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.
|
||||
pub fn body_zone(rect: Rect, tab_height: f32, fraction: f32, center: bool, pointer: Pos2) -> Option<Zone> {
|
||||
if !usable(rect) || !pointer.is_finite() || !rect.contains(pointer) || !tab_height.is_finite() || tab_height < 0.0 {
|
||||
return None;
|
||||
}
|
||||
let body = body(rect, tab_height);
|
||||
if pointer.y < body.top() || !usable(body) || fraction.is_nan() || fraction <= 0.0 {
|
||||
return Some(Zone::Center);
|
||||
}
|
||||
let fraction = fraction.min(0.5);
|
||||
let (w, h) = (body.width(), body.height());
|
||||
let sides = [
|
||||
((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))?;
|
||||
Some(if center && distance > fraction { Zone::Center } else { zone })
|
||||
}
|
||||
|
||||
/// The preview line of a drop beside `rect` on `zone`'s side: `width` thick along that whole
|
||||
/// edge, inside `rect`. None for the center.
|
||||
pub(super) fn edge_line(rect: Rect, zone: Zone, width: f32) -> Option<Rect> {
|
||||
if !usable(rect) {
|
||||
return None;
|
||||
}
|
||||
let (w, h) = (width.max(1.0).min(rect.width()), width.max(1.0).min(rect.height()));
|
||||
Some(match zone {
|
||||
Zone::Left => Rect::from_x_y_ranges(rect.left()..=(rect.left() + w), rect.y_range()),
|
||||
Zone::Right => Rect::from_x_y_ranges((rect.right() - w)..=rect.right(), rect.y_range()),
|
||||
Zone::Top => Rect::from_x_y_ranges(rect.x_range(), rect.top()..=(rect.top() + h)),
|
||||
Zone::Bottom => Rect::from_x_y_ranges(rect.x_range(), (rect.bottom() - h)..=rect.bottom()),
|
||||
Zone::Center => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// The part of a group drawn at `rect` below its tab header.
|
||||
fn body(rect: Rect, tab_height: f32) -> Rect {
|
||||
Rect::from_min_max(pos2(rect.left(), rect.top() + tab_height.min(rect.height())), rect.max)
|
||||
@@ -106,7 +159,8 @@ pub(super) fn header_height<P>(target: &DropTarget<P>) -> f32 {
|
||||
target.tab_strip.map(|strip| strip.intersect(target.rect)).filter(|strip| usable(*strip)).map_or(0.0, |strip| strip.bottom() - target.rect.top())
|
||||
}
|
||||
|
||||
/// Preview of the region occupied after a split or group merge.
|
||||
/// The region a split or group merge occupies: the half of `rect` on `zone`'s side, or all of
|
||||
/// it. [`resolve_drop`] previews with it.
|
||||
pub fn zone_rect(rect: Rect, zone: Zone) -> Option<Rect> {
|
||||
if !usable(rect) {
|
||||
return None;
|
||||
@@ -152,10 +206,29 @@ pub fn tab_insertion_marker<P: Eq>(target: &DropTarget<P>, before: Option<&P>) -
|
||||
|
||||
/// Resolve a pointer against visible groups. The frontmost containing group occludes groups
|
||||
/// behind it even if it rejects the drop. A strip reorders by midpoint; the body of the source
|
||||
/// group is a no-op at center. The body's side zones are [`drop_zone`]'s with `zone_depth` (see
|
||||
/// [`crate::docking::DockStyle::zone_depth`]): one depth on all four sides, at most a quarter of
|
||||
/// the body's smaller side. Floating groups normally set `allow_splits` to false.
|
||||
/// group is a no-op at center. The body's side zones are [`drop_zone`]'s with `zone_depth`
|
||||
/// ([`SIDE_EDGE_ZONE_DEPTH`] in the docking area): one depth on all four sides, at most a quarter
|
||||
/// of the body's smaller side. A split previews the half of the group it takes ([`zone_rect`]).
|
||||
/// Floating groups normally set `allow_splits` to false. These are the split-tree zones; a host
|
||||
/// that resolves drops for a [`crate::docking::DockStyle`] calls [`resolve_drop_by`] with its
|
||||
/// [`crate::docking::DockStyle::zone_rule`].
|
||||
pub fn resolve_drop<P: Clone + Eq>(panel: &P, pointer: Pos2, targets: &[DropTarget<P>], zone_depth: f32) -> Option<DropProposal<P>> {
|
||||
resolve_drop_by(panel, pointer, targets, ZoneRule::Depth(zone_depth))
|
||||
}
|
||||
|
||||
/// How [`resolve_drop_by`] divides a group's body.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum ZoneRule {
|
||||
/// [`drop_zone`] with this depth; a split previews the half it takes.
|
||||
Depth(f32),
|
||||
/// [`body_zone`] with this fraction; a split previews a `line` thick line along the
|
||||
/// group's edge on that side.
|
||||
Fraction { fraction: f32, line: f32 },
|
||||
}
|
||||
|
||||
/// [`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>> {
|
||||
if !pointer.is_finite() || targets.len() > super::MAX_PANELS {
|
||||
return None;
|
||||
}
|
||||
@@ -193,7 +266,12 @@ pub fn resolve_drop<P: Clone + Eq>(panel: &P, pointer: Pos2, targets: &[DropTarg
|
||||
noop,
|
||||
});
|
||||
}
|
||||
let zone = if target.allow_splits { drop_zone(target.rect, header_height(target), zone_depth, pointer)? } else { Zone::Center };
|
||||
let header = header_height(target);
|
||||
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)?,
|
||||
};
|
||||
if zone == Zone::Center && target.accepts_tabs && target.tabs.iter().any(|(p, _)| p == panel) {
|
||||
return Some(DropProposal {
|
||||
anchor: anchor.clone(),
|
||||
@@ -216,7 +294,10 @@ pub fn resolve_drop<P: Clone + Eq>(panel: &P, pointer: Pos2, targets: &[DropTarg
|
||||
Some(DropProposal {
|
||||
anchor: anchor.clone(),
|
||||
placement: Placement::Split(zone),
|
||||
preview: zone_rect(target.rect, zone)?,
|
||||
preview: match rule {
|
||||
ZoneRule::Fraction { line, .. } if zone != Zone::Center => edge_line(target.rect, zone, line)?,
|
||||
_ => zone_rect(target.rect, zone)?,
|
||||
},
|
||||
insertion: None,
|
||||
noop: false,
|
||||
})
|
||||
@@ -238,7 +319,7 @@ pub(super) enum DockTarget<P> {
|
||||
ColumnEdge { anchor: P, after: bool },
|
||||
/// The seam between two stacked groups: a new group below the group of `anchor` (P4).
|
||||
Seam { anchor: P },
|
||||
/// The top or bottom quarter of a group body, or the free room under a column's last group (P3).
|
||||
/// The top or bottom zone of a group body, or the free room under a column's last group (P3).
|
||||
GroupBand { anchor: P, below: bool },
|
||||
/// A window edge without a dock, or an empty dock (P5, P6).
|
||||
NewDock { side: Side },
|
||||
@@ -273,23 +354,21 @@ pub(super) fn resolve_dock<P: Clone>(pointer: Pos2, targets: &[DockDrop<P>]) ->
|
||||
targets.iter().filter(|t| usable(t.band) && t.band.contains(pointer)).min_by_key(|t| t.target.rank())
|
||||
}
|
||||
|
||||
/// Width of the insertion lines dock targets preview with.
|
||||
const LINE: f32 = 4.0;
|
||||
/// How far a column edge band reaches into the column, and past the edge out of it.
|
||||
const EDGE_IN: f32 = 6.0;
|
||||
const EDGE_OUT: f32 = 10.0;
|
||||
/// How far from its line a border band reaches into a tab strip that takes tabs: the rest of
|
||||
/// the strip joins the group.
|
||||
pub(super) const STRIP_REACH: f32 = 4.0;
|
||||
|
||||
/// Band and line of a column edge at `x`. `inward` is +1 when the column lies right of the edge,
|
||||
/// -1 when it lies left of it. The band reaches further out of the column than into it.
|
||||
pub(super) fn column_edge(x: f32, inward: f32, ys: Rangef) -> (Rect, Rect) {
|
||||
let (a, b) = (x + inward * EDGE_IN, x - inward * EDGE_OUT);
|
||||
(Rect::from_x_y_ranges(a.min(b)..=a.max(b), ys), vertical_line(x, ys))
|
||||
/// Band and line of a column edge at `x`: the band `band` wide, centred on the edge, and the
|
||||
/// line `line` wide.
|
||||
pub(super) fn column_edge(x: f32, ys: Rangef, band: f32, line: f32) -> (Rect, Rect) {
|
||||
let half = band.max(0.0) * 0.5;
|
||||
(Rect::from_x_y_ranges((x - half)..=(x + half), ys), vertical_line(x, ys, line))
|
||||
}
|
||||
|
||||
pub(super) fn vertical_line(x: f32, ys: Rangef) -> Rect {
|
||||
Rect::from_x_y_ranges((x - LINE * 0.5)..=(x + LINE * 0.5), ys)
|
||||
pub(super) fn vertical_line(x: f32, ys: Rangef, width: f32) -> Rect {
|
||||
Rect::from_x_y_ranges((x - width * 0.5)..=(x + width * 0.5), ys)
|
||||
}
|
||||
|
||||
pub(super) fn horizontal_line(y: f32, xs: Rangef) -> Rect {
|
||||
Rect::from_x_y_ranges(xs, (y - LINE * 0.5)..=(y + LINE * 0.5))
|
||||
pub(super) fn horizontal_line(y: f32, xs: Rangef, width: f32) -> Rect {
|
||||
Rect::from_x_y_ranges(xs, (y - width * 0.5)..=(y + width * 0.5))
|
||||
}
|
||||
@@ -303,8 +303,8 @@ fn pointer_targets_distinguish_reorder_center_split_and_occlusion() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn side_zones_are_40_pt_deep_or_a_quarter_of_the_bodys_smaller_side() {
|
||||
let depth = DockStyle::default().zone_depth;
|
||||
fn side_edge_zones_are_40_pt_deep_or_a_quarter_of_the_bodys_smaller_side() {
|
||||
let depth = SIDE_EDGE_ZONE_DEPTH;
|
||||
assert_eq!(depth, 40.0);
|
||||
// The inner corner of the left trapezoid is the depth of both bands.
|
||||
let inner = |rect: Rect| zone_shape(rect, 0.0, depth, Zone::Left).unwrap()[1] - rect.min;
|
||||
@@ -336,6 +336,70 @@ fn side_zones_are_40_pt_deep_or_a_quarter_of_the_bodys_smaller_side() {
|
||||
assert!(zone_shape(rect, 0.0, 0.0, Zone::Left).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_bodys_outer_thirds_are_its_sides_and_the_relatively_nearer_side_wins_at_a_corner() {
|
||||
let fraction = DockStyle::default().side_zone_fraction;
|
||||
assert!((fraction - 1.0 / 3.0).abs() < 1e-6);
|
||||
// A 24 pt header over a 600 x 300 body.
|
||||
let rect = Rect::from_min_size(pos2(10.0, 20.0), vec2(600.0, 324.0));
|
||||
let body = Rect::from_min_max(pos2(10.0, 44.0), rect.max);
|
||||
let zone = |p| body_zone(rect, 24.0, fraction, true, p);
|
||||
let y = body.center().y;
|
||||
assert_eq!(zone(pos2(body.left() + 199.0, y)), Some(Zone::Left));
|
||||
assert_eq!(zone(pos2(body.left() + 201.0, y)), Some(Zone::Center));
|
||||
assert_eq!(zone(pos2(body.right() - 199.0, y)), Some(Zone::Right));
|
||||
assert_eq!(zone(pos2(body.center().x, body.top() + 99.0)), Some(Zone::Top));
|
||||
assert_eq!(zone(pos2(body.center().x, body.top() + 101.0)), Some(Zone::Center));
|
||||
assert_eq!(zone(pos2(body.center().x, body.bottom() - 99.0)), Some(Zone::Bottom));
|
||||
assert_eq!(zone(pos2(body.left() + 2.0, rect.top() + 10.0)), Some(Zone::Center), "the header joins");
|
||||
// In a corner: a quarter of the width from the left is further than a fifth of the height
|
||||
// from the top, a tenth of the width nearer than three tenths of the height.
|
||||
assert_eq!(zone(pos2(body.left() + 150.0, body.top() + 60.0)), Some(Zone::Top));
|
||||
assert_eq!(zone(pos2(body.left() + 60.0, body.top() + 90.0)), Some(Zone::Left));
|
||||
// Without a middle, the relatively nearest side takes every point.
|
||||
let edges = |p| body_zone(rect, 24.0, fraction, false, p);
|
||||
assert_eq!(edges(body.center() - vec2(10.0, 0.0)), Some(Zone::Left));
|
||||
assert_eq!(edges(body.center() - vec2(0.0, 10.0)), Some(Zone::Top));
|
||||
assert_eq!(edges(body.center() + vec2(0.0, 10.0)), Some(Zone::Bottom));
|
||||
// No fraction: only the middle. Outside or invalid: nothing.
|
||||
assert_eq!(body_zone(rect, 24.0, 0.0, true, pos2(body.left() + 1.0, y)), Some(Zone::Center));
|
||||
assert!(body_zone(rect, 24.0, fraction, true, pos2(0.0, 0.0)).is_none());
|
||||
assert!(body_zone(Rect::NOTHING, 24.0, fraction, true, pos2(0.0, 0.0)).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_drop_beside_a_group_previews_a_line_along_its_edge_and_a_group_without_tabs_has_no_middle() {
|
||||
let style = DockStyle::from_visuals(&egui::Visuals::dark());
|
||||
let rule = style.zone_rule();
|
||||
assert_eq!(rule, ZoneRule::Fraction { fraction: 1.0 / 3.0, line: 4.0 });
|
||||
assert_eq!(DockStyle { side_edge_zones: true, ..style }.zone_rule(), ZoneRule::Depth(SIDE_EDGE_ZONE_DEPTH));
|
||||
let rect = Rect::from_min_size(pos2(10.0, 20.0), vec2(300.0, 224.0));
|
||||
let group = target(2, &[2, 3], rect);
|
||||
let resolve = |at, group: &DropTarget<u32>| resolve_drop_by(&5, at, std::slice::from_ref(group), rule);
|
||||
let left = resolve(pos2(20.0, 140.0), &group).unwrap();
|
||||
assert_eq!(left.placement, Placement::Split(Zone::Left));
|
||||
assert_eq!(left.preview, Rect::from_x_y_ranges(10.0..=14.0, rect.y_range()), "the group's whole left edge");
|
||||
let bottom = resolve(pos2(160.0, 240.0), &group).unwrap();
|
||||
assert_eq!(bottom.placement, Placement::Split(Zone::Bottom));
|
||||
assert_eq!(bottom.preview, Rect::from_x_y_ranges(rect.x_range(), 240.0..=244.0));
|
||||
let middle = resolve(pos2(160.0, 144.0), &group).unwrap();
|
||||
assert_eq!((middle.placement, middle.preview), (Placement::Split(Zone::Center), rect));
|
||||
// A canvas: its middle goes to the nearest edge, and its strip takes nothing.
|
||||
let canvas = DropTarget { accepts_tabs: false, ..group };
|
||||
assert_eq!(resolve(pos2(150.0, 144.0), &canvas).unwrap().placement, Placement::Split(Zone::Left));
|
||||
assert!(resolve(pos2(90.0, 30.0), &canvas).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_style_needs_a_drop_line_and_side_zones_of_at_most_half_the_body() {
|
||||
let style = DockStyle::from_visuals(&egui::Visuals::dark());
|
||||
assert!(style.valid());
|
||||
assert!(DockStyle { side_zone_fraction: 0.0, ..style.clone() }.valid());
|
||||
assert!(DockStyle { side_zone_fraction: 0.5, ..style.clone() }.valid());
|
||||
assert!(!DockStyle { side_zone_fraction: 0.6, ..style.clone() }.valid());
|
||||
assert!(!DockStyle { drop_line_width: 0.0, ..style }.valid());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn long_action_sequence_never_loses_or_duplicates_a_panel() {
|
||||
for (start, ids) in [(sample(), vec![1, 2, 3, 4, 5, 6]), (super::model_tests::docked(), super::model_tests::ALL.to_vec())] {
|
||||
|
||||
+56
-21
@@ -85,8 +85,7 @@ layout.apply(Action::Move {
|
||||
that side or a new group above or below in a dock. A floating window takes splits as the
|
||||
docked layout does; `Placement::Group` on a floating anchor splits its group above or below.
|
||||
- A side split zone puts what moves in a new half-size split of the anchor's frame, and no
|
||||
other frame changes size. The new split has its own divider, and the drop preview shows the
|
||||
half the new frame takes.
|
||||
other frame changes size. The new split has its own divider.
|
||||
- Other actions select, minimize, collapse to icons, resize splits (`ResizeSplit` in the docked
|
||||
layout, `ResizeWindowSplit` in the floating window holding a panel), columns and groups, and
|
||||
move a floating window to its own OS window (`SetNative`, and `SetMonitor` for the monitor it
|
||||
@@ -169,8 +168,8 @@ workspace reset hides, closes or moves its panel, and then gives it up. A divide
|
||||
window's resize grip holds focus only while the pointer drags it, so the app's shortcuts and
|
||||
canvas arrow keys work right after; one reached with Tab or F6 keeps its focus, and arrow keys
|
||||
resize it. Hovering focuses nothing. `begin_drag` hands a drag that started in the app's own chrome to the area.
|
||||
`docking::chrome` has the floating frame, the title bar and `drop_preview` (the drop preview
|
||||
docking and `craft_ui::panes` both draw) for apps that draw their own windows.
|
||||
`docking::chrome` has the floating frame, the title bar and `drop_preview` (the drag ghost
|
||||
and region preview docking and `craft_ui::panes` both draw) for apps that draw their own windows.
|
||||
|
||||
## Tab strips
|
||||
|
||||
@@ -227,16 +226,44 @@ A flyout fits its content the same way, without the title bar.
|
||||
4 pt band above the title bar (`chrome::RESIZE_BAND`), so they take no moves and no clicks on
|
||||
the title bar's buttons. A resize keeps the opposite edges in place and stays within the size
|
||||
limits and the screen. A focused resize handle moves its edge 8 pt per arrow key.
|
||||
- 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 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
|
||||
side zones apply.
|
||||
- Over a group (a frame, a dock column's group or a floating group), the pointer's place
|
||||
decides:
|
||||
- The tab strip joins the group's tabs at the insertion slot under the pointer.
|
||||
- In the body below the strip, the outer `DockStyle::side_zone_fraction` (one third) of its
|
||||
width from the left or right edge, or of its height from the top or bottom edge, docks
|
||||
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.
|
||||
- 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
|
||||
frames beyond it can give the room, as they do for a dragged divider. When they cannot, that
|
||||
side joins the frame's tabs instead, or takes nothing when the frame takes no tabs. The gap
|
||||
between two frames is no target.
|
||||
- Bands along borders come before the group zones. Each is centred on its border where the
|
||||
pointer can reach both sides:
|
||||
|
||||
| Band | Drop | Field (default) |
|
||||
|---|---|---|
|
||||
| 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) |
|
||||
|
||||
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.
|
||||
- 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
|
||||
that is less, so every frame keeps a center. A dock column's groups take the same depth.
|
||||
`drop_zone` and `resolve_drop` give these zones to hosts that draw their own frames.
|
||||
- A host that resolves drops over its own groups calls `resolve_drop_by` with
|
||||
`DockStyle::zone_rule()`, so its zones and previews match the dock area's in either profile.
|
||||
`resolve_drop` always gives the split-tree zones.
|
||||
- A dragged divider grows the frames that touch it on one side, up to their maximums. On the
|
||||
other side the frame touching it shrinks to its minimum, then the next frame along the same
|
||||
axis, and so on: the divider stops when every frame on that side is at its minimum. Frames
|
||||
@@ -261,13 +288,21 @@ A flyout fits its content the same way, without the title bar.
|
||||
way. Towards the minimums, groups give way in proportion to their room. A group takes the
|
||||
largest default, compact and fill height of its panels. All three are body heights, without
|
||||
the tab strip, like `PanelLimits`.
|
||||
- Drop previews show the destination: an outline for tabs, a line for a new group, column or dock,
|
||||
a filled half for a split. With `side_edge_zones`, a drag over a group instead outlines each of
|
||||
its zones that a drop would change something in (the side trapezoids, the center rectangle and
|
||||
the tab strip; `zone_shape` gives the outlines) and fills the zone under the pointer. Sides the
|
||||
group does not take, by its permissions or because a split leaves no room, are not drawn; nor is
|
||||
the center of the group a tab comes from. Escape or losing window focus cancels
|
||||
a drag.
|
||||
- Drop previews have two shapes, the same for tab and group drags:
|
||||
- A drop beside something shows a `DockStyle::drop_line_width` (4 pt) accent line along the
|
||||
whole edge it docks to: a group's edge for a split or a new group above or below it, the
|
||||
column's width for a seam, the dock's height for a new column or dock, and the layout's
|
||||
whole edge for the outer edge band. The line's length shows what the drop docks beside.
|
||||
- A drop that joins a group shows a 2 pt accent outline of the whole group, with a light fill,
|
||||
and on its strip the 2 pt insertion marker.
|
||||
|
||||
A group drag also shows its ghost at the pointer. With `side_edge_zones`, a drag over a group
|
||||
instead outlines each of its zones that a drop would change something in (the side
|
||||
trapezoids, the center rectangle and the tab strip; `zone_shape` gives the outlines) and
|
||||
fills the zone under the pointer. Sides the group does not take, by its permissions or
|
||||
because a split leaves no room, are not drawn; nor is the center of the group a tab comes
|
||||
from. A drop there that the zones do not show, such as the outer edge band, fills the region
|
||||
it takes. Escape or losing window focus cancels a drag.
|
||||
- A drag in progress (a tab, group, column, window bar, native window bar or divider, in any of
|
||||
the app's windows) also ends when the layout passed to the area is not the one it drew last
|
||||
frame, with or without its returned actions applied: the app switched workspaces, ran Reset
|
||||
|
||||
Reference in new issue
Block a user