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:
aiongg authored and GitHub committed 2026-10-24 23:51:43 -04:00
1 parent 865c9af7fd
commit 8a503cfe79
18 files changed
+783 -124

No files matched your search

+3 -1
View File
@@ -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);
+102
View File
@@ -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.
+197
View File
@@ -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);
}
+12
View File
@@ -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();
+2 -2
View File
@@ -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);
+6 -1
View File
@@ -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;
+5 -5
View File
@@ -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]
+3
View File
@@ -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);
+107 -31
View File
@@ -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);
+13 -2
View File
@@ -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())),
+2 -1
View File
@@ -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 });
}
}
+2
View File
@@ -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>,
+55 -19
View File
@@ -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()));
+3 -2
View File
@@ -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]
+47 -14
View File
@@ -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,
+102 -23
View File
@@ -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))
}
+66 -2
View File
@@ -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
View File
@@ -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