diff --git a/crates/ui/src/docking/native_tests.rs b/crates/ui/src/docking/native_tests.rs index 388bb445d..5fd79bdc7 100644 --- a/crates/ui/src/docking/native_tests.rs +++ b/crates/ui/src/docking/native_tests.rs @@ -1710,6 +1710,50 @@ fn a_focused_field_and_a_held_press_stay_inert_across_a_block() { assert_eq!(d.content.count, 0); } +#[test] +fn blocking_input_hands_focus_from_a_native_window_to_the_main_window() { + let focus_main = |d: &DockDriver| { + d.host().commands.iter().filter(|(v, c)| *v == egui::ViewportId::ROOT && matches!(c, ViewportCommand::Focus)).count() + }; + let steps = |d: &mut DockDriver, n: usize| { + for _ in 0..n { + d.step(vec![]); + } + }; + let focused_float = || { + let mut d = blocked_driver(); + let viewport = d.viewport(id_of(&d)); + d.host().focus = Some(viewport); + d.step(vec![]); + d.input_blocked = true; + d + }; + // A dialog opened from a floating panel needs the keys there, in the main window: once, + // after a short wait. + let mut d = focused_float(); + steps(&mut d, 2); + assert_eq!(focus_main(&d), 0, "not yet: an OS dialog may still take focus"); + steps(&mut d, 30); + assert_eq!(focus_main(&d), 1); + // An OS dialog took focus meanwhile: no window of the area has it, so nothing is sent. + let mut d = focused_float(); + d.step(vec![]); + d.host().focus = None; + steps(&mut d, 30); + assert_eq!(focus_main(&d), 0); + // Blocking ended before the wait was over. + let mut d = focused_float(); + d.step(vec![]); + d.input_blocked = false; + steps(&mut d, 30); + assert_eq!(focus_main(&d), 0); + // With the main window focused already, nothing is sent. + let mut d = blocked_driver(); + d.input_blocked = true; + steps(&mut d, 30); + assert_eq!(focus_main(&d), 0); +} + #[test] fn blocking_input_cancels_a_drag_in_progress() { // A tab drag a native window owns. diff --git a/crates/ui/src/docking/render/mod.rs b/crates/ui/src/docking/render/mod.rs index 8c433ac55..b669502c7 100644 --- a/crates/ui/src/docking/render/mod.rs +++ b/crates/ui/src/docking/render/mod.rs @@ -31,6 +31,39 @@ pub(in crate::docking) use body_drag::{cancel_body_route, mark_app_drag}; #[cfg(feature = "native-windows")] pub(in crate::docking) use native::{InputHook, set_input_hook}; +/// 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 +/// later, and a Focus sent before then could raise the main window over it. +#[cfg(feature = "native-windows")] +const BLOCKED_FOCUS_WAIT: f64 = 0.2; + +/// While input is blocked (`starts`: from this frame), a native window of the dock area `area` +/// with the keyboard would drop the keys the modal needs, so once, after +/// [`BLOCKED_FOCUS_WAIT`], the main window, where the modal is, takes the keyboard if one still +/// has it. If focus went elsewhere meanwhile, such as to an OS dialog, nothing is sent. +#[cfg(feature = "native-windows")] +pub(in crate::docking) fn focus_main_from_native

(ctx: &Context, area: Id, layout: &Layout

, blocked: bool, starts: bool) { + let (key, now) = (area.with("blocked-focus"), ctx.input(|i| i.time)); + if !blocked { + ctx.data_mut(|d| d.remove::(key)); + return; + } + if starts { + ctx.data_mut(|d| d.insert_temp(key, now)); + } + let Some(since) = ctx.data(|d| d.get_temp::(key)) else { return }; + if now - since < BLOCKED_FOCUS_WAIT { + ctx.request_repaint_after(std::time::Duration::from_secs_f64(BLOCKED_FOCUS_WAIT - (now - since))); + return; + } + ctx.data_mut(|d| d.remove::(key)); + let focused = + ctx.input(|i| layout.floating.iter().any(|w| i.raw.viewports.get(&native_viewport(area, w.id)).and_then(|v| v.focused) == Some(true))); + if focused { + ctx.send_viewport_cmd_to(egui::ViewportId::ROOT, egui::ViewportCommand::Focus); + } +} + /// The viewport of the native window `window` of the dock area `area`. #[cfg(feature = "native-windows")] pub(in crate::docking) fn native_viewport(area: Id, window: u64) -> egui::ViewportId { diff --git a/crates/ui/src/docking/surface.rs b/crates/ui/src/docking/surface.rs index 77fecb91f..c1af45d24 100644 --- a/crates/ui/src/docking/surface.rs +++ b/crates/ui/src/docking/surface.rs @@ -1136,9 +1136,12 @@ impl<'a, P: Clone + Eq + Hash + Debug + Send + Sync + 'static> DockArea<'a, P> { // Blocking starts while the pointer may be down: a drag that went on would act on its // release, under the modal. let blocked_key = self.id.with("input-blocked"); - if self.input_blocked && !ui.ctx().data(|d| d.get_temp::(blocked_key).unwrap_or(false)) { + let starts = self.input_blocked && !ui.ctx().data(|d| d.get_temp::(blocked_key).unwrap_or(false)); + if starts { cancel_drag::

(ui.ctx(), self.id); } + #[cfg(feature = "native-windows")] + super::render::focus_main_from_native(ui.ctx(), self.id, layout, self.input_blocked, starts); ui.ctx().data_mut(|d| d.insert_temp(blocked_key, self.input_blocked)); cancel_drag_if_replaced(ui.ctx(), self.id, layout); release_moved_tab_focus(ui.ctx(), self.id, layout); diff --git a/docs/ui-docking.md b/docs/ui-docking.md index c27f2291d..9123eaece 100644 --- a/docs/ui-docking.md +++ b/docs/ui-docking.md @@ -491,8 +491,12 @@ windows from the first frame. `DockArea::native_windows(bool)` overrides the cho main window is raised last, in front of the normal windows and of a palette the user clicked. - *Edges.* Turning blocking on cancels a drag in progress, and a widget of a native window - loses keyboard focus. A zoom gesture still shows in egui's `InputState::zoom_delta`, which - cannot be cleared; a widget that zooms only while hovered ignores it. egui smooths wheel + loses keyboard focus. If a native window has the OS keyboard focus then (the modal was + opened from it, say by a double-click in a floating panel) and still has it 0.2 s later, + the main window gets `ViewportCommand::Focus`, so the modal gets the keys. The wait lets an + OS dialog opened with the modal (a file dialog) take focus first; then nothing is sent. A + zoom gesture still shows in egui's `InputState::zoom_delta`, which cannot be cleared; a + widget that zooms only while hovered ignores it. egui smooths wheel scrolling, so a scroll area in a native window may still scroll for about 100 ms after blocking ends, with the remainder of a wheel turn made just before it began. - Floating windows drawn in the main window need nothing: the modal covers them.