mirror of
https://github.com/storytold/craft-libs.git
synced 2026-10-31 07:39:23 +00:00
docking: floating windows stay inside the app's float bounds
DockArea::float_bounds sets where floating windows, flyouts and drag ghosts may go; the default is the whole viewport. An app with its own title bar under a transparent macOS title bar passes the viewport below it, so a floating window can't be dropped, dragged or restored where pressing it moves the app window.
This commit is contained in:
1 parent
a0b923ac1e
commit
2fa4ad79d7
10 files changed
+60
-20
No files matched your search
@@ -388,3 +388,23 @@ fn chrome_has_focus_covers_every_drawn_control() {
|
||||
assert!(DockArea::<u32>::new(d.id).chrome_has_focus(&d.ctx, &d.layout), "{hit:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn floating_windows_stay_inside_the_float_bounds() {
|
||||
// The app's title bar strip (top 30 pt) is outside the bounds: a window saved there is
|
||||
// drawn below it, and a title bar drag towards it stops at its edge. Command keeps the
|
||||
// drag from docking.
|
||||
let mut layout = docked(&[&[1]], false);
|
||||
layout.floating.push(Floating::single(vec![5], 0, [400.0, 0.0, 240.0, 200.0]));
|
||||
let mut d = DockDriver::new(layout, Content);
|
||||
d.float_bounds = Some(Rect::from_min_max(pos2(0.0, 30.0), pos2(800.0, 600.0)));
|
||||
d.settle();
|
||||
let title = hit(&d, Hit::Title(5));
|
||||
assert!(title.top() >= 30.0, "{title:?}");
|
||||
d.modifiers = egui::Modifiers::COMMAND;
|
||||
d.drag(title.center(), pos2(title.center().x, 2.0));
|
||||
d.modifiers = egui::Modifiers::NONE;
|
||||
d.settle();
|
||||
let title = hit(&d, Hit::Title(5));
|
||||
assert!(title.top() >= 30.0, "{title:?}");
|
||||
}
|
||||
@@ -23,7 +23,7 @@ pub(in crate::docking) struct StackHit<P> {
|
||||
|
||||
impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
pub(in crate::docking) fn floating(&mut self, ctx: &Context, windows: &[Floating<P>]) {
|
||||
let screen = ctx.content_rect();
|
||||
let screen = self.float_bounds;
|
||||
if !crate::docking::targets::usable(screen) {
|
||||
return;
|
||||
}
|
||||
@@ -139,7 +139,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
&& !ui.input(|i| i.key_pressed(Key::Escape) || !i.focused)
|
||||
{
|
||||
// From the press origin, so fractional pointer steps do not accumulate rounding.
|
||||
let moved = clamp_float(drag.source.translate(pointer - drag.origin), ui.ctx().content_rect());
|
||||
let moved = clamp_float(drag.source.translate(pointer - drag.origin), self.float_bounds);
|
||||
self.output.actions.push(Action::MoveFloating { panel: first.clone(), rect: [moved.left(), moved.top(), moved.width(), moved.height()] });
|
||||
}
|
||||
if response.bar.double_clicked() {
|
||||
|
||||
@@ -269,7 +269,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
}
|
||||
if floatable && !matches!(owner.kind, GroupKind::Floating { .. }) && self.menu_action(ui, &self.float_group_label()) {
|
||||
let size = self.group_float_size(owner.panels, owner.rect.size());
|
||||
let rect = clamp_float(Rect::from_min_size(owner.rect.min + vec2(24.0, 24.0), size), ui.ctx().content_rect());
|
||||
let rect = clamp_float(Rect::from_min_size(owner.rect.min + vec2(24.0, 24.0), size), self.float_bounds);
|
||||
self.output.actions.push(Action::MoveGroup {
|
||||
panel: panel.clone(),
|
||||
to: Target::Float { rect: Some([rect.left(), rect.top(), rect.width(), rect.height()]) },
|
||||
@@ -410,7 +410,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
response.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), &logical));
|
||||
if response.clicked() {
|
||||
let size = self.float_size(panel, group.size());
|
||||
let rect = clamp_float(Rect::from_min_size(group.min + vec2(24.0, 24.0), size), ui.ctx().content_rect());
|
||||
let rect = clamp_float(Rect::from_min_size(group.min + vec2(24.0, 24.0), size), self.float_bounds);
|
||||
self.output.actions.push(Action::Float { panel: panel.clone(), rect: [rect.left(), rect.top(), rect.width(), rect.height()] });
|
||||
ui.close();
|
||||
}
|
||||
|
||||
@@ -296,8 +296,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
close_flyout::<P>(ctx, self.id);
|
||||
return;
|
||||
}
|
||||
let screen = ctx.content_rect();
|
||||
let bounds = self.workspace.intersect(screen);
|
||||
let bounds = self.workspace.intersect(self.float_bounds);
|
||||
let width = request.width.max(self.style.min_pane).min(bounds.width());
|
||||
let max = self
|
||||
.content
|
||||
|
||||
@@ -35,6 +35,8 @@ pub(super) struct Renderer<'a, P> {
|
||||
pub(super) layout: &'a Layout<P>,
|
||||
/// The docked area, for host drop requests.
|
||||
pub(super) workspace: Rect,
|
||||
/// Where floating windows, flyouts and drag ghosts go: [`crate::docking::DockArea::float_bounds`].
|
||||
pub(super) float_bounds: Rect,
|
||||
pub(super) targets: Vec<DropTarget<P>>,
|
||||
/// [D] Seams, group bands, column edges and new-dock bands, see [`crate::docking::targets::DockDrop`].
|
||||
pub(super) dock_targets: Vec<crate::docking::targets::DockDrop<P>>,
|
||||
|
||||
@@ -134,7 +134,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
Some(window) => window.rect,
|
||||
None => {
|
||||
let size = self.float_size(panel, owner.rect.size());
|
||||
let rect = clamp_float(Rect::from_min_size(owner.rect.min + vec2(24.0, 24.0), size), ctx.content_rect());
|
||||
let rect = clamp_float(Rect::from_min_size(owner.rect.min + vec2(24.0, 24.0), size), self.float_bounds);
|
||||
[rect.left(), rect.top(), rect.width(), rect.height()]
|
||||
}
|
||||
};
|
||||
|
||||
@@ -34,7 +34,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
let (escape, focused, down, released, pointer) =
|
||||
ctx.input(|i| (i.key_pressed(Key::Escape), i.focused, i.pointer.primary_down(), i.pointer.primary_released(), i.pointer.interact_pos()));
|
||||
let pointer = pointer.filter(|p| p.is_finite());
|
||||
let ghost = pointer.map(|pointer| self.group_ghost(ctx, &drag, pointer));
|
||||
let ghost = pointer.map(|pointer| self.group_ghost(&drag, pointer));
|
||||
if !escape
|
||||
&& focused
|
||||
&& (down || released)
|
||||
@@ -67,7 +67,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
let Some(pointer) = pointer.filter(|p| !self.blockers.iter().any(|r| r.contains(*p))) else { return };
|
||||
match drag.gesture {
|
||||
GroupGesture::Window => self.drop_window(&drag, pointer, float_only),
|
||||
GroupGesture::Group | GroupGesture::Column => self.drop_group(ctx, &drag, pointer, float_only),
|
||||
GroupGesture::Group | GroupGesture::Column => self.drop_group(&drag, pointer, float_only),
|
||||
}
|
||||
} else if let Some(pointer) = pointer.filter(|p| !self.blockers.iter().any(|r| r.contains(*p))) {
|
||||
self.output.group_drag = Some(drag.panel.clone());
|
||||
@@ -109,7 +109,7 @@ 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.
|
||||
fn drop_group(&mut self, ctx: &Context, drag: &GroupDrag<P>, pointer: Pos2, float_only: bool) {
|
||||
fn drop_group(&mut self, drag: &GroupDrag<P>, pointer: Pos2, float_only: bool) {
|
||||
if pointer.distance(drag.origin) < 2.0 {
|
||||
return;
|
||||
}
|
||||
@@ -129,7 +129,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
}
|
||||
// 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(ctx, drag, pointer);
|
||||
let rect = self.group_ghost(drag, pointer);
|
||||
let action = group_action(drag, Target::Float { rect: Some([rect.left(), rect.top(), rect.width(), rect.height()]) });
|
||||
if self.layout.clone().apply_with_permissions(action.clone(), |p| self.permissions(p)).is_ok() {
|
||||
self.output.actions.push(action);
|
||||
@@ -142,7 +142,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
let ctx = ui.ctx();
|
||||
let painter = self.preview_painter(ctx);
|
||||
if drag.gesture != GroupGesture::Window {
|
||||
let ghost = self.group_ghost(ctx, drag, pointer);
|
||||
let ghost = self.group_ghost(drag, pointer);
|
||||
drop_preview(&painter, ghost, self.style.accent, true);
|
||||
let strip = Rect::from_min_size(ghost.min, vec2(ghost.width(), self.style.tab_height.min(ghost.height())));
|
||||
let labels = painter.with_clip_rect(strip.shrink(2.0));
|
||||
@@ -207,7 +207,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
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 floating_rect = floatable.then(|| self.tab_float_rect(ctx, &drag, pointer));
|
||||
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 {
|
||||
cancel_drag::<P>(ctx, self.id);
|
||||
@@ -357,16 +357,16 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
|
||||
/// Where a group or column drag floats with the pointer at `pointer`, keeping the grab
|
||||
/// offset, inside the window (Z2).
|
||||
fn group_ghost(&self, ctx: &Context, drag: &GroupDrag<P>, pointer: Pos2) -> Rect {
|
||||
fn group_ghost(&self, drag: &GroupDrag<P>, pointer: Pos2) -> Rect {
|
||||
let size = if drag.gesture == GroupGesture::Window { drag.source.size() } else { self.group_float_size(&drag.members, drag.source.size()) };
|
||||
let grab = (drag.origin - drag.source.min).clamp(Vec2::ZERO, size);
|
||||
clamp_float(Rect::from_min_size(pointer - grab, size), ctx.content_rect())
|
||||
clamp_float(Rect::from_min_size(pointer - grab, size), self.float_bounds)
|
||||
}
|
||||
|
||||
fn tab_float_rect(&self, ctx: &Context, drag: &Drag<P>, pointer: Pos2) -> Rect {
|
||||
fn tab_float_rect(&self, drag: &Drag<P>, pointer: Pos2) -> Rect {
|
||||
let size = self.float_size(&drag.panel, drag.source.size());
|
||||
let grab = drag.grab.min(vec2(size.x, self.style.tab_height));
|
||||
clamp_float(Rect::from_min_size(pointer - grab, size), ctx.content_rect())
|
||||
clamp_float(Rect::from_min_size(pointer - grab, size), self.float_bounds)
|
||||
}
|
||||
|
||||
/// Offer the gesture to [`crate::docking::DockContent::resolve_drag`]. True when the host
|
||||
@@ -388,7 +388,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
whole_group,
|
||||
released,
|
||||
workspace: self.workspace,
|
||||
screen: ctx.content_rect(),
|
||||
screen: self.float_bounds,
|
||||
};
|
||||
let Some(feedback) = self.content.resolve_drag(ctx, self.style, &request) else { return false };
|
||||
if !released {
|
||||
|
||||
@@ -672,11 +672,12 @@ pub struct DockArea<'a, P = ()> {
|
||||
maximized: Option<P>,
|
||||
hide: HidePanels<'a, P>,
|
||||
auto_collapse: bool,
|
||||
float_bounds: Option<Rect>,
|
||||
}
|
||||
|
||||
impl<'a, P: Clone + Eq + Hash + Debug + Send + Sync + 'static> DockArea<'a, P> {
|
||||
pub fn new(id: Id) -> Self {
|
||||
Self { id, activation: None, blockers: Vec::new(), maximized: None, hide: HidePanels::None, auto_collapse: true }
|
||||
Self { id, activation: None, blockers: Vec::new(), maximized: None, hide: HidePanels::None, auto_collapse: true, float_bounds: None }
|
||||
}
|
||||
|
||||
/// [D] Auto-Collapse Iconic Panels (default on): a flyout closes on Escape or a press
|
||||
@@ -720,6 +721,16 @@ impl<'a, P: Clone + Eq + Hash + Debug + Send + Sync + 'static> DockArea<'a, P> {
|
||||
self
|
||||
}
|
||||
|
||||
/// Where floating windows, flyouts and drag ghosts may go (default: the whole viewport).
|
||||
/// An app whose own title bar sits under a transparent OS title bar (macOS full-size
|
||||
/// content) passes the viewport below that strip: a window drawn there would take presses
|
||||
/// that the OS or the app turns into moving the whole app window. Windows outside the
|
||||
/// bounds, such as ones restored from a saved layout, are drawn moved inside.
|
||||
pub fn float_bounds(mut self, bounds: Rect) -> Self {
|
||||
self.float_bounds = Some(bounds);
|
||||
self
|
||||
}
|
||||
|
||||
/// Host overlays that must reject docking gestures, including floating fallback.
|
||||
pub fn blocked_rects(mut self, rects: &[Rect]) -> Self {
|
||||
self.blockers = rects.to_vec();
|
||||
@@ -801,6 +812,7 @@ impl<'a, P: Clone + Eq + Hash + Debug + Send + Sync + 'static> DockArea<'a, P> {
|
||||
blockers: self.blockers,
|
||||
layout,
|
||||
workspace: rect,
|
||||
float_bounds: self.float_bounds.map_or(ui.ctx().content_rect(), |bounds| bounds.intersect(ui.ctx().content_rect())),
|
||||
targets: Vec::new(),
|
||||
dock_targets: Vec::new(),
|
||||
owned_interactions: Vec::new(),
|
||||
|
||||
@@ -42,6 +42,8 @@ pub struct DockDriver<P, C> {
|
||||
pub maximized: Option<P>,
|
||||
/// Passed to [`DockArea::raise_panel`] on the next frame only.
|
||||
pub raise: Option<P>,
|
||||
/// Passed to [`DockArea::float_bounds`].
|
||||
pub float_bounds: Option<Rect>,
|
||||
style: Option<StyleFn>,
|
||||
shapes: Vec<ClippedShape>,
|
||||
time: f64,
|
||||
@@ -63,6 +65,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static, C: DockContent<P>> Do
|
||||
auto_collapse: true,
|
||||
maximized: None,
|
||||
raise: None,
|
||||
float_bounds: None,
|
||||
style: None,
|
||||
shapes: Vec::new(),
|
||||
time: 0.0,
|
||||
@@ -97,7 +100,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static, C: DockContent<P>> Do
|
||||
..Default::default()
|
||||
};
|
||||
let (layout, content, area, id) = (&self.layout, &mut self.content, self.area, self.id);
|
||||
let (style_fn, maximized, raise, auto) = (&self.style, self.maximized.clone(), self.raise.take(), self.auto_collapse);
|
||||
let (style_fn, maximized, raise, auto, bounds) = (&self.style, self.maximized.clone(), self.raise.take(), self.auto_collapse, self.float_bounds);
|
||||
let mut full = self.ctx.run_ui(input, |ui| {
|
||||
let mut style = DockStyle::from_ui(ui);
|
||||
if let Some(adjust) = style_fn {
|
||||
@@ -108,6 +111,9 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static, C: DockContent<P>> Do
|
||||
if let Some(panel) = raise.clone() {
|
||||
dock = dock.raise_panel(panel);
|
||||
}
|
||||
if let Some(bounds) = bounds {
|
||||
dock = dock.float_bounds(bounds);
|
||||
}
|
||||
output = dock.show_content(ui, layout, &style, content);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -133,6 +133,7 @@ Builder options on `DockArea`:
|
||||
| `auto_collapse_iconic(on)` | Close a flyout on Escape or a click outside (default on). |
|
||||
| `raise_panel(panel)` | Raise a panel's floating window, or open its flyout, this frame. It moves no keyboard focus and does not set `DockOutput::focused`. |
|
||||
| `blocked_rects(rects)` | App overlays that docking gestures must ignore. |
|
||||
| `float_bounds(rect)` | Where floating windows, flyouts and drag ghosts may go (default: the whole viewport). Pass the viewport below a title bar drawn under a transparent OS title bar, so no floating window sits where a press moves the app window. |
|
||||
|
||||
`DockStyle::close_menu_items` and `DockStyle::close_button` hide the built-in Close menu items
|
||||
and the floating window ×. They hide controls only: a panel that stays `closable` still closes
|
||||
|
||||
Reference in new issue
Block a user