mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-01 16:24:51 +00:00
docking: the drop preview shows the zone under the pointer
With side_edge_zones, a drop on a group fills the zone the pointer is in (a side's trapezoid, or the center rectangle with the tab strip) and outlines the rect the drop gives what moves. zone_shape gives a zone's outline; DockDriver::filled_polygon checks it. The dock-and-canvas arrangement keeps its filled half and insertion lines.
This commit is contained in:
1 parent
809bf90936
commit
281ea71f5b
6 files changed
+112
-9
No files matched your search
@@ -30,7 +30,7 @@ pub use surface::{
|
||||
DockArea, DockContent, DockEvent, DockOutput, DockStyle, DropFeedback, DropRequest, FloatSpace, HidePanels, PanelLimits, begin_drag, cancel_drag,
|
||||
float_space, is_dragging,
|
||||
};
|
||||
pub use targets::{DropProposal, DropTarget, drop_zone, resolve_drop, tab_insertion_marker, zone_rect};
|
||||
pub use targets::{DropProposal, DropTarget, drop_zone, resolve_drop, tab_insertion_marker, zone_rect, zone_shape};
|
||||
pub use workspace::Workspace;
|
||||
|
||||
use surface::{Drag, GroupDrag, GroupGesture};
|
||||
|
||||
@@ -220,7 +220,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
}
|
||||
if let Some(proposal) = self.group_proposal(drag, pointer).filter(|p| !p.noop) {
|
||||
let split = matches!(proposal.placement, Placement::Split(zone) | Placement::Edge(zone) if zone != Zone::Center);
|
||||
if crate::docking::targets::usable(proposal.preview) {
|
||||
if crate::docking::targets::usable(proposal.preview) && !self.paint_zone(&painter, &proposal, pointer) {
|
||||
drop_preview(&painter, proposal.preview.shrink(1.0), self.style.accent, split);
|
||||
}
|
||||
if let Some(marker) = proposal.insertion {
|
||||
@@ -322,7 +322,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
// Reordering inside the source strip shows only the insertion line.
|
||||
let same_group = proposal.insertion.is_some()
|
||||
&& self.targets.iter().any(|target| target.rect == proposal.preview && target.tabs.iter().any(|(panel, _)| panel == &drag.panel));
|
||||
if !same_group && !proposal.noop && crate::docking::targets::usable(proposal.preview) {
|
||||
if !same_group && !proposal.noop && crate::docking::targets::usable(proposal.preview) && !self.paint_zone(&painter, &proposal, pointer) {
|
||||
drop_preview(&painter, proposal.preview.shrink(1.0), self.style.accent, true);
|
||||
}
|
||||
if let Some(marker) = proposal.insertion {
|
||||
@@ -336,6 +336,29 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
}
|
||||
}
|
||||
|
||||
/// With [`crate::docking::DockStyle::side_edge_zones`], a drop on a group shows the zone
|
||||
/// under the pointer (a side's trapezoid, or the center rectangle with the tab strip)
|
||||
/// filled, and the rect the drop gives what moves outlined. False when it painted nothing.
|
||||
fn paint_zone(&self, painter: &egui::Painter, proposal: &DropProposal<P>, pointer: Pos2) -> bool {
|
||||
let zone = match proposal.placement {
|
||||
Placement::Split(zone) => zone,
|
||||
Placement::Tab { .. } => Zone::Center,
|
||||
_ => return false,
|
||||
};
|
||||
let target = self.targets.iter().rev().find(|t| crate::docking::targets::usable(t.rect) && t.rect.contains(pointer));
|
||||
let (Some(target), true) = (target, self.style.side_edge_zones) else { return false };
|
||||
let Some(points) = crate::docking::targets::zone_shape(target.rect, zone) else { return false };
|
||||
let fill = self.style.accent.gamma_multiply(0.25);
|
||||
painter.add(egui::Shape::convex_polygon(points, fill, egui::Stroke::NONE));
|
||||
if zone == Zone::Center
|
||||
&& let Some(strip) = target.tab_strip.map(|strip| strip.intersect(target.rect)).filter(|r| crate::docking::targets::usable(*r))
|
||||
{
|
||||
painter.rect_filled(strip, 0.0, fill);
|
||||
}
|
||||
drop_preview(painter, proposal.preview.shrink(1.0), self.style.accent, false);
|
||||
true
|
||||
}
|
||||
|
||||
/// The single drop preview layer, above every floating window.
|
||||
pub(super) fn preview_painter(&self, ctx: &Context) -> egui::Painter {
|
||||
let layer = egui::LayerId::new(egui::Order::Tooltip, self.id.with("drop-preview"));
|
||||
|
||||
@@ -133,7 +133,7 @@ fn a_group_is_a_picture_frame_whose_edges_split_it_in_half_and_whose_middle_join
|
||||
let at = inset(zone);
|
||||
d.hold(tab(&d, INFO), at);
|
||||
let half = zone_rect(timeline, zone).unwrap();
|
||||
assert!(d.filled(half.shrink(1.0)), "{zone:?}: the preview is that half of the group");
|
||||
assert!(d.outlined(half.shrink(1.0)), "{zone:?}: the preview outlines that half of the group");
|
||||
let out = d.release(at);
|
||||
assert_eq!(out.actions, vec![Action::Move { panel: INFO, anchor: TIMELINE, placement: Placement::Split(zone) }], "{zone:?}");
|
||||
d.settle();
|
||||
@@ -275,7 +275,7 @@ fn a_drop_beside_a_narrow_group_splits_that_group_only() {
|
||||
assert!(tools.width() > 120.0, "{tools:?}");
|
||||
let at = pos2(tools.right() - 8.0, tools.center().y + 13.0);
|
||||
d.hold(tab(&d, INFO), at);
|
||||
assert!(d.filled(zone_rect(tools, Zone::Right).unwrap().shrink(1.0)), "the preview is Tools' right half");
|
||||
assert!(d.outlined(zone_rect(tools, Zone::Right).unwrap().shrink(1.0)), "the preview outlines Tools' right half");
|
||||
let out = d.release(at);
|
||||
assert_eq!(out.actions, vec![Action::Move { panel: INFO, anchor: TOOLS, placement: Placement::Split(Zone::Right) }]);
|
||||
d.settle();
|
||||
@@ -610,3 +610,45 @@ fn a_closed_floating_window_closes_its_panels_and_each_reopens_where_it_was() {
|
||||
let window = &d.layout.floating[0];
|
||||
assert_eq!((&window.root, window.rect), (&tree, [700.0, 100.0, 600.0, 400.0]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_preview_fills_the_zone_under_the_pointer_and_outlines_the_result() {
|
||||
let d = film();
|
||||
let timeline = group(&d, TIMELINE);
|
||||
let strip = Rect::from_min_size(timeline.min, vec2(timeline.width(), 26.0));
|
||||
// A side band: its trapezoid, and the half the panel takes.
|
||||
for (zone, at) in [
|
||||
(Zone::Left, pos2(timeline.left() + 8.0, timeline.center().y + 13.0)),
|
||||
(Zone::Bottom, pos2(timeline.center().x, timeline.bottom() - 20.0)),
|
||||
] {
|
||||
let mut d = film();
|
||||
d.hold(tab(&d, INFO), at);
|
||||
assert!(d.filled_polygon(&zone_shape(timeline, zone).unwrap()), "{zone:?}");
|
||||
assert!(d.outlined(zone_rect(timeline, zone).unwrap().shrink(1.0)), "{zone:?}");
|
||||
d.key(egui::Key::Escape);
|
||||
}
|
||||
// The middle and the tab row: the center rectangle and the strip, and the whole group.
|
||||
for at in [timeline.center() + vec2(0.0, 13.0), strip.center()] {
|
||||
let mut d = film();
|
||||
d.hold(tab(&d, INFO), at);
|
||||
assert!(d.filled_polygon(&zone_shape(timeline, Zone::Center).unwrap()), "{at:?}");
|
||||
assert!(d.filled(strip), "{at:?}");
|
||||
assert!(d.outlined(timeline.shrink(1.0)), "{at:?}");
|
||||
d.key(egui::Key::Escape);
|
||||
}
|
||||
// A group's drag bar shows the same.
|
||||
let mut d = film();
|
||||
let bar = d.hit(&Hit::DragBar(PROJECT[0])).unwrap().center();
|
||||
d.hold(bar, pos2(timeline.left() + 8.0, timeline.center().y + 13.0));
|
||||
assert!(d.filled_polygon(&zone_shape(timeline, Zone::Left).unwrap()));
|
||||
d.key(egui::Key::Escape);
|
||||
|
||||
// The dock-and-canvas arrangement keeps its filled half.
|
||||
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));
|
||||
assert!(!d.filled_polygon(&zone_shape(timeline, Zone::Left).unwrap()));
|
||||
assert!(d.filled(zone_rect(timeline, Zone::Left).unwrap().shrink(1.0)));
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use egui::{Pos2, Rangef, Rect, pos2};
|
||||
use egui::{Pos2, Rangef, Rect, Vec2, pos2, vec2};
|
||||
|
||||
use super::{Placement, Side, Target, Zone};
|
||||
|
||||
@@ -45,8 +45,7 @@ pub fn drop_zone(rect: Rect, tab_height: f32, pointer: Pos2) -> Option<Zone> {
|
||||
if pointer.y < rect.top() + tab_height.min(rect.height()) {
|
||||
return Some(Zone::Center);
|
||||
}
|
||||
let depth = |extent: f32| (extent * 0.25).max(MIN_SIDE_ZONE).min(extent * 0.5);
|
||||
let (x, y) = (depth(rect.width()), depth(rect.height()));
|
||||
let Vec2 { x, y } = side_depths(rect);
|
||||
let distances = [
|
||||
((pointer.x - rect.left()) / x, Zone::Left),
|
||||
((rect.right() - pointer.x) / x, Zone::Right),
|
||||
@@ -57,6 +56,30 @@ pub fn drop_zone(rect: Rect, tab_height: f32, pointer: Pos2) -> Option<Zone> {
|
||||
Some(if distance > 1.0 { Zone::Center } else { zone })
|
||||
}
|
||||
|
||||
/// How deep the side zones of a group drawn at `rect` reach in from its left and right edges
|
||||
/// (`x`) and its top and bottom edges (`y`).
|
||||
fn side_depths(rect: Rect) -> Vec2 {
|
||||
let depth = |extent: f32| (extent * 0.25).max(MIN_SIDE_ZONE).min(extent * 0.5);
|
||||
vec2(depth(rect.width()), depth(rect.height()))
|
||||
}
|
||||
|
||||
/// The outline of a drop zone of the group drawn at `rect`, as [`drop_zone`] finds it: a side's
|
||||
/// trapezoid (its outer edge the whole side, its inner edge on the center rectangle), or the
|
||||
/// center rectangle. The tab strip is a grouping zone too.
|
||||
pub fn zone_shape(rect: Rect, zone: Zone) -> Option<Vec<Pos2>> {
|
||||
if !usable(rect) {
|
||||
return None;
|
||||
}
|
||||
let inner = rect.shrink2(side_depths(rect));
|
||||
Some(match zone {
|
||||
Zone::Left => vec![rect.left_top(), inner.left_top(), inner.left_bottom(), rect.left_bottom()],
|
||||
Zone::Right => vec![inner.right_top(), rect.right_top(), rect.right_bottom(), inner.right_bottom()],
|
||||
Zone::Top => vec![rect.left_top(), rect.right_top(), inner.right_top(), inner.left_top()],
|
||||
Zone::Bottom => vec![inner.left_bottom(), inner.right_bottom(), rect.right_bottom(), rect.left_bottom()],
|
||||
Zone::Center => vec![inner.left_top(), inner.right_top(), inner.right_bottom(), inner.left_bottom()],
|
||||
})
|
||||
}
|
||||
|
||||
/// Preview of the region occupied after a split or group merge.
|
||||
pub fn zone_rect(rect: Rect, zone: Zone) -> Option<Rect> {
|
||||
if !usable(rect) {
|
||||
|
||||
@@ -215,6 +215,18 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static, C: DockContent<P>> Do
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether the last frame filled a polygon with these corners, each within half a point.
|
||||
pub fn filled_polygon(&self, corners: &[Pos2]) -> bool {
|
||||
self.shapes.iter().any(|clipped| match &clipped.shape {
|
||||
egui::Shape::Path(path) => {
|
||||
path.fill != egui::Color32::TRANSPARENT
|
||||
&& path.points.len() == corners.len()
|
||||
&& path.points.iter().zip(corners).all(|(a, b)| a.distance(*b) <= 0.5)
|
||||
}
|
||||
_ => false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether the last frame outlined `rect` without filling it.
|
||||
pub fn outlined(&self, rect: Rect) -> bool {
|
||||
self.shapes.iter().any(|clipped| match &clipped.shape {
|
||||
|
||||
+4
-1
@@ -224,7 +224,10 @@ A flyout fits its content the same way, without the title bar.
|
||||
for one through `PanelLimits::max`: a panel without a maximum lifts the maximum of the panels it
|
||||
joins.
|
||||
- Drop previews show the destination: an outline for tabs, a line for a new group, column or dock,
|
||||
a filled half for a split. Escape or losing window focus cancels a drag.
|
||||
a filled half for a split. With `side_edge_zones`, a drop on a group instead fills the zone
|
||||
under the pointer (a side's trapezoid, or the center rectangle and the tab strip; `zone_shape`
|
||||
gives the outline) and outlines the rect the drop gives what moves. Escape or losing window
|
||||
focus cancels a drag.
|
||||
- With `float_on_empty_drop`, a release over no target floats what moves at the pointer, such
|
||||
as a panel dropped on a protected canvas. Without it, that release cancels the drag and
|
||||
nothing moves: the gap between two groups, a menu bar or a status bar is no target. A release
|
||||
|
||||
Reference in new issue
Block a user