Merge latest docking preview changes into egui 0.37 migration

This commit is contained in:
skylar taylor-barrick committed 2026-10-25 17:24:24 +01:00
commit f71bccb65e
9 files changed
+446 -140

No files matched your search

+95 -18
View File
@@ -186,6 +186,35 @@ impl Driver {
}
}
/// Whether the last frame painted the result preview of `rect`: a 2 pt outline with a light
/// fill, inset 1 pt.
fn previewed(&self, rect: Rect) -> bool {
let rect = rect.shrink(1.0);
let shapes = || {
self.shapes.iter().filter_map(|c| match &c.shape {
egui::Shape::Rect(r) if r.rect == rect => Some(r),
_ => None,
})
};
shapes()
.filter(|r| r.fill == egui::Color32::TRANSPARENT && r.stroke.width == 2.0)
.any(|outline| shapes().any(|r| r.stroke.width == 0.0 && r.fill == outline.stroke.color.gamma_multiply(0.08)))
}
/// The rect of the group whose strip starts with `panel`'s tab, in the column whose bar
/// names `column`: down to the next group's tab, `next`, or the window's bottom.
fn group_rect(&self, panel: u32, next: Option<u32>, column: u32) -> Rect {
let tab = self.tab(panel);
let bottom = next.map_or(600.0, |next| self.tab(next).top());
Rect::from_x_y_ranges(self.column_of(column).x_range(), tab.top()..=bottom)
}
/// The rect of the dock column whose bar names `panel`.
fn column_of(&self, panel: u32) -> Rect {
let bar = self.rect("column-bar", panel).union(self.rect("column-collapse", panel));
Rect::from_x_y_ranges(bar.x_range(), bar.top()..=600.0)
}
/// Whether the last frame painted the filled insertion line `line`.
fn painted(&self, line: Rect) -> bool {
self.shapes.iter().any(|clipped| match &clipped.shape {
@@ -214,10 +243,6 @@ fn hline(y: f32, x: std::ops::RangeInclusive<f32>) -> Rect {
Rect::from_x_y_ranges(x, (y - 2.0)..=(y + 2.0))
}
fn vline(x: f32, y: std::ops::RangeInclusive<f32>) -> Rect {
Rect::from_x_y_ranges((x - 2.0)..=(x + 2.0), y)
}
/// A floating group's drag bar.
fn bar(d: &Driver, panel: u32) -> Pos2 {
d.rect("drag-bar", panel).center()
@@ -227,14 +252,18 @@ fn bar(d: &Driver, panel: u32) -> Pos2 {
// groups share 14..600, meeting at y = 307.
#[test]
fn group_dropped_on_a_seam_inserts_between_and_shows_a_horizontal_line() {
fn group_dropped_on_a_seam_inserts_between_and_previews_the_rect_it_takes() {
let layout = with_floating(docked(Side::Start, &[&[&[2], &[3]]]), &[(5, [400.0, 300.0, 150.0, 150.0])]);
let mut d = Driver::new(layout);
assert_eq!(d.tab(3).top(), 307.0);
let seam = pos2(120.0, 309.0);
d.hold(bar(&d, 5), seam);
assert!(d.painted(hline(307.0, 0.0..=240.0)), "a horizontal line across the column at the seam");
let held = d.shapes.clone();
let out = d.step(vec![button(seam, false)]);
d.settle();
let rect = d.group_rect(5, Some(3), 2);
d.shapes = held;
assert!(d.previewed(rect), "group 5's rect between groups 2 and 3: {rect:?}");
assert_eq!(moved(&out.actions), Some((5, &Target::At { anchor: 2, placement: Placement::Group { below: true, height: None } })));
assert_eq!(columns(&d.layout, Side::Start), vec![vec![vec![2], vec![5], vec![3]]]);
assert!(d.layout.floating.is_empty());
@@ -269,22 +298,29 @@ 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))),
// Group 3's body is 0..240 across and 333..600 down. Each drop beside previews the rect
// group 5 then takes: in the column, below the group `Some` names, or a whole column.
for (at, expected, result) in [
(pos2(120.0, 360.0), vec![vec![vec![2], vec![5], vec![3]]], Some(Some(3))),
(pos2(120.0, 580.0), vec![vec![vec![2], vec![3], vec![5]]], Some(None)),
(pos2(30.0, 466.0), vec![vec![vec![5]], vec![vec![2], vec![3]]], None),
(pos2(200.0, 466.0), vec![vec![vec![2], vec![3]], vec![vec![5]]], None),
(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");
}
let held = d.shapes.clone();
d.step(vec![button(at, false)]);
assert_eq!(columns(&d.layout, Side::Start), expected, "{at:?}");
d.settle();
let rect = match result {
Some(next) => d.group_rect(5, next, 2),
None if expected.len() == 2 => d.column_of(5),
None => continue,
};
d.shapes = held;
assert!(d.previewed(rect), "{at:?}: the rect group 5 takes, {rect:?}");
}
}
@@ -321,6 +357,37 @@ fn a_group_edge_scrolled_out_of_view_previews_its_line_at_the_views_edge() {
assert_eq!(columns(&d.layout, Side::Start), vec![vec![vec![2], vec![3], vec![5], vec![4]]]);
}
#[test]
fn in_a_scrolled_column_a_stack_previews_the_rect_at_the_scroll_offset() {
// Groups 306 pt high from y = 14: group 3 spans 320..626 before scrolling.
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 over = d.tab(2).center();
d.step(vec![
Event::PointerMoved(over),
Event::MouseWheel {
unit: egui::MouseWheelUnit::Point,
delta: vec2(0.0, -200.0),
phase: egui::TouchPhase::Move,
modifiers: Modifiers::NONE,
source: egui::MouseWheelSource::Unknown,
},
]);
d.wait();
let group = d.tab(3);
assert!(group.top() < 250.0, "scrolled up: {group:?}");
// Group 3's top third stacks 5 above it, where 3 is now.
let at = pos2(120.0, group.bottom() + 40.0);
d.hold(bar(&d, 5), at);
let held = d.shapes.clone();
d.step(vec![button(at, false)]);
assert_eq!(columns(&d.layout, Side::Start), vec![vec![vec![2], vec![5], vec![3], vec![4]]]);
d.settle();
let rect = d.group_rect(5, Some(3), 2);
d.shapes = held;
assert!(d.previewed(rect), "the rect group 5 takes, {rect:?}");
}
#[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
@@ -376,8 +443,14 @@ fn group_dropped_on_a_column_edge_makes_a_new_column() {
let mut d = Driver::new(layout.clone());
let edge = pos2(246.0, 200.0);
d.hold(bar(&d, 5), edge);
assert!(d.painted(vline(240.0, 0.0..=600.0)), "a vertical line the full height of the dock");
let held = d.shapes.clone();
let out = d.step(vec![button(edge, false)]);
d.settle();
let rect = d.column_of(5);
assert_eq!((rect.left(), rect.top(), rect.bottom()), (240.0, 0.0, 600.0));
let shapes = std::mem::replace(&mut d.shapes, held);
assert!(d.previewed(rect), "the new column's rect, the full height of the dock");
d.shapes = shapes;
assert_eq!(moved(&out.actions), Some((5, &Target::At { anchor: 2, placement: Placement::Column { after: true } })));
assert_eq!(columns(&d.layout, Side::Start), vec![vec![vec![2], vec![3]], vec![vec![5]]]);
assert_eq!(column(&d.layout, 1).width, 150.0, "the new column keeps the window's width");
@@ -394,9 +467,13 @@ fn group_dropped_on_the_window_edge_without_a_dock_makes_a_new_dock() {
let layout = with_floating(docked(side, &[&[&[2]]]), &[(5, [300.0, 300.0, 150.0, 150.0])]);
let mut d = Driver::new(layout);
d.hold(bar(&d, 5), at);
let x = if new == Side::End { 598.0 } else { 2.0 };
assert!(d.painted(vline(x, 0.0..=600.0)), "{new:?}: a vertical line at the window edge");
let held = d.shapes.clone();
let out = d.step(vec![button(at, false)]);
d.settle();
let rect = d.column_of(5);
let shapes = std::mem::replace(&mut d.shapes, held);
assert!(d.previewed(rect), "{new:?}: the new dock's column at the window edge, {rect:?}");
d.shapes = shapes;
assert_eq!(moved(&out.actions), Some((5, &Target::NewDock { side: new })));
assert_eq!(columns(&d.layout, new), vec![vec![vec![5]]]);
assert_eq!(columns(&d.layout, side), vec![vec![vec![2]]]);
+125 -55
View File
@@ -81,7 +81,7 @@ fn accent() -> Color32 {
}
#[test]
fn a_drop_on_a_groups_outer_third_docks_beside_it_with_a_line_and_its_middle_joins_with_an_outline() {
fn a_drop_on_a_groups_outer_third_docks_beside_it_with_its_rect_and_its_middle_joins_with_an_outline() {
let d = driver(&[]);
let (body, rect) = group(&d, 1);
let (w, h) = (body.width(), body.height());
@@ -96,11 +96,9 @@ fn a_drop_on_a_groups_outer_third_docks_beside_it_with_a_line_and_its_middle_joi
] {
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:?}");
let out = previews_result(driver(&[]), tab(&d, 3), at, |d| group(d, 3).1);
assert_eq!(out, 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)] {
@@ -119,11 +117,6 @@ fn a_drop_on_a_groups_outer_third_docks_beside_it_with_a_line_and_its_middle_joi
// 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)));
}
@@ -134,11 +127,8 @@ fn a_group_that_takes_no_tabs_maps_its_whole_body_to_its_edges() {
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:?}");
let out = previews_result(driver(&[(1, canvas)]), tab(&d, 3), at, |d| group(d, 3).1);
assert_eq!(out, vec![Action::Move { panel: 3, anchor: 1, placement: Placement::Split(zone) }], "{zone:?}");
}
// Its strip joins nothing.
let mut d = driver(&[(1, canvas)]);
@@ -174,15 +164,17 @@ fn a_side_without_room_joins_as_a_tab_or_offers_nothing() {
}
#[test]
fn the_root_edge_band_is_28_pt_and_previews_a_line_along_the_layouts_edge() {
fn the_root_edge_band_is_28_pt_and_previews_the_new_frame_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);
let along_the_bottom = |d: &Driver| {
let (_, rect) = group(d, 3);
assert_eq!((rect.left(), rect.right(), rect.bottom()), (0.0, 800.0, 600.0), "along the layout's whole bottom edge");
rect
};
let out = previews_result(driver(&[]), tab(&d, 3), pos2(x, 573.0), along_the_bottom);
assert!(matches!(&out[..], [Action::Move { panel: 3, placement: Placement::Edge(Zone::Bottom), .. }]), "{out:?}");
// Past the band, the group's own bottom third.
let mut d = driver(&[]);
let out = d.drag(tab(&d, 3), pos2(x, 571.0));
@@ -225,11 +217,14 @@ fn between_side_frames_the_outer_edge_band_covers_only_the_canvas_column() {
let (side, _) = group(&d, 1);
let at = pos2(canvas.center().x, 590.0);
let mut d = canvas_driver(Permissions::PROTECTED, |_| {});
d.hold(tab(&d, 3), at);
assert!(d.filled(line(canvas, Zone::Bottom)), "a line along the canvas column's bottom");
assert!(!d.filled(Rect::from_x_y_ranges(0.0..=800.0, 596.0..=600.0)), "no line along the window's bottom");
let out = d.release(at);
assert_eq!(out.actions, vec![Action::Move { panel: 3, anchor: 0, placement: Placement::Edge(Zone::Bottom) }]);
let under_the_canvas = |d: &Driver| {
let (_, rect) = group(d, 3);
assert_eq!(rect.x_range(), canvas.x_range(), "the canvas column's width");
rect
};
let out = previews_result(canvas_driver(Permissions::PROTECTED, |_| {}), tab(&d, 3), at, under_the_canvas);
assert_eq!(out, vec![Action::Move { panel: 3, anchor: 0, placement: Placement::Edge(Zone::Bottom) }]);
d.drag(tab(&d, 3), at);
// The side frames keep their full height.
let column = split(SplitAxis::Vertical, SplitSize::Ratio(0.75), tabs(&[0]), tabs(&[3]));
let sides = split(SplitAxis::Horizontal, SplitSize::FixedSecond(200.0), column, tabs(&[2]));
@@ -253,11 +248,15 @@ fn between_docks_the_outer_edge_band_covers_the_part_between_them() {
let (timeline, _) = group(&d, 4);
let area = Rect::from_x_y_ranges(canvas.x_range(), (canvas.top() - 26.0)..=timeline.bottom());
let at = pos2(canvas.center().x, 590.0);
let between_the_docks = |d: &Driver| {
let (_, rect) = group(d, 3);
assert_eq!((rect.x_range(), rect.bottom()), (area.x_range(), area.bottom()), "along the bottom between the docks");
rect
};
let out = previews_result(driver(), tab(&d, 3), at, between_the_docks);
assert!(matches!(&out[..], [Action::Move { panel: 3, placement: Placement::Edge(Zone::Bottom), .. }]), "{out:?}");
let mut d = driver();
d.hold(tab(&d, 3), at);
assert!(d.filled(line(area, Zone::Bottom)), "a line along the bottom between the docks");
let out = d.release(at);
assert!(matches!(&out.actions[..], [Action::Move { panel: 3, placement: Placement::Edge(Zone::Bottom), .. }]), "{:?}", out.actions);
d.drag(tab(&d, 3), at);
let Some(Node::Split { second, .. }) = &d.layout.root else { panic!("{:?}", d.layout) };
let Node::Split { first, .. } = &**second else { panic!("{:?}", d.layout) };
assert_eq!(**first, split(SplitAxis::Vertical, SplitSize::Ratio(0.75), middle, tabs(&[3])));
@@ -283,11 +282,13 @@ fn over_a_full_width_row_the_top_edge_band_covers_only_the_canvas_column_above_i
let at = pos2(canvas.center().x, 5.0);
let mut d = driver();
d.hold(tab(&d, 3), at);
assert!(d.filled(Rect::from_x_y_ranges(canvas.x_range(), 0.0..=4.0)), "a line along the canvas column's top");
assert!(!d.filled(Rect::from_x_y_ranges(0.0..=800.0, 0.0..=4.0)), "no line along the window's top");
let preview = result_preview(&d).unwrap_or_else(|| panic!("no result preview"));
assert_eq!(preview.x_range(), canvas.shrink(1.0).x_range(), "the canvas column's width, not the window's");
let out = d.release(at);
d.settle();
assert_eq!(out.actions, vec![Action::Move { panel: 3, anchor: 0, placement: Placement::Edge(Zone::Top) }]);
assert_eq!(wrapped(&d.layout, 3), Some(tabs(&[0])), "{:?}", d.layout);
assert_eq!(preview, group(&d, 3).1.shrink(1.0));
}
#[test]
@@ -303,7 +304,8 @@ fn an_outer_edge_drop_wraps_the_area_it_previews_when_the_app_applies_other_perm
let at = pos2(canvas.center().x, 5.0);
let mut d = driver();
d.hold(tab(&d, 3), at);
assert!(d.filled(Rect::from_x_y_ranges(0.0..=800.0, 0.0..=4.0)), "a line along the window's top");
let preview = result_preview(&d).unwrap_or_else(|| panic!("no result preview"));
assert_eq!(preview.x_range(), Rect::from_x_y_ranges(0.0..=800.0, 0.0..=1.0).shrink2(egui::vec2(1.0, 0.0)).x_range(), "the window's width");
let out = d.release(at);
let mut applied = layout.clone();
let apply = |p: &u32| if *p == 0 { Permissions::PROTECTED } else { Permissions::default() };
@@ -390,16 +392,15 @@ fn a_drop_that_changes_only_sizes_shows_nothing_and_does_nothing() {
let layout = Layout { root: Some(ratio_split(SplitAxis::Horizontal, 0.6, tabs(&[1]), column)), floating: Vec::new() };
let start = || DockDriver::new(layout.clone(), Bodies::default());
let d = start();
let (body, rect) = group(&d, 3);
let (body, _) = group(&d, 3);
let top = pos2(body.center().x, body.top() + body.height() * 0.2);
// Properties' bottom third moves Layers, with a line.
let mut d = start();
d.hold(tab(&d, 2), pos2(body.center().x, body.bottom() - body.height() * 0.2));
assert!(previews(&d) && d.filled(line(rect, Zone::Bottom)));
// Properties' bottom third moves Layers, with the rect it takes.
let below = pos2(body.center().x, body.bottom() - body.height() * 0.2);
previews_result(start(), tab(&d, 2), below, |d| group(d, 2).1);
// Its top third would put Layers back where it is.
let mut d = start();
d.hold(tab(&d, 2), top);
assert!(!previews(&d), "no line, outline or fill");
assert!(!previews(&d), "no outline or fill");
let out = d.release(top);
assert!(out.actions.is_empty(), "{:?}", out.actions);
assert_eq!(d.layout, layout);
@@ -426,7 +427,7 @@ fn a_dock_drop_that_changes_only_heights_shows_nothing_and_does_nothing() {
let d = start();
let (body, rect) = group(&d, 3);
let (upper, _) = group(&d, 2);
// Below the lower group moves the upper one, with a line.
// Below the lower group moves the upper one, with the rect it takes.
let mut d = start();
d.hold(tab(&d, 2), pos2(body.center().x, body.bottom() - body.height() * 0.2));
assert!(previews(&d));
@@ -434,7 +435,7 @@ fn a_dock_drop_that_changes_only_heights_shows_nothing_and_does_nothing() {
for at in [pos2(body.center().x, body.top() + body.height() * 0.2), pos2(body.center().x, (upper.bottom() + rect.top()) * 0.5)] {
let mut d = start();
d.hold(tab(&d, 2), at);
assert!(!previews(&d), "{at:?}: no line");
assert!(!previews(&d), "{at:?}: no preview");
let out = d.release(at);
assert!(out.actions.is_empty(), "{at:?}: {:?}", out.actions);
assert_eq!(d.layout, layout);
@@ -452,12 +453,14 @@ fn a_header_drag_of_a_windows_only_group_looks_through_its_window() {
let window = Rect::from_min_size(pos2(40.0, 150.0), vec2(300.0, 260.0));
let at = pos2(body.left() + body.width() * 0.25, body.center().y);
assert!(window.contains(at));
let mut d = start();
let bar = d.hit(&Hit::DragBar(4)).unwrap().center();
d.hold(bar, at);
assert!(d.filled(line(rect, Zone::Left)), "frame 1's left third, behind the window");
let out = d.release(at);
assert_eq!(out.actions, vec![Action::MoveGroup { panel: 4, to: Target::At { anchor: 1, placement: Placement::Split(Zone::Left) } }]);
// Frame 1's left third, behind the window.
let out = previews_result(start(), bar, at, |d| {
let (_, group) = group(d, 4);
assert_eq!(group.left(), rect.left());
group
});
assert_eq!(out, vec![Action::MoveGroup { panel: 4, to: Target::At { anchor: 1, placement: Placement::Split(Zone::Left) } }]);
}
#[test]
@@ -489,18 +492,85 @@ fn a_drop_beside_a_frame_too_narrow_to_halve_takes_room_from_its_neighbour() {
let root =
Node::Split { axis: SplitAxis::Horizontal, size: SplitSize::FixedSecond(60.0), first: Box::new(tabs(&[1])), second: Box::new(tabs(&[2])) };
let layout = Layout { root: Some(root), floating: vec![Floating::single(vec![4], 0, [100.0, 100.0, 200.0, 200.0])] };
let mut d = DockDriver::new(layout, Bodies::default());
let start = || DockDriver::new(layout.clone(), Bodies::default());
let d = start();
let (body, rect) = group(&d, 2);
let at = pos2(body.left() + 8.0, body.center().y);
d.hold(tab(&d, 4), at);
assert!(d.filled(line(rect, Zone::Left)), "a line along frame 2's left edge");
let out = d.release(at);
assert!(
matches!(&out.actions[..], [Action::Move { panel: 4, anchor: 2, placement: Placement::SplitSized { zone: Zone::Left, .. } }]),
"{:?}",
out.actions
);
// The preview is the rect the group takes left of frame 2.
let out = previews_result(start(), tab(&d, 4), at, |d| group(d, 4).1);
assert!(matches!(&out[..], [Action::Move { panel: 4, anchor: 2, placement: Placement::SplitSized { zone: Zone::Left, .. } }]), "{out:?}");
let mut d = start();
d.drag(tab(&d, 4), at);
d.settle();
assert!((group(&d, 2).1.width() - rect.width()).abs() < 1.0, "frame 2 keeps its width");
assert!(group(&d, 4).1.width() >= 40.0);
}
/// The rect of the result preview the last frame painted: a 2 pt accent outline with a light
/// fill.
fn result_preview(d: &Driver) -> Option<Rect> {
let fill = accent().gamma_multiply(0.08);
d.shapes()
.iter()
.find_map(|clipped| match &clipped.shape {
egui::Shape::Rect(shape) if shape.fill == fill && shape.stroke.width == 0.0 => Some(shape.rect),
_ => None,
})
.filter(|rect| d.outlined(*rect))
}
/// Drags `from` to `at` and back to rest: the preview while held is the rect `rect_of` reads
/// once the drop is applied, inset by its 1 pt margin. Returns the drop's actions.
fn previews_result(mut d: Driver, from: Pos2, at: Pos2, rect_of: impl Fn(&Driver) -> Rect) -> Vec<Action<u32>> {
d.hold(from, at);
let preview = result_preview(&d);
let out = d.release(at);
d.settle();
assert_eq!(preview, Some(rect_of(&d).shrink(1.0)), "{:?}", out.actions);
out.actions
}
#[test]
fn a_drop_beside_a_frame_previews_the_rect_the_group_takes() {
let d = driver(&[]);
let (body, _) = group(&d, 1);
let at = pos2(body.left() + body.width() * 0.2, body.center().y);
let out = previews_result(driver(&[]), tab(&d, 3), at, |d| group(d, 3).1);
assert_eq!(out, vec![Action::Move { panel: 3, anchor: 1, placement: Placement::Split(Zone::Left) }]);
// A whole group the same.
let bar = d.hit(&Hit::DragBar(2)).unwrap().center();
// Panel 3 is the group's selected tab, the one it draws.
let out = previews_result(driver(&[]), bar, at, |d| group(d, 3).1);
assert_eq!(out, vec![Action::MoveGroup { panel: 2, to: Target::At { anchor: 1, placement: Placement::Split(Zone::Left) } }]);
}
/// A 240 pt dock column at the start, group `[2]` above `[3, 4]`, beside frame 1.
fn dock_driver() -> Driver {
let column = Column::new(vec![Group::new(vec![2], 0), Group::new(vec![3, 4], 0)], 240.0);
let dock = Node::Dock { side: Side::Start, columns: vec![column] };
let root = Node::Split { axis: SplitAxis::Horizontal, size: SplitSize::FixedFirst(240.0), first: Box::new(dock), second: Box::new(tabs(&[1])) };
DockDriver::new(Layout { root: Some(root), floating: Vec::new() }, Bodies::default())
}
/// The rect of the dock column of `panel`'s group, its bar included, when that group fills it.
fn column_of(d: &Driver, panel: u32) -> Rect {
let bar = d.hit(&Hit::ColumnBar(panel)).unwrap_or_else(|| panic!("column bar {panel}"));
let (body, _) = group(d, panel);
Rect::from_x_y_ranges(body.x_range(), bar.top()..=body.bottom())
}
#[test]
fn a_dock_drop_previews_the_rect_the_group_or_column_takes() {
let d = dock_driver();
let (upper, _) = group(&d, 2);
// Above group 2: a new group at the column's top.
let at = pos2(upper.center().x, upper.top() + 10.0);
let out = previews_result(dock_driver(), tab(&d, 4), at, |d| group(d, 4).1);
assert_eq!(out, vec![Action::Move { panel: 4, anchor: 2, placement: Placement::Group { below: false, height: None } }]);
// The column's inner edge: a new column after it, the full height.
let out = previews_result(dock_driver(), tab(&d, 4), pos2(upper.right() + 6.0, 200.0), |d| column_of(d, 4));
assert_eq!(out, vec![Action::Move { panel: 4, anchor: 2, placement: Placement::Column { after: true } }]);
// The window's right edge: a new dock.
let out = previews_result(dock_driver(), tab(&d, 4), pos2(795.0, 200.0), |d| column_of(d, 4));
assert_eq!(out, vec![Action::MovePanel { panel: 4, to: Target::NewDock { side: Side::End } }]);
}
+83 -29
View File
@@ -97,6 +97,29 @@ pub(in crate::docking) fn column_width<P>(column: &Column<P>, style: &DockStyle)
if column.iconic { column.iconic_width.unwrap_or(style.iconic_width).clamp(style.iconic_width, style.iconic_max_width) } else { column.width }
}
/// [D] A dock's columns as drawn in its rect: each column's width bounds, natural width with
/// the floor it shrinks to, and the width [`fit_widths`] gives it, and each column with groups
/// as `(index, width, rect)`, from the window edge inwards.
pub(in crate::docking) struct DockColumns {
bounds: Vec<(f32, f32)>,
natural: Vec<(f32, Option<f32>)>,
widths: Vec<f32>,
pub(in crate::docking) rects: Vec<(usize, f32, Rect)>,
}
/// The rects of groups `heights` high, stacked from the top of `area`.
pub(in crate::docking) fn stacked(area: Rect, heights: &[f32]) -> Vec<Rect> {
let mut top = area.top();
heights
.iter()
.map(|height| {
let rect = Rect::from_min_max(pos2(area.left(), top), pos2(area.right(), top + height));
top += height;
rect
})
.collect()
}
/// A group of a column, as [`group_heights`] sizes it. Heights include the tab strip.
#[derive(Clone, Copy, Debug, Default)]
pub(in crate::docking) struct GroupSize {
@@ -270,21 +293,17 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
if usable(rect) {
ui.painter().rect_filled(rect, 0.0, self.style.background);
}
let bounds: Vec<(f32, f32)> =
columns.iter().map(|c| if c.iconic { (self.style.iconic_width, self.style.iconic_max_width) } else { self.column_bounds(c) }).collect();
let natural: Vec<(f32, Option<f32>)> =
columns.iter().zip(&bounds).map(|(c, (lo, _))| (column_width(c, self.style), (!c.iconic).then_some(lo.max(1.0)))).collect();
let widths = fit_widths(&natural, rect.width());
let DockColumns { bounds, natural, widths, rects } = self.dock_columns(side, columns, rect);
let scaled = widths.iter().zip(&natural).any(|(w, (n, _))| w != n);
let total: f32 = widths.iter().sum();
let mut x = outer;
let mut drawn = Vec::new();
for (index, column) in columns.iter().enumerate() {
for (index, width, column_rect) in rects {
let Some(column) = columns.get(index) else { continue };
let Some(first) = column.groups.first().and_then(|g| g.panels.first()) else { continue };
let fitted = widths.get(index).copied().unwrap_or_default();
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 (x, inner) = match edge {
Edge::Left => (column_rect.left(), column_rect.right()),
Edge::Right => (column_rect.right(), column_rect.left()),
};
let groups = self.dock_groups.len();
self.column(ui, column, first, column_rect, edge);
let (outer_band, outer_line) = column_edge(x, rect.y_range(), band_width, line_width);
@@ -304,7 +323,6 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
}
self.dock_targets.push(DockDrop { target: after.0, band: inner_band, line: after.1 });
drawn.push((index, column, first, width, inner));
x = inner;
}
// Registered after the columns, so the narrow hit areas win at the edges.
for (index, column, first, width, inner) in drawn {
@@ -348,6 +366,49 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
}
}
/// The columns of a dock drawn in `rect` ([`DockColumns`]).
pub(in crate::docking) fn dock_columns(&self, side: Side, columns: &[Column<P>], rect: Rect) -> DockColumns {
let (outer, inward) = match edge_of(side, self.style.right_to_left) {
Edge::Left => (rect.left(), 1.0),
Edge::Right => (rect.right(), -1.0),
};
let bounds: Vec<(f32, f32)> =
columns.iter().map(|c| if c.iconic { (self.style.iconic_width, self.style.iconic_max_width) } else { self.column_bounds(c) }).collect();
let natural: Vec<(f32, Option<f32>)> =
columns.iter().zip(&bounds).map(|(c, (lo, _))| (column_width(c, self.style), (!c.iconic).then_some(lo.max(1.0)))).collect();
let widths = fit_widths(&natural, rect.width());
let mut x = outer;
let mut rects = Vec::new();
for (index, column) in columns.iter().enumerate() {
if column.groups.first().and_then(|g| g.panels.first()).is_none() {
continue;
}
let fitted = widths.get(index).copied().unwrap_or_default();
let width = fitted.min((rect.width() - (x - outer).abs()).max(0.0));
let inner = x + inward * width;
rects.push((index, width, Rect::from_x_y_ranges(x.min(inner)..=x.max(inner), rect.y_range())));
x = inner;
}
DockColumns { bounds, natural, widths, rects }
}
/// A column drawn at `rect`: its bar, the body below it, and the heights of its groups in
/// the body, top to bottom (none when the column is iconic or its body is empty). Groups
/// taller than the body together scroll.
pub(in crate::docking) fn column_layout(&self, column: &Column<P>, rect: Rect) -> (Rect, Rect, Vec<f32>) {
let bar = Rect::from_min_max(rect.min, pos2(rect.right(), (rect.top() + self.style.column_bar_height).min(rect.bottom())));
let body = Rect::from_min_max(pos2(rect.left(), bar.bottom()), rect.max);
if column.iconic || !usable(body) {
return (bar, body, Vec::new());
}
let sizes: Vec<_> = column
.groups
.iter()
.map(|g| GroupSize { minimized: g.minimized, saved: g.height, min: self.group_min(g), content: self.dock_heights(g) })
.collect();
(bar, body, group_heights(&sizes, self.style.tab_height, body.height()))
}
fn handle_style(&self) -> ResizeHandleStyle {
ResizeHandleStyle { hovered: self.style.accent, dragged: self.style.accent, focused: self.style.accent }
}
@@ -390,9 +451,8 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
return;
}
self.record_column_float(column.groups.iter().flat_map(|g| g.panels.iter().cloned()).collect(), rect);
let bar = Rect::from_min_max(rect.min, pos2(rect.right(), (rect.top() + self.style.column_bar_height).min(rect.bottom())));
let (bar, body, heights) = self.column_layout(column, rect);
self.column_bar(ui, column, first, bar, rect, edge);
let body = Rect::from_min_max(pos2(rect.left(), bar.bottom()), rect.max);
if column.iconic && usable(body) {
self.iconic_column(ui, column, body, edge);
}
@@ -400,19 +460,13 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
ui.painter().rect_stroke(rect, 0.0, self.style.border, StrokeKind::Inside);
return;
}
let sizes: Vec<_> = column
.groups
.iter()
.map(|g| GroupSize { minimized: g.minimized, saved: g.height, min: self.group_min(g), content: self.dock_heights(g) })
.collect();
let heights = group_heights(&sizes, self.style.tab_height, body.height());
let total: f32 = heights.iter().sum();
let start = self.targets.len();
if total > body.height() + 0.5 {
let mut child =
ui.new_child(UiBuilder::new().id_salt(("dock-column", first)).max_rect(body).layout(egui::Layout::top_down(egui::Align::Min)));
child.set_clip_rect(body.intersect(ui.clip_rect()));
egui::ScrollArea::vertical()
let scrolled = egui::ScrollArea::vertical()
.id_salt(self.id.with(("column-scroll", first)))
.auto_shrink([false, false])
.scroll_source(egui::scroll_area::ScrollSource::SCROLL_BAR | egui::scroll_area::ScrollSource::MOUSE_WHEEL)
@@ -420,6 +474,8 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
let (content, _) = ui.allocate_exact_size(vec2(ui.available_width(), total), Sense::hover());
self.column_groups(ui, column, &heights, content, body);
});
// Match the drawn content, including rubber-band displacement in egui 0.37.
self.column_scroll.insert(first.clone(), scrolled.state.unclamped_offset().y);
} else {
self.column_groups(ui, column, &heights, body, body);
}
@@ -437,14 +493,11 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
/// Groups top to bottom in `area` (the scrolled content when the column scrolls), their
/// seams and bands clipped to the visible `view`, and the height dividers.
fn column_groups(&mut self, ui: &mut Ui, column: &Column<P>, heights: &[f32], area: Rect, view: Rect) {
let mut top = area.top();
let mut rects = Vec::new();
for (group, height) in column.groups.iter().zip(heights) {
let rect = Rect::from_min_max(pos2(area.left(), top), pos2(area.right(), top + height));
self.group(ui, &group.panels, group.active, group.minimized, rect, GroupKind::Dock);
rects.push(rect);
top += height;
let rects = stacked(area, heights);
for (group, rect) in column.groups.iter().zip(&rects) {
self.group(ui, &group.panels, group.active, group.minimized, *rect, GroupKind::Dock);
}
let top = rects.last().map_or(area.top(), |r| r.bottom());
let xs = view.x_range();
let half = self.style.seam_band * 0.5;
let width = self.style.drop_line_width;
@@ -722,7 +775,8 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
Some((!candidate.same_arrangement(self.layout)).then_some(action))
}
/// The line a drop beside a group, column, dock or the layout's edge previews with.
/// The line of a dock drop that shows no result rect: into an iconic column, or any dock
/// drop with [`DockStyle::side_edge_zones`].
pub(super) fn paint_drop_line(&self, ctx: &Context, line: Rect) {
if usable(line) {
let painter = self.preview_painter(ctx);
+3 -1
View File
@@ -11,6 +11,8 @@ pub(in crate::docking) use crate::docking::chrome::RESIZE_HANDLES;
/// A floating window as drawn: what covers the layout behind it.
pub(in crate::docking) struct WindowHit<P> {
pub(super) window: Rect,
/// Where the window's tree is drawn.
pub(super) body: Rect,
pub(super) members: Vec<P>,
}
@@ -120,7 +122,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
self.tree = outer;
}
}
self.frame.windows.push(WindowHit { window: frame, members: window.panels().cloned().collect() });
self.frame.windows.push(WindowHit { window: frame, body, members: window.panels().cloned().collect() });
}
/// Drag moves the whole window live, double-click minimizes or expands it, × closes every
+2
View File
@@ -104,6 +104,8 @@ pub(super) struct Renderer<'a, P> {
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>>,
/// [D] The scroll offset of each dock column that scrolls this frame, by its first panel.
pub(super) column_scroll: HashMap<P, f32>,
pub(super) owned_interactions: Vec<Id>,
pub(super) output: DockOutput<P>,
pub(super) frame: FrameState<P>,
+104 -22
View File
@@ -20,8 +20,8 @@ fn float_only(ctx: &Context) -> bool {
/// 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.
/// The preview of a drop that joins a group as a tab, or of the rect a drop beside something
/// gives the moved panels: 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);
@@ -421,8 +421,8 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
return;
}
if let Some((action, line)) = self.dock_group_proposal(drag, pointer) {
if action.is_some() {
self.paint_drop_line(ctx, line);
if let Some(action) = action {
self.paint_dock_preview(ctx, &painter, action, &drag.members, line);
if drag.gesture != GroupGesture::Window {
self.paint_float_hint(ctx, pointer);
}
@@ -436,7 +436,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
self.paint_marker(&painter, &proposal);
} else {
let split = matches!(proposal.placement, Placement::Split(zone) | Placement::SplitSized { zone, .. } | Placement::Edge(zone) if zone != Zone::Center);
self.paint_preview(ctx, &painter, &proposal, split);
self.paint_preview(ctx, &painter, &proposal, split, &drag.members, |to| group_action(drag, to));
}
if drag.gesture != GroupGesture::Window {
self.paint_float_hint(ctx, pointer);
@@ -492,8 +492,9 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
self.output.actions.push(action);
self.output.focused = Some(drag.panel);
}
} else if action.is_some() {
self.paint_drop_line(ctx, line);
} else if let Some(action) = action {
let painter = self.preview_painter(ctx);
self.paint_dock_preview(ctx, &painter, action, std::slice::from_ref(&drag.panel), line);
self.paint_float_hint(ctx, pointer);
}
return;
@@ -533,10 +534,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
/// The group target a single tab drag would take at `pointer`.
pub(super) fn tab_proposal(&self, panel: &P, pointer: Pos2) -> Option<DropProposal<P>> {
let action = |to| match to {
Target::At { anchor, placement } => Action::Move { panel: panel.clone(), anchor, placement },
to => Action::MovePanel { panel: panel.clone(), to },
};
let action = |to| tab_action(panel, to);
match self.extra_proposal(std::slice::from_ref(panel), pointer, false) {
Some(proposal) => Some(self.settled(proposal, action)),
None => self.resolve_roomy(panel, pointer, &self.targets, action),
@@ -560,7 +558,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
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 && !zones {
self.paint_preview(ctx, painter, proposal, true);
self.paint_preview(ctx, painter, proposal, true, std::slice::from_ref(&drag.panel), |to| tab_action(&drag.panel, to));
} else {
self.paint_marker(painter, proposal);
}
@@ -569,17 +567,30 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
}
}
/// 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) {
/// The preview of a group target, for tab and group drags of `members`, which `action` moves
/// there: for a drop beside the group or the layout, the rect the members then take
/// ([`Self::result_rect`]), 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,
members: &[P],
action: impl Fn(Target<P>) -> Action<P>,
) {
if crate::docking::targets::usable(proposal.preview) {
let beside = matches!(proposal.placement, Placement::Split(zone) | Placement::SplitSized { 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);
let to = Target::At { anchor: proposal.anchor.clone(), placement: proposal.placement.clone() };
match self.result_rect(action(to), members) {
Some(rect) => tab_outline(painter, rect.shrink(1.0), self.style.accent),
None => self.paint_drop_line(ctx, proposal.preview),
}
} else {
tab_outline(painter, proposal.preview.shrink(1.0), self.style.accent);
}
@@ -587,6 +598,72 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
self.paint_marker(painter, proposal);
}
/// The preview of a dock drop that `action` makes of `members`: the rect they then take
/// ([`Self::result_rect`]). A drop into an iconic column, or any dock drop with
/// [`crate::docking::DockStyle::side_edge_zones`], shows its `line`.
fn paint_dock_preview(&self, ctx: &Context, painter: &egui::Painter, action: Action<P>, members: &[P], line: Rect) {
match (!self.style.side_edge_zones).then_some(action).and_then(|action| self.result_rect(action, members)) {
Some(rect) => tab_outline(painter, rect.shrink(1.0), self.style.accent),
None => self.paint_drop_line(ctx, line),
}
}
/// Where `members` are drawn once `action` is applied, laid out by the code that draws the
/// layout: the union of the rects of the groups that hold them, or the rect of the dock
/// column they make up alone. A floating window's tree is laid out in its body as drawn
/// this frame. In a dock column that scrolls, a group's rect sits at the column's scroll
/// offset this frame and is clipped to the column's body. `None` when the model rejects `action`, or when the members join an iconic column,
/// whose icons show no rect.
pub(super) fn result_rect(&self, action: Action<P>, members: &[P]) -> Option<Rect> {
let mut candidate = self.layout.clone();
candidate.apply_with_permissions(action, |p| self.permissions(p)).ok()?;
let moved = |panels: &[P]| panels.iter().any(|p| members.contains(p));
let sizer = self.sizer();
let mut rects = Vec::new();
for (root, area) in self.drawn_trees(&candidate, members) {
let mut drawn = Vec::new();
sizer.groups(root, area, &mut drawn);
for Drawn { node, rect, .. } in drawn {
let Node::Dock { side, columns } = node else {
if members.iter().any(|p| holds(node, p)) {
rects.push(rect);
}
continue;
};
for (index, _, rect) in self.dock_columns(*side, columns, rect).rects {
let Some(column) = columns.get(index).filter(|c| c.groups.iter().any(|g| moved(&g.panels))) else { continue };
if column.groups.iter().flat_map(|g| &g.panels).all(|p| members.contains(p)) {
rects.push(rect);
continue;
}
if column.iconic {
return None;
}
let (_, body, heights) = self.column_layout(column, rect);
// The column keeps the scroll state of its first panel, as drawn.
let overflow = (heights.iter().sum::<f32>() - body.height()).max(0.0);
let scroll = column.groups.first().and_then(|g| g.panels.first()).and_then(|p| self.column_scroll.get(p));
let area = body.translate(vec2(0.0, -scroll.copied().unwrap_or(0.0).min(overflow)));
let groups = column.groups.iter().zip(super::dock::stacked(area, &heights));
rects.extend(groups.filter(|(g, _)| moved(&g.panels)).map(|(_, rect)| rect.intersect(body)));
}
}
}
rects.into_iter().filter(|r| crate::docking::targets::usable(*r)).reduce(Rect::union)
}
/// [`Self::trees`] of `layout`, where a floating window that holds a panel other than
/// `members` is laid out in the body that window is drawn in this frame.
fn drawn_trees<'l>(&self, layout: &'l Layout<P>, members: &[P]) -> Vec<(&'l Node<P>, Rect)> {
let body = |window: &crate::docking::Floating<P>| {
let stays = |p: &&P| !members.contains(*p);
let hit = self.frame.windows.iter().find(|hit| window.panels().filter(stays).any(|p| hit.members.contains(p)));
hit.map_or_else(|| self.window_body(window), |hit| hit.body)
};
let windows = layout.floating.iter().filter(|w| !w.minimized).map(|w| (&w.root, body(w)));
layout.root.iter().map(|root| (root, self.workspace)).chain(windows).collect()
}
/// 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 {
@@ -605,10 +682,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
};
let own = target.tabs.iter().any(|(p, _)| p == panel);
let at = |at: Pos2| {
let move_to = |to| match to {
Target::At { anchor, placement } => Action::Move { panel: panel.clone(), anchor, placement },
to => Action::MovePanel { panel: panel.clone(), to },
};
let move_to = |to| tab_action(panel, to);
let proposal = self.resolve_roomy(panel, at, std::slice::from_ref(target), move_to)?;
let to = Target::At { anchor: proposal.anchor.clone(), placement: proposal.placement.clone() };
let joins = matches!(proposal.placement, Placement::Tab { .. } | Placement::Split(Zone::Center));
@@ -805,6 +879,14 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
}
}
/// The move a single tab's drag makes.
fn tab_action<P: Clone>(panel: &P, to: Target<P>) -> Action<P> {
match to {
Target::At { anchor, placement } => Action::Move { panel: panel.clone(), anchor, placement },
to => Action::MovePanel { panel: panel.clone(), to },
}
}
/// Whether the drawn group `node` holds `panel`.
fn holds<P: PartialEq>(node: &Node<P>, panel: &P) -> bool {
match node {
+13 -3
View File
@@ -857,16 +857,26 @@ 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 shows a line along the group's left edge instead.
// The dock-and-canvas arrangement shows the rect the panel takes there instead.
let mut d = DockDriver::new(editing(), Film::default());
d.screen = Rect::from_min_size(Pos2::ZERO, SCREEN);
d.area = d.screen;
d.settle();
d.hold(tab(&d, INFO), pos2(timeline.left() + 8.0, timeline.center().y + 13.0));
let at = pos2(timeline.left() + 8.0, timeline.center().y + 13.0);
d.hold(tab(&d, INFO), at);
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(Rect::from_x_y_ranges(timeline.left()..=(timeline.left() + 4.0), timeline.y_range())));
let shapes = d.shapes().to_vec();
d.release(at);
d.settle();
let info = group(&d, INFO);
assert_eq!(info.left(), timeline.left());
let painted = |fill: egui::Color32, stroke: f32| {
shapes.iter().any(|c| matches!(&c.shape, egui::Shape::Rect(r) if r.rect == info.shrink(1.0) && r.fill == fill && r.stroke.width == stroke))
};
let accent = DockStyle::default().accent;
assert!(painted(accent.gamma_multiply(0.08), 0.0) && painted(egui::Color32::TRANSPARENT, 2.0), "Info's rect beside the timeline");
}
#[test]
+7 -3
View File
@@ -1,4 +1,4 @@
use std::{fmt::Debug, hash::Hash};
use std::{collections::HashMap, fmt::Debug, hash::Hash};
use egui::{Color32, Context, FontId, Id, Pos2, Rect, Sense, Stroke, Ui, Vec2};
@@ -75,8 +75,11 @@ pub struct DockStyle {
/// (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).
/// Width of the line that a drop between the icon groups of an iconic column, and every
/// dock drop with [`Self::side_edge_zones`], previews with, and of the line
/// [`crate::docking::ZoneRule::Fraction`] gives hosts as a side's preview (4 pt, more than
/// zero). Other drops beside a group, column, dock or the layout's edge preview the rect the
/// moved panels take. Border bands reach 4 pt past this line into a tab strip.
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`]).
@@ -1198,6 +1201,7 @@ impl<'a, P: Clone + Eq + Hash + Debug + Send + Sync + 'static> DockArea<'a, P> {
targets: Vec::new(),
dock_targets: Vec::new(),
dock_groups: Vec::new(),
column_scroll: HashMap::new(),
owned_interactions: Vec::new(),
output: DockOutput::default(),
frame,
+14 -9
View File
@@ -315,13 +315,18 @@ 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 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.
- Drop previews have three shapes, the same for tab and group drags:
- A drop beside something shows the rect the dragged panel, group or column will take, at its
real position and size, as a 2 pt accent outline with a light fill. The renderer applies
the drop to a copy of the layout and lays it out with the code that draws it, so the rect
is the one the panel's group gets on release: the new frame for a split (including
`Placement::SplitSized`) or the outer edge band, the new group for a stack above or below
a dock group or on a seam, and the whole column, bar included, for a new column or dock.
A drop between the icon groups of an iconic column shows a `DockStyle::drop_line_width`
(4 pt) accent line there instead.
- A drop that joins a group shows the same outline and fill around the whole group, and on
its strip the 2 pt insertion marker.
- A drop that floats shows the floating rect.
A drop that would leave every panel where it is, changing only sizes, shows nothing and
changes nothing: a lone group dropped beside the group it already borders, or a frame's tab
@@ -335,7 +340,7 @@ A flyout fits its content the same way, without the title bar.
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.
it takes, and dock drops show the line along the edge they dock to. 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
@@ -600,7 +605,7 @@ windows from the first frame. `DockArea::native_windows(bool)` overrides the cho
behind it. A window moving by its bar is never its own target. A point in no window is on the
desktop.
- *Targets and previews.* Each window draws its drop targets in its own coordinates, and the
window under the pointer draws the drop preview (zone, dock line, ghost) in its own frame.
window under the pointer draws the drop preview (zone, result rect, ghost) in its own frame.
The source window shows only the "Hold {key} to undock" hint. The release is resolved at the
end of the main window's frame, after every native window has drawn.
- *Drops.* Over a window, a tab or group docks or joins there with the usual targets: dock lines