mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-15 15:02:12 +00:00
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:
1 parent
869928d2a0
commit
e95bc0d2a2
4 files changed
+87
-3
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user