mirror of
https://github.com/storytold/craft-libs.git
synced 2026-10-31 07:39:23 +00:00
docking: a drop that changes only sizes is a no-op; a lone floating group's header drag looks through its window
This commit is contained in:
1 parent
f268d21055
commit
63b3e4e2e8
8 files changed
+268
-79
No files matched your search
@@ -335,6 +335,66 @@ fn splice<P>(panels: &mut Vec<P>, active: &mut usize, index: usize, new: Vec<P>,
|
||||
}
|
||||
|
||||
impl<P: Clone + Eq + Hash> Layout<P> {
|
||||
/// The layout without its sizes and rectangles: which panels are where, and their state.
|
||||
/// A run of splits along one axis nests one way (`a | (b | c)`), since nesting it the
|
||||
/// other way only moves sizes between its members.
|
||||
pub(super) fn shape(&self) -> Layout<P> {
|
||||
fn run<'a, P>(node: &'a Node<P>, axis: SplitAxis, out: &mut Vec<&'a Node<P>>) {
|
||||
match node {
|
||||
Node::Split { axis: own, first, second, .. } if *own == axis => {
|
||||
run(first, axis, out);
|
||||
run(second, axis, out);
|
||||
}
|
||||
node => out.push(node),
|
||||
}
|
||||
}
|
||||
fn node<P: Clone>(n: &Node<P>) -> Node<P> {
|
||||
match n {
|
||||
Node::Split { axis, .. } => {
|
||||
let mut members = Vec::new();
|
||||
run(n, *axis, &mut members);
|
||||
let mut shaped = members.into_iter().rev().map(node);
|
||||
let last = shaped.next().unwrap_or(Node::Tabs { panels: Vec::new(), active: 0 });
|
||||
shaped.fold(last, |second, first| Node::Split {
|
||||
axis: *axis,
|
||||
size: SplitSize::Ratio(0.5),
|
||||
first: Box::new(first),
|
||||
second: Box::new(second),
|
||||
})
|
||||
}
|
||||
Node::Tabs { panels, active } => Node::Tabs { panels: panels.clone(), active: *active },
|
||||
Node::Stack { entries } => Node::Stack { entries: entries.iter().map(|e| super::StackEntry { height: None, ..e.clone() }).collect() },
|
||||
Node::Dock { side, columns } => Node::Dock {
|
||||
side: *side,
|
||||
columns: columns
|
||||
.iter()
|
||||
.map(|c| Column {
|
||||
groups: c.groups.iter().map(|g| Group { height: None, ..g.clone() }).collect(),
|
||||
width: 0.0,
|
||||
iconic: c.iconic,
|
||||
iconic_width: None,
|
||||
})
|
||||
.collect(),
|
||||
},
|
||||
}
|
||||
}
|
||||
Layout {
|
||||
root: self.root.as_ref().map(node),
|
||||
floating: self
|
||||
.floating
|
||||
.iter()
|
||||
.map(|w| Floating { root: node(&w.root), rect: [0.0; 4], native: None, monitor: None, monitor_size: None, ..w.clone() })
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `other` arranges the panels as this layout does: equal apart from split sizes,
|
||||
/// column widths, group heights and window rectangles. A drop that leads from one to the
|
||||
/// other changes nothing the user would see as a move.
|
||||
pub(super) fn same_arrangement(&self, other: &Self) -> bool {
|
||||
self.shape() == other.shape()
|
||||
}
|
||||
|
||||
pub(super) fn place(&self, panel: &P) -> Option<Place> {
|
||||
for (w, window) in self.floating.iter().enumerate() {
|
||||
for (leaf, path) in leaves(&window.root) {
|
||||
|
||||
@@ -315,3 +315,120 @@ fn a_canvas_that_takes_splits_takes_them_only_on_its_drop_edges() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the last frame painted a drop preview: an accent line, outline or fill.
|
||||
fn previews(d: &Driver) -> bool {
|
||||
let accent = accent();
|
||||
d.shapes().iter().any(|clipped| match &clipped.shape {
|
||||
egui::Shape::Rect(shape) => shape.fill == accent || shape.fill == accent.gamma_multiply(0.08) || shape.stroke.color == accent,
|
||||
egui::Shape::Path(path) => path.fill != Color32::TRANSPARENT && path.fill == accent.gamma_multiply(0.3),
|
||||
_ => false,
|
||||
})
|
||||
}
|
||||
|
||||
fn ratio_split(axis: SplitAxis, ratio: f32, first: Node<u32>, second: Node<u32>) -> Node<u32> {
|
||||
Node::Split { axis, size: SplitSize::Ratio(ratio), first: Box::new(first), second: Box::new(second) }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_drop_that_changes_only_sizes_shows_nothing_and_does_nothing() {
|
||||
// CADCraft's sidebar: Layers (2) alone above Properties (3) and a third group (4), beside
|
||||
// the canvas (1); and the same with the column as one two-group split.
|
||||
let nested = ratio_split(SplitAxis::Vertical, 0.3, tabs(&[2]), ratio_split(SplitAxis::Vertical, 0.5, tabs(&[3]), tabs(&[4])));
|
||||
let flat = ratio_split(SplitAxis::Vertical, 0.3, tabs(&[2]), tabs(&[3]));
|
||||
for column in [nested, flat] {
|
||||
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 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)));
|
||||
// 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");
|
||||
let out = d.release(top);
|
||||
assert!(out.actions.is_empty(), "{:?}", out.actions);
|
||||
assert_eq!(d.layout, layout);
|
||||
}
|
||||
// A lone frame's tab on the layout edge its frame already holds.
|
||||
let layout = Layout { root: Some(ratio_split(SplitAxis::Vertical, 0.75, tabs(&[1]), tabs(&[4]))), floating: Vec::new() };
|
||||
let mut d = DockDriver::new(layout.clone(), Bodies::default());
|
||||
let at = pos2(400.0, 590.0);
|
||||
d.hold(tab(&d, 4), at);
|
||||
assert!(!previews(&d));
|
||||
let out = d.release(at);
|
||||
assert!(out.actions.is_empty(), "{:?}", out.actions);
|
||||
assert_eq!(d.layout, layout);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_dock_drop_that_changes_only_heights_shows_nothing_and_does_nothing() {
|
||||
// A dock column of a sized lone group (2) above another (3), beside the canvas (1).
|
||||
let column = Column::new(vec![Group { height: Some(150.0), ..Group::new(vec![2], 0) }, Group::new(vec![3], 0)], 240.0);
|
||||
let dock = Node::Dock { side: Side::End, columns: vec![column] };
|
||||
let root = Node::Split { axis: SplitAxis::Horizontal, size: SplitSize::FixedSecond(240.0), first: Box::new(tabs(&[1])), second: Box::new(dock) };
|
||||
let layout = Layout { root: Some(root), floating: Vec::new() };
|
||||
let start = || DockDriver::new(layout.clone(), Bodies::default());
|
||||
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.
|
||||
let mut d = start();
|
||||
d.hold(tab(&d, 2), pos2(body.center().x, body.bottom() - body.height() * 0.2));
|
||||
assert!(previews(&d));
|
||||
// The lower group's top third and the seam between them put the group back where it is.
|
||||
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");
|
||||
let out = d.release(at);
|
||||
assert!(out.actions.is_empty(), "{at:?}: {:?}", out.actions);
|
||||
assert_eq!(d.layout, layout);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_header_drag_of_a_windows_only_group_looks_through_its_window() {
|
||||
// Group 4 floats alone over frame 1's left part.
|
||||
let mut layout = frames();
|
||||
layout.floating.push(Floating::single(vec![4], 0, [40.0, 150.0, 300.0, 260.0]));
|
||||
let start = || DockDriver::new(layout.clone(), Bodies::default());
|
||||
let d = start();
|
||||
let (body, rect) = group(&d, 1);
|
||||
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) } }]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_header_drag_in_a_window_of_two_groups_still_offers_the_other_group() {
|
||||
let mut layout = frames();
|
||||
let tree = ratio_split(SplitAxis::Vertical, 0.5, tabs(&[4]), tabs(&[5]));
|
||||
layout.floating.push(Floating::new(tree, [40.0, 100.0, 300.0, 400.0]));
|
||||
let start = || DockDriver::new(layout.clone(), Bodies::default());
|
||||
let d = start();
|
||||
let (body, rect) = group(&d, 5);
|
||||
let mut d = start();
|
||||
let bar = d.hit(&Hit::DragBar(4)).unwrap().center();
|
||||
d.hold(bar, body.center());
|
||||
assert!(d.outlined(rect.shrink(1.0)), "group 5 joins");
|
||||
let out = d.release(body.center());
|
||||
assert!(
|
||||
matches!(
|
||||
&out.actions[..],
|
||||
[Action::MoveGroup { panel: 4, to: Target::At { anchor: 5, placement: Placement::Tab { .. } | Placement::Split(Zone::Center) } }]
|
||||
),
|
||||
"{:?}",
|
||||
out.actions
|
||||
);
|
||||
}
|
||||
@@ -503,7 +503,8 @@ fn release_over_own_position_is_a_noop() {
|
||||
#[test]
|
||||
fn floating_border_and_drag_bar_move_and_cancel_as_a_group() {
|
||||
for name in ["drag-border", "drag-bar"] {
|
||||
let layout = Layout { root: Some(tabs(&[1], 0)), floating: vec![Floating::single(vec![5, 6], 1, [80.0, 70.0, 300.0, 220.0])] };
|
||||
// Nothing docked: a header drag of a window's only group looks through the window.
|
||||
let layout = Layout { root: None, floating: vec![Floating::single(vec![5, 6], 1, [80.0, 70.0, 300.0, 220.0])] };
|
||||
let mut d = Driver::new(layout.clone());
|
||||
let from = d.rect(name, 5).center();
|
||||
d.hold(from, from + vec2(50.0, 40.0));
|
||||
@@ -522,7 +523,7 @@ fn floating_border_and_drag_bar_move_and_cancel_as_a_group() {
|
||||
|
||||
#[test]
|
||||
fn floating_border_movement_does_not_accumulate_fractional_step_rounding() {
|
||||
let layout = Layout { root: Some(tabs(&[1], 0)), floating: vec![Floating::single(vec![5, 6], 1, [80.0, 70.0, 300.0, 220.0])] };
|
||||
let layout = Layout { root: None, floating: vec![Floating::single(vec![5, 6], 1, [80.0, 70.0, 300.0, 220.0])] };
|
||||
let mut d = Driver::new(layout);
|
||||
let from = d.rect("drag-border", 5).center();
|
||||
d.step(vec![Event::PointerMoved(from), button(from, true)]);
|
||||
|
||||
@@ -10,7 +10,7 @@ use egui::{Context, Key, Pos2, Rect, Sense, Stroke, StrokeKind, Ui, UiBuilder, p
|
||||
|
||||
use super::{GroupKind, Renderer, RowItem, row_drag, row_range, stripless};
|
||||
use crate::docking::{
|
||||
Action, Column, DockEvent, DockHeights, DockStyle, Group, GroupDrag, GroupGesture, Node, Placement, Side, Target, Zone,
|
||||
Action, Column, DockEvent, DockHeights, DockStyle, DropTarget, Group, GroupDrag, GroupGesture, Node, Placement, Side, Target, Zone,
|
||||
dock_model::Unit,
|
||||
targets::{
|
||||
DockDrop, DockTarget, SIDE_EDGE_ZONE_DEPTH, STRIP_REACH, body_zone, column_edge, drop_zone, horizontal_line, resolve_dock, usable,
|
||||
@@ -636,18 +636,25 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
/// takes tabs: the strip joins the group, and border bands reach into it only
|
||||
/// [`STRIP_REACH`] from their lines. Not with [`DockStyle::side_edge_zones`].
|
||||
pub(super) fn in_tab_strip(&self, pointer: Pos2) -> bool {
|
||||
self.in_tab_strip_past(pointer, &[])
|
||||
}
|
||||
|
||||
/// [`Self::in_tab_strip`] past the groups holding any of `through`.
|
||||
fn in_tab_strip_past(&self, pointer: Pos2, through: &[P]) -> bool {
|
||||
let own = |t: &DropTarget<P>| t.tabs.iter().any(|(p, _)| through.contains(p));
|
||||
!self.style.side_edge_zones
|
||||
&& self.targets.iter().rev().find(|t| usable(t.rect) && t.rect.contains(pointer)).is_some_and(|t| {
|
||||
&& self.targets.iter().rev().find(|t| usable(t.rect) && t.rect.contains(pointer) && !own(t)).is_some_and(|t| {
|
||||
t.accepts_tabs && t.tab_strip.map(|strip| strip.intersect(t.rect)).is_some_and(|strip| usable(strip) && strip.contains(pointer))
|
||||
})
|
||||
}
|
||||
|
||||
/// The dock target at `pointer`: a band at a border, else a side zone of a dock group's
|
||||
/// body ([`SIDE_EDGE_ZONE_DEPTH`] deep with [`DockStyle::side_edge_zones`]). The middle of a
|
||||
/// group that takes tabs is none: the group's own target joins it.
|
||||
fn dock_drop(&self, pointer: Pos2) -> Option<DockDrop<P>> {
|
||||
/// group that takes tabs is none: the group's own target joins it. The groups holding
|
||||
/// `through` (a floating window the drag looks through) hide no strip.
|
||||
fn dock_drop(&self, pointer: Pos2, through: &[P]) -> Option<DockDrop<P>> {
|
||||
let mut candidates = self.dock_candidates();
|
||||
let strip = self.in_tab_strip(pointer);
|
||||
let strip = self.in_tab_strip_past(pointer, through);
|
||||
if strip {
|
||||
candidates.retain(|t| t.line.distance_to_pos(pointer) <= STRIP_REACH);
|
||||
}
|
||||
@@ -670,11 +677,11 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
/// The dock target a whole group or column drag would take at `pointer`, with the action
|
||||
/// and its line. A target that leaves the layout unchanged is a no-op: `None` action.
|
||||
pub(super) fn dock_group_proposal(&self, drag: &GroupDrag<P>, pointer: Pos2) -> Option<(Option<Action<P>>, Rect)> {
|
||||
let moving = (drag.gesture == GroupGesture::Window).then_some(&drag.panel);
|
||||
if self.dock_covered(pointer, moving) {
|
||||
let through = self.looks_through(drag);
|
||||
if self.dock_covered(pointer, through.then_some(&drag.panel)) {
|
||||
return None;
|
||||
}
|
||||
let drop = self.dock_drop(pointer)?;
|
||||
let drop = self.dock_drop(pointer, if through { &drag.members } else { &[] })?;
|
||||
let action = group_action(drag, drop.target.to());
|
||||
self.checked(action).map(|action| (action, drop.line))
|
||||
}
|
||||
@@ -686,7 +693,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
if self.dock_covered(pointer, None) {
|
||||
return None;
|
||||
}
|
||||
let drop = self.dock_drop(pointer)?;
|
||||
let drop = self.dock_drop(pointer, &[])?;
|
||||
let action = match drop.target.to() {
|
||||
// A target named by the dragged panel is its own group or column: another panel
|
||||
// there names the same one. With none, the drop leaves the panel where it is.
|
||||
@@ -707,11 +714,12 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
self.checked(action).map(|action| (action, drop.line))
|
||||
}
|
||||
|
||||
/// `Some(None)` when the action is valid but changes nothing, `None` when it is invalid.
|
||||
/// `Some(None)` when the action is valid but changes nothing apart from sizes
|
||||
/// ([`crate::docking::Layout::same_arrangement`]), `None` when it is invalid.
|
||||
fn checked(&self, action: Action<P>) -> Option<Option<Action<P>>> {
|
||||
let mut candidate = self.layout.clone();
|
||||
candidate.apply_with_permissions(action.clone(), |p| self.permissions(p)).ok()?;
|
||||
Some((candidate != *self.layout).then_some(action))
|
||||
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.
|
||||
|
||||
@@ -127,24 +127,35 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
}
|
||||
|
||||
/// The valid target under the pointer for a whole group. A target holding the group
|
||||
/// itself is a no-op proposal; a target the model would reject is no target. A window
|
||||
/// moving live looks through itself and does not split frames or, moved by its bar, join
|
||||
/// a body's center.
|
||||
/// itself is a no-op proposal, and so is a drop that would change only sizes; a target
|
||||
/// the model would reject is no target. A drag that takes a whole floating window along
|
||||
/// looks through it ([`Self::looks_through`]). A window moving live does not split frames
|
||||
/// or, moved by its bar, join a body's center.
|
||||
pub(super) fn group_proposal(&self, drag: &GroupDrag<P>, pointer: Pos2) -> Option<DropProposal<P>> {
|
||||
let window = drag.gesture == GroupGesture::Window;
|
||||
if let Some(proposal) = self.extra_proposal(&drag.members, pointer, window) {
|
||||
return self.valid_group(drag, proposal);
|
||||
if let Some(proposal) = self.extra_proposal(&drag.members, pointer, self.looks_through(drag)) {
|
||||
return self.valid_group(drag, self.settled(proposal, |to| group_action(drag, to)));
|
||||
}
|
||||
let target = self.group_target(drag, pointer)?;
|
||||
self.group_proposal_at(drag, pointer, target)
|
||||
}
|
||||
|
||||
/// The group under the pointer that a group drag addresses. A window moving live looks
|
||||
/// through itself.
|
||||
/// Whether a group drag looks through the floating window it comes from: the window's
|
||||
/// title bar moves it, or the drag holds the window's only group and so takes the window
|
||||
/// along. The window then covers nothing behind it.
|
||||
pub(super) fn looks_through(&self, drag: &GroupDrag<P>) -> bool {
|
||||
match drag.gesture {
|
||||
GroupGesture::Window => true,
|
||||
GroupGesture::Group => self.whole_window(&drag.panel),
|
||||
GroupGesture::Column => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// The group under the pointer that a group drag addresses, past the drag's own groups
|
||||
/// when it looks through its window.
|
||||
fn group_target(&self, drag: &GroupDrag<P>, pointer: Pos2) -> Option<&DropTarget<P>> {
|
||||
let window = drag.gesture == GroupGesture::Window;
|
||||
let through = self.looks_through(drag);
|
||||
let own = |t: &DropTarget<P>| t.tabs.iter().any(|(p, _)| drag.members.contains(p));
|
||||
self.targets.iter().rev().find(|t| crate::docking::targets::usable(t.rect) && t.rect.contains(pointer) && !(window && own(t)))
|
||||
self.targets.iter().rev().find(|t| crate::docking::targets::usable(t.rect) && t.rect.contains(pointer) && !(through && own(t)))
|
||||
}
|
||||
|
||||
/// [`Self::group_proposal`] on `target` alone.
|
||||
@@ -189,23 +200,37 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
/// [`crate::docking::resolve_drop_by`] with [`crate::docking::DockStyle::zone_rule`] and the
|
||||
/// canvas sides of [`crate::docking::DockStyle::drop_edges`], where a split that would leave
|
||||
/// a group without room becomes a join of the target group, or no target when the group
|
||||
/// takes no tabs.
|
||||
/// takes no tabs. A drop that would change only sizes is a no-op ([`Self::settled`]).
|
||||
fn resolve_roomy(&self, panel: &P, pointer: Pos2, targets: &[DropTarget<P>], action: impl Fn(Target<P>) -> Action<P>) -> Option<DropProposal<P>> {
|
||||
let proposal = resolve_drop_on(panel, pointer, targets, self.style.zone_rule(), self.style.drop_sides())?;
|
||||
if !matches!(proposal.placement, Placement::Split(zone) if zone != Zone::Center) {
|
||||
return Some(proposal);
|
||||
return Some(self.settled(proposal, action));
|
||||
}
|
||||
if self.leaves_room(action(Target::At { anchor: proposal.anchor.clone(), placement: proposal.placement.clone() })) {
|
||||
return Some(proposal);
|
||||
return Some(self.settled(proposal, action));
|
||||
}
|
||||
let target = targets.iter().rev().find(|t| crate::docking::targets::usable(t.rect) && t.rect.contains(pointer))?;
|
||||
resolve_drop_on(
|
||||
let proposal = resolve_drop_on(
|
||||
panel,
|
||||
pointer,
|
||||
std::slice::from_ref(&DropTarget { allow_splits: false, ..target.clone() }),
|
||||
self.style.zone_rule(),
|
||||
self.style.drop_sides(),
|
||||
)
|
||||
)?;
|
||||
Some(self.settled(proposal, action))
|
||||
}
|
||||
|
||||
/// `proposal`, marked a no-op when `action` at it would leave every panel where it is,
|
||||
/// changing only sizes ([`Layout::same_arrangement`]): a lone group dropped beside the
|
||||
/// group it already borders, or a tab on the edge its frame already holds. Such a drop
|
||||
/// shows no target and changes nothing.
|
||||
fn settled(&self, mut proposal: DropProposal<P>, action: impl Fn(Target<P>) -> Action<P>) -> DropProposal<P> {
|
||||
if !proposal.noop {
|
||||
let mut candidate = self.layout.clone();
|
||||
let to = Target::At { anchor: proposal.anchor.clone(), placement: proposal.placement.clone() };
|
||||
proposal.noop = candidate.apply_with_permissions(action(to), |p| self.permissions(p)).is_ok() && candidate.same_arrangement(self.layout);
|
||||
}
|
||||
proposal
|
||||
}
|
||||
|
||||
/// Whether `action`, a drop that splits a group or the outer edge, leaves room: every group
|
||||
@@ -245,8 +270,9 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
}
|
||||
|
||||
/// Release of a group drag: join or split at the target, or float at the ghost with the
|
||||
/// group's size. Over its own docked position nothing changes; a floating group released
|
||||
/// over itself moves there.
|
||||
/// group's size. Over its own docked position, or where the drop would change only sizes,
|
||||
/// nothing changes; a floating group released over itself or a window's title bar moves
|
||||
/// there.
|
||||
fn drop_group(&mut self, drag: &GroupDrag<P>, pointer: Pos2, float_only: bool) {
|
||||
if pointer.distance(drag.origin) < 2.0 {
|
||||
return;
|
||||
@@ -262,7 +288,8 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
self.output.actions.push(group_action(drag, to));
|
||||
return;
|
||||
}
|
||||
if proposal.is_some() && !floating {
|
||||
// A no-op anchored on another group would change only sizes.
|
||||
if proposal.is_some_and(|p| !floating || !drag.members.contains(&p.anchor)) {
|
||||
return;
|
||||
}
|
||||
if !float_only && !self.style.float_on_empty_drop {
|
||||
@@ -416,12 +443,14 @@ 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>> {
|
||||
self.extra_proposal(std::slice::from_ref(panel), pointer, false).or_else(|| {
|
||||
self.resolve_roomy(panel, pointer, &self.targets, |to| match to {
|
||||
Target::At { anchor, placement } => Action::Move { panel: panel.clone(), anchor, placement },
|
||||
to => Action::MovePanel { panel: panel.clone(), to },
|
||||
})
|
||||
})
|
||||
let action = |to| match to {
|
||||
Target::At { anchor, placement } => Action::Move { panel: panel.clone(), anchor, placement },
|
||||
to => Action::MovePanel { panel: panel.clone(), 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),
|
||||
}
|
||||
}
|
||||
|
||||
/// The preview of a tab drag's group target (with
|
||||
@@ -500,8 +529,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
|
||||
/// [`Self::paint_zones`] for a group drag.
|
||||
pub(super) fn paint_group_zones(&self, painter: &egui::Painter, drag: &GroupDrag<P>, pointer: Pos2, proposal: Option<&DropProposal<P>>) -> bool {
|
||||
let window = drag.gesture == GroupGesture::Window;
|
||||
if !self.style.side_edge_zones || self.extra_proposal(&drag.members, pointer, window).is_some() {
|
||||
if !self.style.side_edge_zones || self.extra_proposal(&drag.members, pointer, self.looks_through(drag)).is_some() {
|
||||
return false;
|
||||
}
|
||||
let Some(target) = self.group_target(drag, pointer) else { return false };
|
||||
|
||||
@@ -496,54 +496,19 @@ fn cancel_drag_if_replaced<P: Clone + Eq + Hash + Debug + Send + Sync + 'static>
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
let now = shape(layout);
|
||||
if now == shape(&last.layout) {
|
||||
let now = layout.shape();
|
||||
if now == last.layout.shape() {
|
||||
return;
|
||||
}
|
||||
let mut applied = last.layout;
|
||||
for action in last.actions {
|
||||
let _ = applied.apply(action);
|
||||
}
|
||||
if now != shape(&applied) {
|
||||
if now != applied.shape() {
|
||||
cancel_drag::<P>(ctx, area);
|
||||
}
|
||||
}
|
||||
|
||||
/// `layout` without its sizes and rectangles: which panels are where, and their state.
|
||||
fn shape<P: Clone>(layout: &Layout<P>) -> Layout<P> {
|
||||
use super::{Column, Floating, Group, Node, StackEntry};
|
||||
use crate::layout::SplitSize;
|
||||
fn node<P: Clone>(n: &Node<P>) -> Node<P> {
|
||||
match n {
|
||||
Node::Split { axis, first, second, .. } => {
|
||||
Node::Split { axis: *axis, size: SplitSize::Ratio(0.5), first: Box::new(node(first)), second: Box::new(node(second)) }
|
||||
}
|
||||
Node::Tabs { panels, active } => Node::Tabs { panels: panels.clone(), active: *active },
|
||||
Node::Stack { entries } => Node::Stack { entries: entries.iter().map(|e| StackEntry { height: None, ..e.clone() }).collect() },
|
||||
Node::Dock { side, columns } => Node::Dock {
|
||||
side: *side,
|
||||
columns: columns
|
||||
.iter()
|
||||
.map(|c| Column {
|
||||
groups: c.groups.iter().map(|g| Group { height: None, ..g.clone() }).collect(),
|
||||
width: 0.0,
|
||||
iconic: c.iconic,
|
||||
iconic_width: None,
|
||||
})
|
||||
.collect(),
|
||||
},
|
||||
}
|
||||
}
|
||||
Layout {
|
||||
root: layout.root.as_ref().map(node),
|
||||
floating: layout
|
||||
.floating
|
||||
.iter()
|
||||
.map(|w| Floating { root: node(&w.root), rect: [0.0; 4], native: None, monitor: None, monitor_size: None, ..w.clone() })
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Keep what [`cancel_drag_if_replaced`] compares the next frame against, while a drag of this
|
||||
/// area goes on.
|
||||
fn record_drag_frame<P: Clone + Eq + Hash + Debug + Send + Sync + 'static>(
|
||||
|
||||
@@ -23,7 +23,8 @@ pub struct DropProposal<P> {
|
||||
pub placement: Placement<P>,
|
||||
pub preview: Rect,
|
||||
pub insertion: Option<Rect>,
|
||||
/// The pointer addresses the tab's current insertion slot.
|
||||
/// The pointer addresses the tab's current insertion slot. The renderer also marks a drop
|
||||
/// that would leave every panel where it is, changing only sizes.
|
||||
pub noop: bool,
|
||||
}
|
||||
|
||||
|
||||
+10
-1
@@ -215,7 +215,10 @@ A flyout fits its content the same way, without the title bar.
|
||||
with `title_bar_docks` a release over a target docks the whole window there, else it only
|
||||
moves.
|
||||
- Inside a floating window, tabs and drag bars drag and dock as they do in the docked layout,
|
||||
into any window. Its groups split, join and resize like frames, with dividers and four-way
|
||||
into any window. A drag bar or border of a window's only group takes the whole window along,
|
||||
so, like the title bar, it looks through the window: the groups, docks and windows behind it
|
||||
take the drop while the pointer is still inside it. In a window of several groups, the other
|
||||
groups stay targets. Its groups split, join and resize like frames, with dividers and four-way
|
||||
handles (`Divider::WindowSplit` and `Divider::WindowIntersection`, named by the window's first
|
||||
panel). Double-clicking the title bar minimizes the window to its title bar, or to the tab strip
|
||||
of its only group; so does double-clicking that group's strip. The group menu's float item
|
||||
@@ -308,6 +311,12 @@ A flyout fits its content the same way, without the title bar.
|
||||
- A drop that joins a group shows a 2 pt accent outline of the whole group, with a light fill,
|
||||
and on its strip the 2 pt insertion marker.
|
||||
|
||||
A 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
|
||||
on the layout edge the frame already holds. Split sizes, column widths, group heights and
|
||||
window rectangles are not compared, and a run of splits along one axis is the same however
|
||||
it nests.
|
||||
|
||||
A group drag also shows its ghost at the pointer. With `side_edge_zones`, a drag over a group
|
||||
instead outlines each of its zones that a drop would change something in (the side
|
||||
trapezoids, the center rectangle and the tab strip; `zone_shape` gives the outlines) and
|
||||
|
||||
Reference in new issue
Block a user