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:
aiongg committed 2026-10-25 05:34:35 +00:00
1 parent f268d21055
commit 63b3e4e2e8
8 files changed
+268 -79

No files matched your search

+60
View File
@@ -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) {
+117
View File
@@ -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
);
}
+3 -2
View File
@@ -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)]);
+19 -11
View File
@@ -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.
+54 -26
View File
@@ -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 };
+3 -38
View File
@@ -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>(
+2 -1
View File
@@ -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
View File
@@ -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