mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-02 08:43:57 +00:00
docking: a release over no target cancels; the title bar only moves a window
Two DockStyle settings, on by default so the dock-and-canvas arrangement keeps its behaviour. Without float_on_empty_drop, a tab or group released over no target inside the app window (the gap between groups, a menu bar, a status bar) cancels the drag; Ctrl/Cmd or a release outside the window floats it. Without title_bar_docks, dragging a floating window's title bar only moves the window.
This commit is contained in:
1 parent
d30c207c8d
commit
5abc37e454
6 files changed
+103
-13
No files matched your search
@@ -87,6 +87,8 @@ pub(super) fn frame_style(style: &mut DockStyle) {
|
||||
style.close_others_item = true;
|
||||
style.maximize_item = true;
|
||||
style.command_press_floats = true;
|
||||
style.float_on_empty_drop = false;
|
||||
style.title_bar_docks = false;
|
||||
style.float_hint = true;
|
||||
style.wheel_selects_tabs = true;
|
||||
style.middle_click_closes_tabs = true;
|
||||
|
||||
@@ -323,6 +323,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
/// group target docks or merges every group in it, else hosts get the release in
|
||||
/// `group_drop`.
|
||||
pub(super) fn drop_window(&mut self, drag: &GroupDrag<P>, pointer: Pos2, float_only: bool) {
|
||||
let float_only = float_only || !self.style.title_bar_docks;
|
||||
if !float_only && let Some((action, _)) = self.dock_group_proposal(drag, pointer) {
|
||||
self.output.actions.extend(action);
|
||||
return;
|
||||
|
||||
@@ -60,14 +60,18 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Leaving the window ends a group drag where it is, with no drop.
|
||||
if !released || !(has_pointer || touch_ended) {
|
||||
// Leaving the window ends a group drag where it is, with no drop, unless only a
|
||||
// release outside the window floats.
|
||||
let gone = !has_pointer && !touch_ended;
|
||||
let floats_outside = !self.style.float_on_empty_drop && drag.gesture != GroupGesture::Window;
|
||||
if !(released && !gone) && !(gone && floats_outside) {
|
||||
return;
|
||||
}
|
||||
let Some(pointer) = pointer.filter(|p| !self.blockers.iter().any(|r| r.contains(*p))) else { return };
|
||||
let outside = gone || !ctx.content_rect().contains(pointer);
|
||||
match drag.gesture {
|
||||
GroupGesture::Window => self.drop_window(&drag, pointer, float_only),
|
||||
GroupGesture::Group | GroupGesture::Column => self.drop_group(&drag, pointer, float_only),
|
||||
GroupGesture::Group | GroupGesture::Column => self.drop_group(&drag, pointer, float_only || outside),
|
||||
}
|
||||
} else if let Some(pointer) = pointer.filter(|p| !self.blockers.iter().any(|r| r.contains(*p))) {
|
||||
self.output.group_drag = Some(drag.panel.clone());
|
||||
@@ -164,6 +168,9 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
if proposal.is_some() && !floating {
|
||||
return;
|
||||
}
|
||||
if !float_only && !self.style.float_on_empty_drop {
|
||||
return;
|
||||
}
|
||||
// No target: hosts that still apply their own policy for a whole-group release get it.
|
||||
self.output.group_drop = Some((drag.panel.clone(), pointer));
|
||||
let rect = self.group_ghost(drag, pointer);
|
||||
@@ -196,7 +203,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
x += advance;
|
||||
}
|
||||
}
|
||||
if float_only {
|
||||
if float_only || (drag.gesture == GroupGesture::Window && !self.style.title_bar_docks) {
|
||||
return;
|
||||
}
|
||||
if let Some((action, line)) = self.dock_group_proposal(drag, pointer) {
|
||||
@@ -246,8 +253,10 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
return;
|
||||
}
|
||||
let float_only = float_only(ctx) || drag.undock;
|
||||
let floatable = self.permissions(&drag.panel).floatable
|
||||
&& (float_only || gone || !drag.source.contains(pointer) || self.layout.floating.iter().any(|g| g.contains(&drag.panel)));
|
||||
let outside = gone || !ctx.content_rect().contains(pointer);
|
||||
let empty_drop = self.style.float_on_empty_drop
|
||||
&& (!drag.source.contains(pointer) || self.layout.floating.iter().any(|g| g.contains(&drag.panel)));
|
||||
let floatable = self.permissions(&drag.panel).floatable && (float_only || outside || empty_drop);
|
||||
let floating_rect = floatable.then(|| self.tab_float_rect(&drag, pointer));
|
||||
if self.host_drop(ctx, &drag.panel, pointer, false, released, floating_rect) {
|
||||
if released {
|
||||
@@ -255,7 +264,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
}
|
||||
return;
|
||||
}
|
||||
let dock = if float_only || gone { None } else { self.dock_tab_proposal(&drag.panel, pointer) };
|
||||
let dock = if float_only || outside { None } else { self.dock_tab_proposal(&drag.panel, pointer) };
|
||||
if let Some((action, line)) = dock {
|
||||
if released {
|
||||
cancel_drag::<P>(ctx, self.id);
|
||||
@@ -282,10 +291,10 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
};
|
||||
if released {
|
||||
cancel_drag::<P>(ctx, self.id);
|
||||
if pointer.distance(drag.origin) < 2.0 || proposal.as_ref().is_some_and(|proposal| proposal.noop && !gone) {
|
||||
if pointer.distance(drag.origin) < 2.0 || proposal.as_ref().is_some_and(|proposal| proposal.noop && !outside) {
|
||||
return;
|
||||
}
|
||||
let action = if let Some(proposal) = proposal.filter(|_| !gone) {
|
||||
let action = if let Some(proposal) = proposal.filter(|_| !outside) {
|
||||
Some(Action::Move { panel: drag.panel.clone(), anchor: proposal.anchor, placement: proposal.placement })
|
||||
} else if let Some(rect) = floating_rect {
|
||||
Some(Action::Float { panel: drag.panel.clone(), rect: [rect.left(), rect.top(), rect.width(), rect.height()] })
|
||||
|
||||
@@ -149,12 +149,12 @@ fn a_group_is_a_picture_frame_whose_edges_split_it_in_half_and_whose_middle_join
|
||||
let mut d = film();
|
||||
let out = d.drag(tab(&d, INFO), pos2(timeline.center().x, timeline.top() + 10.0));
|
||||
assert!(matches!(&out.actions[..], [Action::Move { panel: INFO, placement: Placement::Tab { .. }, .. }]), "{:?}", out.actions);
|
||||
// The gap between two groups belongs to neither: the tab floats.
|
||||
// The gap between two groups belongs to neither: the drag cancels.
|
||||
let mut d = film();
|
||||
let tools = group(&d, TOOLS);
|
||||
let gap = pos2(tools.right() + 2.0, tools.center().y);
|
||||
let out = d.drag(tab(&d, INFO), gap);
|
||||
assert!(matches!(&out.actions[..], [Action::Float { panel: INFO, .. }]), "{:?}", out.actions);
|
||||
assert!(out.actions.is_empty(), "{:?}", out.actions);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -443,3 +443,66 @@ fn ctrl_or_cmd_floats_from_a_tab_or_a_drag_bar_and_the_preview_says_so() {
|
||||
assert!(floated(&out.actions, PROJECT[0]), "{from:?}: {:?}", out.actions);
|
||||
}
|
||||
}
|
||||
|
||||
/// The Editing workspace under a 24 pt menu bar and above a 20 pt status bar.
|
||||
fn film_in_window() -> Driver {
|
||||
let mut d = DockDriver::new(editing(), Film::default()).with_style(frame_style);
|
||||
d.screen = Rect::from_min_size(Pos2::ZERO, SCREEN);
|
||||
d.area = Rect::from_min_max(pos2(0.0, 24.0), pos2(SCREEN.x, SCREEN.y - 20.0));
|
||||
d.settle();
|
||||
d
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_release_over_no_target_inside_the_window_cancels_the_drag() {
|
||||
let d = film_in_window();
|
||||
let (source, program) = (group(&d, SOURCE[0]), group(&d, PROGRAM));
|
||||
let gap = pos2((source.right() + program.left()) * 0.5, source.center().y);
|
||||
let from_tab = tab(&d, PROJECT[0]);
|
||||
let from_bar = d.hit(&Hit::DragBar(PROJECT[0])).unwrap().center();
|
||||
for from in [from_tab, from_bar] {
|
||||
for to in [pos2(600.0, 10.0), pos2(600.0, SCREEN.y - 8.0), gap] {
|
||||
let mut d = film_in_window();
|
||||
let before = d.layout.clone();
|
||||
let out = d.drag(from, to);
|
||||
d.settle();
|
||||
assert!(out.actions.is_empty(), "{from:?} to {to:?}: {:?}", out.actions);
|
||||
assert_eq!(d.layout, before, "{from:?} to {to:?}");
|
||||
}
|
||||
// Ctrl/Cmd held floats it there.
|
||||
let mut d = film_in_window();
|
||||
d.modifiers = Modifiers::COMMAND;
|
||||
d.drag(from, pos2(600.0, 10.0));
|
||||
d.settle();
|
||||
assert!(d.layout.floating.iter().any(|w| w.contains(&PROJECT[0])), "{from:?}: {:?}", d.layout.floating);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_release_outside_the_window_floats() {
|
||||
let d = film_in_window();
|
||||
let from_tab = tab(&d, PROJECT[0]);
|
||||
let from_bar = d.hit(&Hit::DragBar(PROJECT[0])).unwrap().center();
|
||||
for from in [from_tab, from_bar] {
|
||||
let mut d = film_in_window();
|
||||
d.hold(from, pos2(600.0, 10.0));
|
||||
d.leave();
|
||||
d.settle();
|
||||
let window = d.layout.floating.iter().find(|w| w.contains(&PROJECT[0]));
|
||||
assert!(window.is_some(), "{from:?}: {:?}", d.layout);
|
||||
// A tab takes one panel; a drag bar takes the group.
|
||||
assert_eq!(window.unwrap().contains(&PROJECT[1]), from == from_bar, "{from:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_title_bar_moves_a_floating_window_without_docking() {
|
||||
let mut d = film();
|
||||
let title = d.hit(&Hit::Title(INFO)).unwrap().center();
|
||||
let timeline = group(&d, TIMELINE);
|
||||
d.drag(title, timeline.center());
|
||||
d.settle();
|
||||
let window = d.layout.floating.iter().find(|w| w.contains(&INFO)).expect("still floating");
|
||||
assert!(window.rect[0] < 1000.0 && window.rect[1] > 400.0, "moved towards the drop: {:?}", window.rect);
|
||||
assert_eq!(d.layout.root, editing().root);
|
||||
}
|
||||
@@ -101,6 +101,13 @@ pub struct DockStyle {
|
||||
/// Ctrl/Cmd held when a tab drag of a split-tree group starts makes its drop float, even
|
||||
/// when the key is let go during the drag. Off: only the key held at the drop counts.
|
||||
pub command_press_floats: bool,
|
||||
/// A drag released over no drop target inside the window floats what it carries, such as
|
||||
/// a panel dropped on a protected canvas. Off: that release cancels the drag; only
|
||||
/// Ctrl/Cmd or a release outside the window floats.
|
||||
pub float_on_empty_drop: bool,
|
||||
/// Dragging a floating window's title bar onto a drop target docks the whole window there.
|
||||
/// Off: the title bar only moves the window; tabs and drag bars dock.
|
||||
pub title_bar_docks: bool,
|
||||
/// Show the built-in Close items in tab and group menus (Close Panel, Close Tab Group,
|
||||
/// Close Other Panels in Group) for closable panels. A host that draws its own Close items
|
||||
/// turns this off and keeps [`Permissions::closable`], so its close actions still apply.
|
||||
@@ -183,6 +190,8 @@ impl DockStyle {
|
||||
float_hint_label: "Hold {key} to undock".into(),
|
||||
float_hint: false,
|
||||
command_press_floats: false,
|
||||
float_on_empty_drop: true,
|
||||
title_bar_docks: true,
|
||||
close_menu_items: true,
|
||||
close_button: true,
|
||||
wheel_selects_tabs: false,
|
||||
|
||||
+8
-2
@@ -17,7 +17,7 @@ outside the crate.
|
||||
| Arrangement | Layout | Style settings | Apps |
|
||||
|---|---|---|---|
|
||||
| Docks beside a canvas (the defaults) | `Node::Dock`s on the window's start and end edges. A dock has columns; a column has stacked groups and can collapse to icons. Frames (tab groups and splits) fill the space between the docks. | `new_dock_zones` on | Creative apps with a canvas and panel docks |
|
||||
| A split tree of tab groups | `Split`s and `Tabs` only. | `side_edge_zones`, `maximize_item`, `close_others_item`, `command_press_floats`, `float_hint`, `wheel_selects_tabs`, `middle_click_closes_tabs` on, `new_dock_zones` off; menu labels such as "Undock Panel" set on `float_label`, `float_group_label` and `close_group_label` | Apps whose whole window is frames |
|
||||
| A split tree of tab groups | `Split`s and `Tabs` only. | `side_edge_zones`, `maximize_item`, `close_others_item`, `command_press_floats`, `float_hint`, `wheel_selects_tabs`, `middle_click_closes_tabs` on, `new_dock_zones`, `float_on_empty_drop` and `title_bar_docks` off; menu labels such as "Undock Panel" set on `float_label`, `float_group_label` and `close_group_label` | Apps whose whole window is frames |
|
||||
|
||||
Sides are logical: `Side::Start` is the left edge, or the right edge when
|
||||
`DockStyle::right_to_left` is set. egui has no app-wide direction, so the app sets it from its
|
||||
@@ -185,7 +185,9 @@ A flyout fits its content the same way, without the title bar.
|
||||
## Gestures
|
||||
|
||||
- A tab drags one panel. A group's drag bar (the space beside its tabs) or its top border drags
|
||||
the whole group. A column bar drags a column. A floating window's title bar drags the window.
|
||||
the whole group. A column bar drags a column. A floating window's title bar drags the window;
|
||||
with `title_bar_docks` a release over a target docks the whole window there, else it only
|
||||
moves.
|
||||
- A movable floating window resizes from every edge and corner. The top edge and corners are a
|
||||
4 pt band above the title bar (`chrome::RESIZE_BAND`), so they take no moves and no clicks on
|
||||
the title bar's buttons. A resize keeps the opposite edges in place and stays within the size
|
||||
@@ -207,6 +209,10 @@ A flyout fits its content the same way, without the title bar.
|
||||
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.
|
||||
- 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
|
||||
outside the app window floats in both cases.
|
||||
- Ctrl (⌘ on macOS) held at the drop floats it wherever it lands. With `command_press_floats`,
|
||||
the key held when a tab drag of a frame starts is enough. With `float_hint`, a hint under the
|
||||
pointer names the modifier while a drag offers a docking target: `float_hint_label`, where
|
||||
|
||||
Reference in new issue
Block a user