mirror of
https://github.com/storytold/craft-libs.git
synced 2026-10-31 15:49:54 +00:00
Merge latest docking preview changes into egui 0.37 migration
This commit is contained in:
commit
f71bccb65e
9 files changed
+446
-140
No files matched your search
@@ -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]]]);
|
||||
|
||||
@@ -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 } }]);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in new issue
Block a user