docking: blocking input hands the keyboard to the main window (#306)

* docking: blocking input hands the keyboard from a native window to the main window

* docking: the keyboard goes to the main window only if a native window still has it after a short wait
This commit is contained in:
aiongg authored and GitHub committed 2026-10-24 22:10:52 -04:00
1 parent 869928d2a0
commit e95bc0d2a2
4 files changed
+87 -3

No files matched your search

+44
View File
@@ -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<u32, Content>| {
d.host().commands.iter().filter(|(v, c)| *v == egui::ViewportId::ROOT && matches!(c, ViewportCommand::Focus)).count()
};
let steps = |d: &mut DockDriver<u32, Content>, 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.
+33
View File
@@ -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<P>(ctx: &Context, area: Id, layout: &Layout<P>, blocked: bool, starts: bool) {
let (key, now) = (area.with("blocked-focus"), ctx.input(|i| i.time));
if !blocked {
ctx.data_mut(|d| d.remove::<f64>(key));
return;
}
if starts {
ctx.data_mut(|d| d.insert_temp(key, now));
}
let Some(since) = ctx.data(|d| d.get_temp::<f64>(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::<f64>(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 {
+4 -1
View File
@@ -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::<bool>(blocked_key).unwrap_or(false)) {
let starts = self.input_blocked && !ui.ctx().data(|d| d.get_temp::<bool>(blocked_key).unwrap_or(false));
if starts {
cancel_drag::<P>(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);
+6 -2
View File
@@ -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.