diff --git a/crates/ui/src/docking/native_tests.rs b/crates/ui/src/docking/native_tests.rs index 8326de97..f872ddae 100644 --- a/crates/ui/src/docking/native_tests.rs +++ b/crates/ui/src/docking/native_tests.rs @@ -1,7 +1,7 @@ //! Floating windows in their own OS windows (feature `native-windows`), drawn by the driver's //! stand-in for OS windows. -use egui::{Color32, Event, Key, Modifiers, PointerButton, Pos2, Rect, Vec2, ViewportCommand, epaint::ClippedShape, pos2, vec2}; +use egui::{Color32, Event, Key, Modifiers, PointerButton, Pos2, Rect, ViewportCommand, epaint::ClippedShape, pos2, vec2}; use super::render::{ APPKIT_CORNER, DragPointer, INACTIVE_PALETTE, MACOS_CLOSE, WindowDrag, appkit_resize_areas, close_rects, drag_opacity, resize_bands, wayland, @@ -1241,45 +1241,6 @@ fn a_bar_drag_follows_the_pointer_when_moves_land_late() { assert_eq!(window(&d, 5).native.map(|r| [r[0], r[1]]), Some([to.x, to.y])); } -/// A bar drag at a steady pointer speed under AppKit's timing: the window's reported place is -/// fresh, while pointer events are measured against where the window was a frame back on some -/// frames. Returns the window's step each frame. Released, the layout keeps where it ended. -fn bar_drag_with_stale_pointer(tells_cursor: bool) -> Vec { - let mut d = driver(layout(true)); - d.host().tells_cursor = tells_cursor; - d.host().stale = vec![1, 0, 1, 1, 0]; - let id = id_of(&d); - let viewport = d.viewport(id); - let grab = pos2(40.0, 9.0); - let mut pointer = d.host().windows[&viewport].rect.min + grab.to_vec2(); - d.window_step(id, vec![Event::PointerMoved(grab), button(grab, true)]); - let mut place = d.host().windows[&viewport].rect.min; - let mut steps = Vec::new(); - // On to the desktop right of the main window, where the release docks nothing. - for _ in 0..24 { - pointer += vec2(20.0, 5.0); - d.window_pointer(id, pointer); - let now = d.host().windows[&viewport].rect.min; - steps.push(now - place); - place = now; - } - let local = pointer - place.to_vec2(); - d.window_step(id, vec![button(local, false)]); - d.settle(); - assert_eq!(window(&d, 5).native.map(|r| pos2(r[0], r[1])), Some(d.host().windows[&viewport].rect.min)); - steps -} - -#[test] -fn a_bar_drag_follows_the_cursor_where_pointer_events_trail_the_window() { - // Without the cursor the window overshoots and steps back. - let steps = bar_drag_with_stale_pointer(false); - assert!(steps.iter().any(|s| s.x < 0.0), "{steps:?}"); - // With it, every frame moves the window by the pointer's step. - let steps = bar_drag_with_stale_pointer(true); - assert!(steps.iter().all(|s| *s == vec2(20.0, 5.0)), "{steps:?}"); -} - #[test] fn a_native_windows_content_is_its_origin_where_its_frame_differs() { // A title bar's height between the window's outer position and its content. @@ -2700,3 +2661,107 @@ fn a_native_window_fades_only_while_its_bar_drag_highlights_a_drop_target() { d.window_step(id, vec![button(pos2(40.0, 9.0), false)]); assert_eq!(drag_opacity(&d.ctx, d.id), 1.0, "after the drop"); } + +/// A driver whose window system moves a native window for a bar drag, as AppKit does. +fn os_driver(layout: Layout) -> DockDriver { + let d = driver(layout); + d.host().moves_windows = true; + d +} + +/// Where window [5, 6]'s bar is pressed, in its coordinates. +const GRAB: Pos2 = pos2(40.0, 9.0); + +/// Press window [5, 6]'s bar. +fn os_press(d: &mut DockDriver) -> u64 { + let id = id_of(d); + d.window_step(id, vec![Event::PointerMoved(GRAB), button(GRAB, true)]); + id +} + +fn bar_dragging(d: &DockDriver) -> bool { + d.ctx.data(|data| data.get_temp::(d.id.with("window-drag"))).is_some() +} + +#[test] +fn the_window_system_moves_a_native_window_for_its_bar_drag() { + let mut d = os_driver(layout(true)); + let id = id_of(&d); + let viewport = d.viewport(id); + let before = moves(&d, viewport).len(); + let sent = d.host().commands.len(); + os_press(&mut d); + assert!(d.host().commands[sent..].iter().any(|(v, c)| *v == viewport && *c == ViewportCommand::StartDrag), "on the press"); + // Over the right edge zone of frame [3, 4]. The window reports its place where the drag + // began, so the target is found from the cursor. + let target = ORIGIN + vec2(760.0, 300.0); + os_bar_drag_on(&mut d, id, target); + assert_eq!(d.host().windows[&viewport].rect.min, target - GRAB.to_vec2()); + assert_eq!(drag_opacity(&d.ctx, d.id), 0.75, "over a drop target"); + // Escape does not cancel it. + d.window_step(id, vec![escape()]); + assert!(bar_dragging(&d)); + assert_eq!(d.host().windows[&viewport].rect.min, target - GRAB.to_vec2()); + // Released on the desktop right of the main window, it stays where the cursor left it. + let desktop = ORIGIN + vec2(1000.0, 300.0); + d.window_pointer(id, desktop); + assert_eq!(drag_opacity(&d.ctx, d.id), 1.0, "over empty space"); + let out = d.window_release(id, GRAB); + assert_eq!(window_move(&out), None, "{:?}", out.actions); + d.settle(); + let to = desktop - GRAB.to_vec2(); + assert_eq!(window(&d, 5).native, Some([to.x, to.y, 240.0, 300.0])); + assert_eq!(moves(&d, viewport).len(), before, "the app moved the window"); +} + +/// Move the cursor of the bar drag of window `id` to the desktop point `to` in two steps. +fn os_bar_drag_on(d: &mut DockDriver, id: u64, to: Pos2) { + let from = d.host().windows[&d.viewport(id)].rect.min + GRAB.to_vec2(); + d.window_pointer(id, from.lerp(to, 0.5)); + d.window_pointer(id, to); +} + +#[test] +fn a_bar_drag_the_window_system_moves_ends_once_on_the_first_release_reported() { + // The window system reports the release a frame before egui: the window docks then. + let mut d = os_driver(layout(true)); + let id = os_press(&mut d); + os_bar_drag_on(&mut d, id, ORIGIN + vec2(760.0, 300.0)); + d.host().held = false; + let out = d.step(vec![]); + assert!(matches!(window_move(&out), Some(Target::At { anchor: 3 | 4, placement: Placement::Split(super::Zone::Right) })), "{:?}", out.actions); + assert!(in_root(&d, 5) && in_root(&d, 6)); + + // With Cmd held it only moves, and egui's release without Cmd a frame later docks nothing. + let mut d = os_driver(layout(true)); + let id = os_press(&mut d); + let viewport = d.viewport(id); + d.modifiers = Modifiers::COMMAND; + let target = ORIGIN + vec2(760.0, 300.0); + os_bar_drag_on(&mut d, id, target); + assert_eq!(drag_opacity(&d.ctx, d.id), 1.0, "a floating drop"); + d.host().held = false; + let out = d.step(vec![]); + assert_eq!(window_move(&out), None, "{:?}", out.actions); + assert!(!bar_dragging(&d)); + d.modifiers = Modifiers::NONE; + let sent = d.host().commands.len(); + let out = d.window_release(id, GRAB); + assert_eq!(window_move(&out), None, "{:?}", out.actions); + assert!(!bar_dragging(&d)); + assert!( + !d.host().commands[sent..].iter().any(|(v, c)| *v == viewport && matches!(c, ViewportCommand::StartDrag | ViewportCommand::OuterPosition(_))) + ); + d.settle(); + let to = target - GRAB.to_vec2(); + assert_eq!(window(&d, 5).native.map(|r| [r[0], r[1]]), Some([to.x, to.y])); + + // A click on the bar over the main window docks and moves nothing. + let mut d = os_driver(layout(true)); + let native = window(&d, 5).native; + let id = os_press(&mut d); + let out = d.window_release(id, GRAB); + assert!(out.actions.is_empty(), "{:?}", out.actions); + d.settle(); + assert_eq!(window(&d, 5).native, native); +} diff --git a/crates/ui/src/docking/render/cross.rs b/crates/ui/src/docking/render/cross.rs index a868744f..0b1dea9a 100644 --- a/crates/ui/src/docking/render/cross.rs +++ b/crates/ui/src/docking/render/cross.rs @@ -154,11 +154,21 @@ impl Renderer<'_, P> { window: window.map(|w| w.window), docks: window.is_some_and(|w| w.docks), }); - ctx.data_mut(|d| d.insert_temp(key, pointer)); - self.cross.pointer = Some(pointer); + self.write_pointer(ctx, pointer); pointer } + /// Write `pointer` as the drag's for this frame. An Escape from another window is no + /// cancel of a drag that the window system carries out. + pub(super) fn write_pointer(&mut self, ctx: &Context, pointer: DragPointer) { + let id = self.id; + ctx.data_mut(|d| { + d.remove::(id.with(ESCAPE)); + d.insert_temp(id.with(POINTER), pointer); + }); + self.cross.pointer = Some(pointer); + } + /// In a window that does not own the drag (`window`, `None` for the main window): an Escape /// pressed here cancels the drag, and is no shortcut. pub(super) fn escape_elsewhere(&self, ctx: &Context, window: Option) { diff --git a/crates/ui/src/docking/render/mod.rs b/crates/ui/src/docking/render/mod.rs index 0f5eed6c..751e17b4 100644 --- a/crates/ui/src/docking/render/mod.rs +++ b/crates/ui/src/docking/render/mod.rs @@ -29,7 +29,7 @@ pub(in crate::docking) use body_drag::held_events; #[cfg(feature = "native-windows")] pub(in crate::docking) use body_drag::{cancel_body_route, mark_app_drag}; #[cfg(feature = "native-windows")] -pub(in crate::docking) use native::{DesktopCursor, InputHook, send_keys, set_input_hook, typing_window}; +pub(in crate::docking) use native::{InputHook, WindowSystem, send_keys, set_input_hook, typing_window}; /// How long after blocking starts a native window that still has the keyboard hands it to the /// main window: an OS dialog the app opens with the modal (a file dialog) takes focus a little @@ -137,9 +137,10 @@ pub(super) struct Renderer<'a, P> { /// The other OS windows a drag may cross into. #[cfg(feature = "native-windows")] pub(super) cross: cross::Cross

, - /// Where a native window that follows a bar drag goes. + /// What the window system tells of the desktop pointer, and whether it moves a native + /// window for a bar drag. #[cfg(feature = "native-windows")] - pub(super) cursor: DesktopCursor, + pub(super) window_system: WindowSystem, } pub(super) struct DrawnDivider { diff --git a/crates/ui/src/docking/render/native.rs b/crates/ui/src/docking/render/native.rs index e7e495a0..eaa3f688 100644 --- a/crates/ui/src/docking/render/native.rs +++ b/crates/ui/src/docking/render/native.rs @@ -128,27 +128,66 @@ fn set_window_opacity(title: &str, main: Option<&str>, opacity: f64) { } } -/// Where the cursor is on the desktop, in egui's desktop points, for a native window that -/// follows a bar drag. +/// What the window system tells of the desktop pointer, and whether it moves a native window +/// for a bar drag itself. #[derive(Clone, Copy, Debug, Default, PartialEq)] -pub(in crate::docking) enum DesktopCursor { - /// The window system's: AppKit's on macOS, none elsewhere. +pub(in crate::docking) enum WindowSystem { + /// The running one: AppKit on macOS tells the cursor, the button and the modifiers, and + /// moves a window for `StartDrag`; elsewhere none of these is told. #[default] - System, - /// A stand-in window system's (`docking::testing::NativeHost`), or none. + Native, + /// A stand-in (`docking::testing::NativeHost`). #[cfg(any(test, feature = "testing"))] - Fixed(Option), + Host { + /// The cursor on the desktop, where told. + cursor: Option, + /// Whether the primary button is down, where told. + held: Option, + /// It moves a window for `StartDrag`, as AppKit does. + moves: bool, + }, } -impl DesktopCursor { - fn at(self, ctx: &Context) -> Option { +impl WindowSystem { + /// Where the cursor is on the desktop, in egui's desktop points. + fn cursor(self, ctx: &Context) -> Option { match self { - Self::System => system_cursor(ctx), + Self::Native => system_cursor(ctx), #[cfg(any(test, feature = "testing"))] - Self::Fixed(at) => at, + Self::Host { cursor, .. } => cursor, } .filter(|p| p.is_finite()) } + + /// Whether the primary button is down now. egui can report a release a frame after it. + fn primary_held(self) -> Option { + match self { + Self::Native => system_primary_held(), + #[cfg(any(test, feature = "testing"))] + Self::Host { held, .. } => held, + } + } + + /// Whether Ctrl or Cmd is down now. AppKit sends no key events while it moves a window, so + /// egui's modifiers can be stale then. + fn command_held(self) -> Option { + match self { + Self::Native => system_command_held(), + #[cfg(any(test, feature = "testing"))] + Self::Host { .. } => None, + } + } + + /// Whether it moves a native window for a bar drag (`StartDrag`), and tells the cursor + /// and the button meanwhile: AppKit does, smoothly, while a window moved from the app by + /// `OuterPosition` jitters there. + fn moves_windows(self) -> bool { + match self { + Self::Native => cfg!(target_os = "macos"), + #[cfg(any(test, feature = "testing"))] + Self::Host { moves, .. } => moves, + } + } } /// AppKit's cursor, in the desktop points egui reports window positions in. AppKit measures @@ -170,6 +209,29 @@ fn system_cursor(_: &Context) -> Option { None } +/// AppKit's primary button state, current when egui's lags. +#[cfg(target_os = "macos")] +fn system_primary_held() -> Option { + Some(objc2_app_kit::NSEvent::pressedMouseButtons() & 1 != 0) +} + +#[cfg(not(target_os = "macos"))] +fn system_primary_held() -> Option { + None +} + +/// AppKit's Ctrl and Cmd state, current while it sends egui no key events. +#[cfg(target_os = "macos")] +fn system_command_held() -> Option { + use objc2_app_kit::{NSEvent, NSEventModifierFlags}; + Some(NSEvent::modifierFlags_class().intersects(NSEventModifierFlags::Command | NSEventModifierFlags::Control)) +} + +#[cfg(not(target_os = "macos"))] +fn system_command_held() -> Option { + None +} + /// Whether native windows outline their ×'s hit area, and on macOS AppKit's resize areas: /// `CRAFT_DOCK_DEBUG_CLOSE=1`, read once. fn debug_close() -> bool { @@ -184,13 +246,19 @@ pub(in crate::docking) struct WindowDrag { pub window: u64, /// Where the pointer pressed, in the window's local points. pub grab: Vec2, - /// Desktop position of the window's content as last commanded. + /// Desktop position of the window's content as last commanded, or under `os`, the + /// cursor less `grab`. pub to: Pos2, /// Desktop position of the window's content when the drag began, for Escape. pub start: Pos2, /// The drag began on the window's only tab or group, so it docks whatever /// [`crate::docking::DockStyle::title_bar_docks`] says. pub docks: bool, + /// The window system moves the window ([`WindowSystem::moves_windows`]). + pub os: bool, + /// The cursor has gone past the click distance from the press. Under `os` the drag starts + /// on the press, and until then it is a click, which docks nothing. + pub moved: bool, } /// What the dock area knows of a shown native window's place, in the context's data under @@ -728,7 +796,14 @@ impl Renderer<'_, P> { if response.close.as_ref().is_some_and(|close| close.clicked()) { self.output.actions.push(Action::CloseWindow { panel: first }); } - if movable && response.bar.drag_started() { + // Where the window system moves the window, it takes the press, as a title bar's; + // a drag that egui would only start past the click distance would lose the way there. + let start = if self.window_system.moves_windows() { + response.bar.is_pointer_button_down_on() && ui.input(|i| i.pointer.primary_pressed()) + } else { + response.bar.drag_started() + }; + if movable && start { self.start_window_drag(ui, window.id, false); } response.close.iter().chain(&response.menu).map(|r| r.rect).collect() @@ -753,7 +828,7 @@ impl Renderer<'_, P> { } self.escape_elsewhere(ctx, Some(window.id)); let origin = ui.input(|i| content_of(i.viewport())).map(|(origin, _)| origin); - if self.follow_window_drag(ui, window.id) { + if let Some(origin) = self.follow_window_drag(ui, window.id, origin) { return origin; } let owns = ctx.data(|d| d.get_temp::>(drag_key).map(|d| d.panel).or(d.get_temp::>(group_key).map(|d| d.panel))); @@ -765,50 +840,56 @@ impl Renderer<'_, P> { } /// Start moving the window with the pointer, from the position the window system reports, - /// or, where it reports none (Wayland), by handing the move to it. `docks`: the drag began - /// on the window's only tab or group. + /// or by handing the move to the window system: where it moves windows smoothly (macOS), + /// tracking the drag, and where it reports no positions (Wayland), leaving it alone. `docks`: + /// the drag began on the window's only tab or group. fn start_window_drag(&mut self, ui: &Ui, window: u64, docks: bool) { - let press = ui.input(|i| i.pointer.press_origin().or(i.pointer.latest_pos())); + let ctx = ui.ctx(); + let key = self.id.with("window-drag"); + if ctx.data(|d| d.get_temp::(key)).is_some_and(|d| d.window == window) { + return; + } + let os = self.window_system.moves_windows(); + // The window system keeps the window where it is under the pointer now. + let press = + ui.input(|i| if os { i.pointer.latest_pos().or(i.pointer.press_origin()) } else { i.pointer.press_origin().or(i.pointer.latest_pos()) }); match (ui.input(|i| content_of(i.viewport())), press) { (Some((origin, _)), Some(press)) => { - let drag = WindowDrag { window, grab: press.to_vec2(), to: origin, start: origin, docks }; - ui.ctx().data_mut(|d| d.insert_temp(self.id.with("window-drag"), drag)); + let drag = WindowDrag { window, grab: press.to_vec2(), to: origin, start: origin, docks, os, moved: !os || docks }; + ctx.data_mut(|d| d.insert_temp(key, drag)); + if os { + ctx.send_viewport_cmd(ViewportCommand::StartDrag); + } } - _ => ui.ctx().send_viewport_cmd(ViewportCommand::StartDrag), + _ => ctx.send_viewport_cmd(ViewportCommand::StartDrag), } } /// Move the window by `OuterPosition` while the button is held, so the pressed point stays - /// under the pointer. Escape puts it back; losing focus leaves it. Returns whether the - /// window is moving. - fn follow_window_drag(&mut self, ui: &Ui, window: u64) -> bool { + /// under the pointer. Escape puts it back; losing focus leaves it. Where the window system + /// moves the window, follow it instead ([`Self::follow_os_drag`]). Returns the desktop + /// position of the window's content while it moves, `origin` as reported or where it is. + fn follow_window_drag(&mut self, ui: &Ui, window: u64, origin: Option) -> Option> { let key = self.id.with("window-drag"); - let Some(mut drag) = ui.ctx().data(|d| d.get_temp::(key)).filter(|d| d.window == window) else { return false }; + let mut drag = ui.ctx().data(|d| d.get_temp::(key)).filter(|d| d.window == window)?; + if drag.os { + return Some(self.follow_os_drag(ui, drag, origin)); + } let content = ui.input(|i| content_of(i.viewport())); let frame = content.map_or(Vec2::ZERO, |(_, frame)| frame); - // Where the window system tells the cursor on the desktop (macOS), the window goes - // there: AppKit reports the window's place as soon as a move is commanded, but measures - // the pointer events it sends against where the window server had the window, which - // can be a move behind. Elsewhere a pointer move of this frame is relative to where the - // window was when the window system sent it, which is where it reports the window now: - // a move commanded earlier may not have landed yet. Without one the window stays, since - // the last position egui keeps (button events included) was measured against an older - // place. - let to = match self.cursor.at(ui.ctx()) { - Some(cursor) => Some(cursor - drag.grab), - None => { - let moved = ui.input(|i| i.events.iter().rev().find_map(|e| if let Event::PointerMoved(p) = e { Some(*p) } else { None })); - content.zip(moved.filter(|p| p.is_finite())).map(|((origin, _), local)| origin + (local.to_vec2() - drag.grab)) - } - }; + // A pointer move of this frame is relative to where the window was when the window + // system sent it, which is where it reports the window now: a move commanded earlier + // may not have landed yet. Without one the window stays, since the last position egui + // keeps (button events included) was measured against an older place. + let moved = ui.input(|i| i.events.iter().rev().find_map(|e| if let Event::PointerMoved(p) = e { Some(*p) } else { None })); + let to = content.zip(moved.filter(|p| p.is_finite())).map(|((origin, _), local)| origin + (local.to_vec2() - drag.grab)); let mut step = None; if let Some(to) = to.filter(|to| to.distance(drag.to) > 0.0) { (drag.to, step) = (to, Some(to)); ui.ctx().data_mut(|d| d.insert_temp(key, drag)); } // The pointer on the desktop is the pressed point of the window where it was last - // put (the cursor, where the window system tells it), so the origin is that place, - // less the local pointer. Targets are hit there. + // put, so the origin is that place, less the local pointer. Targets are hit there. let local = ui.input(|i| i.pointer.latest_pos()).filter(|p| p.is_finite()); let origin = local.map(|local| drag.to + drag.grab - local.to_vec2()).or(content.map(|(origin, _)| origin)); let pointer = self.record_pointer(ui.ctx(), Some(window), origin, Some(&drag)); @@ -822,7 +903,7 @@ impl Renderer<'_, P> { ui.input_mut(|i| i.consume_key(Modifiers::NONE, Key::Escape)); } ui.ctx().data_mut(|d| d.remove::(key)); - return true; + return Some(origin); } if let Some(to) = step { move_to(self, to); @@ -838,7 +919,66 @@ impl Renderer<'_, P> { } self.end_window_drag(drag, pointer); } - true + Some(origin) + } + + /// A bar drag that the window system carries out (`StartDrag`): the window stays under the + /// cursor at the pressed point, so it is where the cursor is less the grab. Its reported + /// place lags the move, and AppKit tells the cursor fresh each frame. The drag ends on the + /// first frame on which the window system or egui reports the release, and then once. + /// Escape does not end it, as with Finder's windows: AppKit sends no keys meanwhile. + /// Returns where the window's content is. + fn follow_os_drag(&mut self, ui: &Ui, mut drag: WindowDrag, origin: Option) -> Option { + let ctx = ui.ctx(); + let key = self.id.with("window-drag"); + let local = ui.input(|i| i.pointer.latest_pos()).filter(|p| p.is_finite()); + let cursor = self.window_system.cursor(ctx).or_else(|| Some(origin? + local?.to_vec2())); + if let Some(cursor) = cursor { + drag.to = cursor - drag.grab; + drag.moved |= drag.to.distance(drag.start) > ctx.options(|o| o.input_options.max_click_dist); + } + let (egui_down, released) = ui.input(|i| (i.pointer.primary_down(), i.pointer.primary_released())); + let ended = self.window_system.primary_held() == Some(false) || released || !egui_down; + let command = + self.window_system.command_held().unwrap_or_else(|| ui.input(|i| i.modifiers.command || i.modifiers.ctrl || i.modifiers.mac_cmd)); + if self.cross.focus_lost || ended { + ctx.data_mut(|d| d.remove::(key)); + } else { + ctx.data_mut(|d| d.insert_temp(key, drag)); + } + // A click on the bar is no drop, and the window's reports say where it is. + if !drag.moved { + return origin; + } + self.cross.moved = Some(drag.to); + if self.cross.focus_lost { + return Some(drag.to); + } + let pointer = DragPointer { + owner: Some(drag.window), + origin: Some(drag.to), + local: Some(drag.grab.to_pos2()), + down: !ended, + released: ended, + escape: false, + command, + window: Some(drag.window), + docks: drag.docks, + }; + self.write_pointer(ctx, pointer); + if ended { + // Left with too little of its bar on a monitor to grab it: back onto the nearest. + if let Some(size) = ui.input(|i| i.viewport().inner_rect.map(|r| r.size())) { + let [x, y, ..] = kept_reachable([drag.to.x, drag.to.y, size.x, size.y], &self.monitors); + if pos2(x, y).distance(drag.to) > 0.5 { + let frame = ui.input(|i| content_of(i.viewport())).map_or(Vec2::ZERO, |(_, frame)| frame); + ctx.send_viewport_cmd(ViewportCommand::OuterPosition(pos2(x, y) - frame)); + self.cross.moved = Some(pos2(x, y)); + } + } + self.end_window_drag(drag, pointer); + } + Some(drag.to) } /// Release of a bar drag. The window already sits where it was dragged; over a target in diff --git a/crates/ui/src/docking/surface.rs b/crates/ui/src/docking/surface.rs index 79d0666c..ec3cc049 100644 --- a/crates/ui/src/docking/surface.rs +++ b/crates/ui/src/docking/surface.rs @@ -1014,7 +1014,7 @@ pub struct DockArea<'a, P = ()> { #[cfg(feature = "native-windows")] input_hook: Option, #[cfg(feature = "native-windows")] - cursor: super::render::DesktopCursor, + window_system: super::render::WindowSystem, } impl<'a, P: Clone + Eq + Hash + Debug + Send + Sync + 'static> DockArea<'a, P> { @@ -1034,7 +1034,7 @@ impl<'a, P: Clone + Eq + Hash + Debug + Send + Sync + 'static> DockArea<'a, P> { #[cfg(feature = "native-windows")] input_hook: None, #[cfg(feature = "native-windows")] - cursor: Default::default(), + window_system: Default::default(), } } @@ -1092,10 +1092,10 @@ impl<'a, P: Clone + Eq + Hash + Debug + Send + Sync + 'static> DockArea<'a, P> { self } - /// Where native windows that follow a bar drag go: a stand-in window system's cursor. + /// A stand-in for the window system that moves native windows and tells the cursor. #[cfg(all(feature = "native-windows", any(test, feature = "testing")))] - pub(in crate::docking) fn desktop_cursor(mut self, cursor: super::render::DesktopCursor) -> Self { - self.cursor = cursor; + pub(in crate::docking) fn window_system(mut self, window_system: super::render::WindowSystem) -> Self { + self.window_system = window_system; self } @@ -1276,7 +1276,7 @@ impl<'a, P: Clone + Eq + Hash + Debug + Send + Sync + 'static> DockArea<'a, P> { #[cfg(feature = "native-windows")] cross: Default::default(), #[cfg(feature = "native-windows")] - cursor: self.cursor, + window_system: self.window_system, }; if let Some(root) = &view.root { renderer.node(ui, root, rect, &mut Vec::new(), (root, rect)); diff --git a/crates/ui/src/docking/testing.rs b/crates/ui/src/docking/testing.rs index 1fd4227b..7118221c 100644 --- a/crates/ui/src/docking/testing.rs +++ b/crates/ui/src/docking/testing.rs @@ -133,6 +133,7 @@ impl> Do let mut host = host.borrow_mut(); (host.time, host.screen, host.modifiers) = (self.time, self.screen, self.modifiers); host.frames += 1; + host.read_pointer(); input.viewports = host.infos(); input.focused = host.focus == Some(egui::ViewportId::ROOT); } @@ -141,7 +142,7 @@ impl> Do #[cfg(feature = "native-windows")] let input_hook = self.input_hook.clone(); #[cfg(feature = "native-windows")] - let cursor = self.host.as_ref().and_then(|host| host.borrow().cursor); + let window_system = self.host.as_ref().map_or(Default::default(), |host| host.borrow().window_system()); let (style_fn, maximized, raise, auto, bounds, locked, native, blocked) = ( &self.style, self.maximized.clone(), @@ -173,7 +174,7 @@ impl> Do } #[cfg(feature = "native-windows")] { - dock = dock.desktop_cursor(super::render::DesktopCursor::Fixed(cursor)); + dock = dock.window_system(window_system); } #[cfg(feature = "native-windows")] if let Some(hook) = input_hook.clone() { @@ -256,20 +257,19 @@ impl> Do } /// Move the pointer to the desktop point `point` in the native window of `window`, and - /// step. The window gets the point in its coordinates, measured against its place - /// [`NativeHost::stale`] picks; with [`NativeHost::tells_cursor`] the window system also - /// tells the point as the cursor's. + /// step. The window gets the point in its coordinates. While the window system moves the + /// window ([`NativeHost::moves_windows`]), the window goes where the pointer keeps its grab. #[cfg(feature = "native-windows")] pub fn window_pointer(&mut self, window: u64, point: Pos2) -> DockOutput

{ let viewport = self.viewport(window); { let mut host = self.host(); - let behind = host.pointer_moves.checked_rem(host.stale.len()).and_then(|i| host.stale.get(i)).copied().unwrap_or(0); - host.pointer_moves += 1; - host.cursor = host.tells_cursor.then_some(point); + let grab = host.os_drag.filter(|(dragged, _)| *dragged == viewport).map(|(_, grab)| grab); if let Some(target) = host.windows.get_mut(&viewport) { - let place = target.places.iter().rev().nth(behind).or(target.places.first()).copied().unwrap_or(target.rect.min); - target.events.push(Event::PointerMoved(point - place.to_vec2())); + if let Some(grab) = grab { + target.rect = Rect::from_min_size(point - grab, target.rect.size()); + } + target.events.push(Event::PointerMoved(point - target.rect.min.to_vec2())); } } self.step(Vec::new()) @@ -426,21 +426,22 @@ pub struct NativeHost { /// `OuterPosition` lands a frame late, at the end of the frame after the command. That /// frame still reports the old place, and gets pointer events relative to it. pub lag: bool, - /// The window system tells where the cursor is on the desktop, as AppKit does: where - /// [`DockDriver::window_pointer`] last put it. - pub tells_cursor: bool, - /// [`DockDriver::window_pointer`] measures a window's pointer events against its place - /// this many frames back, one entry per call in turn, as AppKit does while the window - /// server catches up with a move it reported at once. Empty: its place now. - pub stale: Vec, + /// The window system moves a window for `StartDrag` itself, as AppKit does: the window + /// keeps the pointer where it was in it, and reports its place as where the drag began until + /// the release. Meanwhile it tells the cursor and [`Self::held`]. + pub moves_windows: bool, + /// The primary button is down, as the window system tells it with + /// [`Self::moves_windows`]: set by the presses and releases given to windows, and by a test + /// that has the window system report a release before egui does. + pub held: bool, pub windows: std::collections::HashMap, /// Every command sent to a native window or the main window, in order. pub commands: Vec<(egui::ViewportId, egui::ViewportCommand)>, /// Moves waiting to land under `lag`: window, content position, frame sent. late: Vec<(egui::ViewportId, Pos2, usize)>, cursor: Option, - /// Calls of [`DockDriver::window_pointer`], for [`Self::stale`]. - pointer_moves: usize, + /// The window it moves for `StartDrag`, and where the pointer is in it. + os_drag: Option<(egui::ViewportId, egui::Vec2)>, frames: usize, /// The driver's modifiers: the keyboard's, whichever window has focus. modifiers: Modifiers, @@ -470,8 +471,10 @@ pub struct HostWindow { pub dropped_files: Vec, /// Shapes of its last frame. pub shapes: Vec, - /// Desktop positions of its content at the end of recent frames, latest last. - places: Vec, + /// Where it reports its content while the window system moves it. + reported: Option, + /// The last pointer position it was given, in its coordinates. + pointer: Option, } #[cfg(feature = "native-windows")] @@ -484,13 +487,13 @@ impl NativeHost { no_position: false, frame: egui::Vec2::ZERO, lag: false, - tells_cursor: false, - stale: Vec::new(), + moves_windows: false, + held: false, windows: Default::default(), commands: Vec::new(), late: Vec::new(), cursor: None, - pointer_moves: 0, + os_drag: None, frames: 0, modifiers: Modifiers::NONE, screen: Rect::NOTHING, @@ -511,10 +514,41 @@ impl NativeHost { }; let main = Rect::from_min_size(self.origin, self.screen.size()); std::iter::once((egui::ViewportId::ROOT, info(egui::ViewportId::ROOT, main))) - .chain(self.windows.iter().map(|(id, window)| (*id, info(*id, window.rect)))) + .chain( + self.windows + .iter() + .map(|(id, window)| (*id, info(*id, Rect::from_min_size(window.reported.unwrap_or(window.rect.min), window.rect.size())))), + ) .collect() } + /// What it tells the dock area. + fn window_system(&self) -> super::render::WindowSystem { + let moves = self.moves_windows; + super::render::WindowSystem::Host { cursor: self.cursor, held: moves.then_some(self.held), moves } + } + + /// With [`Self::moves_windows`], the cursor and the button as the pointer events waiting + /// for each window leave them. + fn read_pointer(&mut self) { + if !self.moves_windows { + return; + } + for window in self.windows.values_mut() { + for event in &window.events { + match event { + Event::PointerMoved(pos) => window.pointer = Some(*pos), + Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, .. } => { + window.pointer = Some(*pos); + self.held = *pressed; + } + _ => continue, + } + self.cursor = window.pointer.map(|p| window.rect.min + p.to_vec2()); + } + } + } + /// Run one native window's frame. fn render(host: &std::cell::RefCell, ctx: &Context, mut viewport: egui::ImmediateViewport<'_>) { let id = viewport.ids.this; @@ -534,7 +568,8 @@ impl NativeHost { hovered_files: Vec::new(), dropped_files: Vec::new(), shapes: Vec::new(), - places: Vec::new(), + reported: None, + pointer: None, }); let mut viewports = host.infos(); let (time, focused, modifiers) = (host.time, host.focus == Some(id), host.modifiers); @@ -567,10 +602,9 @@ impl NativeHost { } } - /// End a frame: moves sent in an earlier one land, and each window keeps its place. + /// End a frame: moves sent in an earlier one land, and a move the window system carries out + /// ends once the button is up. fn land(&mut self) { - /// How many frames back [`Self::stale`] can reach. - const PLACES: usize = 8; let frames = self.frames; let (landed, late) = std::mem::take(&mut self.late).into_iter().partition::, _>(|(.., sent)| *sent < frames); self.late = late; @@ -579,10 +613,11 @@ impl NativeHost { window.rect = Rect::from_min_size(pos, window.rect.size()); } } - for window in self.windows.values_mut() { - window.places.push(window.rect.min); - let excess = window.places.len().saturating_sub(PLACES); - window.places.drain(..excess); + if !self.held + && let Some((id, _)) = self.os_drag.take() + && let Some(window) = self.windows.get_mut(&id) + { + window.reported = None; } } @@ -597,6 +632,11 @@ impl NativeHost { egui::ViewportCommand::OuterPosition(pos) => window.rect = Rect::from_min_size(*pos + self.frame, window.rect.size()), egui::ViewportCommand::InnerSize(size) => window.rect = Rect::from_min_size(window.rect.min, *size), egui::ViewportCommand::Visible(visible) => window.visible = *visible, + egui::ViewportCommand::StartDrag if self.moves_windows && self.held => { + let grab = window.pointer.unwrap_or_default().to_vec2(); + window.reported = Some(window.rect.min); + self.os_drag = Some((*id, grab)); + } _ => {} } } diff --git a/docs/ui-docking.md b/docs/ui-docking.md index 7a08ad25..7fb944ab 100644 --- a/docs/ui-docking.md +++ b/docs/ui-docking.md @@ -363,7 +363,9 @@ A flyout fits its content the same way, without the title bar. a moved window or adjust a split during the drag. Nothing is restored from before the drag; windows go where the new layout has them. `cancel_drag` ends the area's drags at any other time. A native window moved by the compositor (`ViewportCommand::StartDrag`, as on Wayland) - is not the area's drag: it ends when the user releases the button. + is not the area's drag: it ends when the user releases the button. On macOS, where AppKit + moves a native window by its bar, a cancel ends the area's drag, so the release drops + nothing, but AppKit keeps moving the window until the release. - 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 @@ -506,7 +508,8 @@ windows from the first frame. `DockArea::native_windows(bool)` overrides the cho app's other windows. - **Bar.** Each window has a bar at the top, 18 points high, in the floating title bar's style: the title (the shown panel of its first group) and ×. Dragging the bar moves the window. On - Wayland, where a window cannot be placed, the bar hands the move to the compositor. On + macOS the press on the bar hands the move to AppKit, and on Wayland, where a window cannot + be placed, the bar hands the move to the compositor. On Windows and Linux the OS frame is off. On macOS the frame stays, with its title bar and buttons hidden, for its resizing, rounded corners and standard window shadow. The dock area paints no shadow of its own around a native window. @@ -614,10 +617,12 @@ windows from the first frame. `DockArea::native_windows(bool)` overrides the cho the pointer's moves and the release while the button is held, inside the window or not. So the pointer leaving a window (`PointerGone`) is not a release. A drag ends on release, Escape (pressed in any of the app's windows, since keys go to the one with keyboard focus), - when none of the app's windows has focus, or when its window closes or its panel goes. + when none of the app's windows has focus, or when its window closes or its panel goes. A + window that AppKit moves (macOS, below) ignores Escape. - *Hit-testing.* The owner adds its desktop origin to the pointer: a native window's reported `inner_rect.min`, or while it moves by its bar, the place it was last put less the pointer's - offset from the pressed point. The window under that + offset from the pressed point. On macOS a window moving by its bar is hit-tested at + AppKit's cursor (`NSEvent::mouseLocation`). The window under that desktop point is a floating native window in front of the main window first (palettes, then normal windows, most recently focused first), then the main window, then a normal window behind it. A window moving by its bar is never its own target. A point in no window is on the @@ -632,26 +637,35 @@ windows from the first frame. `DockArea::native_windows(bool)` overrides the cho (the window's top-left is the pointer minus the grab offset, less the bar's height). A Ctrl/Cmd-drag floats there wherever it is released. - *Moving a window.* Dragging the bar, the only tab of a window, or the drag area of its only - group moves the window, so the pressed point stays under the pointer. On macOS each frame - puts the window at the cursor's desktop position (AppKit's `NSEvent::mouseLocation`) less - the pressed point, and targets are hit at that position. AppKit reports a window's new place - as soon as a move is commanded, but measures the pointer events it sends against where the - window server had the window, which can be a move behind; placing the window from those - events would overshoot and step back. Elsewhere each move is worked out from a pointer move - of that frame and the window's reported place, which the pointer's position is relative to; - a move commanded earlier may land late. A frame with no pointer move leaves the window. X11 - sends no move whose position in the window repeats the last one, so while the pointer keeps - exact pace with the window the window can trail it by one move until the next. Released over - a target in another window, the whole window docks or - joins there as its groups (`Action::MoveWindow`). The bar docks only with - `DockStyle::title_bar_docks`; the only tab or group docks either way, since it is the way - back in with that flag off. Targets: in the - main window a dock line, a frame's tab row or edge zone, or the layout's outer edge; in a - floating window a frame zone or tab row. A frame body's center takes a window moved by its - only tab or group, as that tab or group drag would, but not one moved by its bar. Over - the desktop, or with Ctrl/Cmd held, it stays where it was moved, and the layout takes its - place once, at the release. Escape puts it back. An - in-window floating window's title bar still splits only groups of other floating windows. + group moves the window, so the pressed point stays under the pointer. + - *macOS.* AppKit moves the window (`ViewportCommand::StartDrag`, which winit turns into + `performWindowDragWithEvent`), as it moves its own windows: from the press on the bar, + or from the start of the tab or group drag. The dock area sends no `OuterPosition` + meanwhile. egui keeps running frames, but the window's reported place lags the move by up + to a tenth of a second, so each frame takes the window's place as AppKit's cursor + (`NSEvent::mouseLocation`, flipped against the primary screen's height and divided by the + zoom factor) less the pressed point, and hit-tests targets, fades the window and routes + the drag at the cursor. The drag ends on the first frame where AppKit + (`NSEvent::pressedMouseButtons`) or egui reports the release; egui's can come a frame + later, and the drag is resolved once. Ctrl/Cmd is read from AppKit's modifier flags, + since AppKit sends egui no key events during the move; for the same reason Escape does + not cancel it, as with Finder's windows. A press on the bar released within the click + distance docks nothing. + - *Elsewhere.* The dock area moves the window with `OuterPosition`. Each move is worked + out from a pointer move of that frame and the window's reported place, which the + pointer's position is relative to; a move commanded earlier may land late. A frame with + no pointer move leaves the window. X11 sends no move whose position in the window repeats + the last one, so while the pointer keeps exact pace with the window the window can trail + it by one move until the next. Escape puts the window back. + - *Drops.* Released over a target in another window, the whole window docks or joins there + as its groups (`Action::MoveWindow`). The bar docks only with + `DockStyle::title_bar_docks`; the only tab or group docks either way, since it is the way + back in with that flag off. Targets: in the main window a dock line, a frame's tab row or + edge zone, or the layout's outer edge; in a floating window a frame zone or tab row. A + frame body's center takes a window moved by its only tab or group, as that tab or group + drag would, but not one moved by its bar. Over the desktop, or with Ctrl/Cmd held, it + stays where it was moved, and the layout takes its place once, at the release. An + in-window floating window's title bar still splits only groups of other floating windows. - *Wayland.* With `DockArea::native_windows(true)` on Wayland, or wherever `outer_rect` is `None`, desktop positions are unknown, so drops across windows cannot be resolved. A drag works inside its own window, and a release outside it floats a new window that the @@ -743,9 +757,10 @@ runs its own frame with its own input and desktop rect (`host()`, `window_step`, `window_click`). `NativeHost` can report a monitor size (`monitor_size`), give windows a frame between their outer position and content (`frame`), land `OuterPosition` a frame late (`lag`), and report no positions (`no_position`, as on Wayland). `window_pointer` moves the pointer to a -desktop point in a native window; the host can tell that point as the cursor's (`tells_cursor`, -as AppKit does) and measure the window's pointer events against its place some frames back -(`stale`, a pattern taken one entry per call). A `HostWindow` takes files dragged +desktop point in a native window. With `moves_windows` the host moves a window for +`StartDrag` itself, as AppKit does: the window keeps the pointer at the pressed point, reports +its place as where the drag began until the release, and the host tells the cursor and the +button (`held`; a test sets it to `false` to report a release before egui does). A `HostWindow` takes files dragged over it (`hovered_files`) or dropped on it (`dropped_files`), and `with_native_input_hook` passes a hook to `DockArea::native_input_hook` every frame.