diff options
Diffstat (limited to 'crates/wayland/src')
| -rw-r--r-- | crates/wayland/src/input.rs | 64 | ||||
| -rw-r--r-- | crates/wayland/src/state/mod.rs | 29 | ||||
| -rw-r--r-- | crates/wayland/src/udev/platform.rs | 2 | ||||
| -rw-r--r-- | crates/wayland/src/winit/connect.rs | 2 |
4 files changed, 76 insertions, 21 deletions
diff --git a/crates/wayland/src/input.rs b/crates/wayland/src/input.rs index 2463d02..5cce9a4 100644 --- a/crates/wayland/src/input.rs +++ b/crates/wayland/src/input.rs @@ -213,7 +213,11 @@ fn notify_idle_activity(state: &mut CompState) { /// staleness trigger (rapid clicks, a tap-to-click event with no /// intervening motion delta, etc.) one cause at a time. #[allow(clippy::type_complexity)] -fn refresh_pointer_focus(state: &mut CompState, pos: Point<f64, Logical>, time: u32) -> (Option<(WindowId, TitlebarHit)>, bool, bool, Option<WindowId>) { +fn refresh_pointer_focus( + state: &mut CompState, + pos: Point<f64, Logical>, + time: u32, +) -> (Option<(WindowId, TitlebarHit)>, bool, bool, Option<WindowId>, Option<(WlSurface, Point<f64, Logical>)>) { // Checked before literally everything else, including layer-shell -- // see `elements::popup_surface_under`'s own doc comment for why: a // popup (tooltip, dropdown, right-click menu) always renders on top of @@ -245,17 +249,17 @@ fn refresh_pointer_focus(state: &mut CompState, pos: Point<f64, Logical>, time: .map(|(id, _)| id) .or_else(|| under.as_ref().and_then(|(window, _)| dwindow_wl_surface(window)).and_then(|s| state.surface_to_id.get(&s).copied())); - let Some(pointer) = state.seat.get_pointer() else { return (hit, over_layer_surface, over_content, hovered_id) }; - if let Some((surface, loc)) = popup_hit { - let surface_loc = pos - loc.to_f64(); - pointer.motion(state, Some((surface, loc.to_f64())), &MotionEvent { location: surface_loc, serial: SERIAL_COUNTER.next_serial(), time }); + let Some(pointer) = state.seat.get_pointer() else { return (hit, over_layer_surface, over_content, hovered_id, None) }; + // Freshly resolved target from ordinary hit-testing - overridden below + // by `pointer_button_grab` when a button is held, per its own doc + // comment (the Wayland implicit-grab rule). + let resolved: Option<(WlSurface, Point<f64, Logical>)> = if let Some((surface, loc)) = popup_hit { + Some((surface, loc.to_f64())) } else if let Some((surface, loc)) = layer_hit { - let surface_loc = pos - loc.to_f64(); - pointer.motion(state, Some((surface, loc.to_f64())), &MotionEvent { location: surface_loc, serial: SERIAL_COUNTER.next_serial(), time }); + Some((surface, loc.to_f64())) } else if hit.is_some() { - // Over our own decoration - no client focus. - pointer.motion(state, None, &MotionEvent { location: pos, serial: SERIAL_COUNTER.next_serial(), time }); - } else if let Some((window, loc)) = under { + None // Over our own decoration - no client focus. + } else if let Some((window, loc)) = &under { // `window.toplevel()` is only ever `Some` for a native xdg-shell // surface - it's `None` for every XWayland window, and even for a // plain xdg-shell one it's always the *root* surface regardless of @@ -269,21 +273,21 @@ fn refresh_pointer_focus(state: &mut CompState, pos: Point<f64, Logical>, time: // unifies the xdg-shell/X11 cases the way `dwindow_wl_surface` does // elsewhere in this module. let win_relative = pos - loc.to_f64(); - if let Some((surface, offset)) = window.surface_under(win_relative, WindowSurfaceType::ALL) { - let surface_loc = win_relative - offset.to_f64(); - let surface_origin = (loc + offset).to_f64(); - pointer.motion(state, Some((surface, surface_origin)), &MotionEvent { location: surface_loc, serial: SERIAL_COUNTER.next_serial(), time }); - } - } else if let Some((surface, loc)) = background_layer_surface_under(state, pos) { + window.surface_under(win_relative, WindowSurfaceType::ALL).map(|(surface, offset)| (surface, (*loc + offset).to_f64())) + } else { // Bare desktop, no window there either - last chance for a // `Bottom`/`Background` layer surface (see // `layer_surface_under_layers`'s doc comment) before giving up. - let surface_loc = pos - loc.to_f64(); - pointer.motion(state, Some((surface, loc.to_f64())), &MotionEvent { location: surface_loc, serial: SERIAL_COUNTER.next_serial(), time }); + background_layer_surface_under(state, pos).map(|(surface, loc)| (surface, loc.to_f64())) + }; + let delivery = state.pointer_button_grab.clone().or_else(|| resolved.clone()); + if let Some((surface, origin)) = delivery { + let surface_loc = pos - origin; + pointer.motion(state, Some((surface, origin)), &MotionEvent { location: surface_loc, serial: SERIAL_COUNTER.next_serial(), time }); } else { pointer.motion(state, None, &MotionEvent { location: pos, serial: SERIAL_COUNTER.next_serial(), time }); } - (hit, over_layer_surface, over_content, hovered_id) + (hit, over_layer_surface, over_content, hovered_id, resolved) } pub(crate) fn handle_pointer_position(state: &mut CompState, pos: Point<f64, Logical>, time: u32) { @@ -300,7 +304,7 @@ pub(crate) fn handle_pointer_position(state: &mut CompState, pos: Point<f64, Log return; } - let (hit, over_layer_surface, over_content, hovered_id) = refresh_pointer_focus(state, pos, time); + let (hit, over_layer_surface, over_content, hovered_id, _) = refresh_pointer_focus(state, pos, time); let Some(pointer) = state.seat.get_pointer() else { return }; // `PointerHandle::motion`/`button`/`axis` only queue the event with the // active grab - nothing sends `wl_pointer.frame` on its own (confirmed @@ -734,7 +738,22 @@ pub(crate) fn handle_pointer_button(state: &mut CompState, pos: Point<f64, Logic // A no-op from the client's perspective when focus was already correct // (an idempotent motion event at the same surface-local coordinates it // already has), so this costs nothing in the common case. - refresh_pointer_focus(state, pos, time); + // + // Also where `pointer_button_grab` starts and ends - see its own doc + // comment. Only the 0->1 transition captures a new grab target (a + // second button going down mid-gesture keeps whatever the first press + // already locked in); only the ->0 transition releases it, and not + // before this press/release's own `pointer.button()` below still goes + // out under the (still-active) grab. + let (.., resolved) = refresh_pointer_focus(state, pos, time); + if pressed { + if state.pointer_buttons_held == 0 { + state.pointer_button_grab = resolved; + } + state.pointer_buttons_held += 1; + } else { + state.pointer_buttons_held = state.pointer_buttons_held.saturating_sub(1); + } if let Some(pointer) = state.seat.get_pointer() { let button_state = if pressed { BackendButtonState::Pressed } else { BackendButtonState::Released }; pointer.button(state, &ButtonEvent { serial, time, button, state: button_state }); @@ -743,6 +762,9 @@ pub(crate) fn handle_pointer_button(state: &mut CompState, pos: Point<f64, Logic // `frame` does. pointer.frame(state); } + if !pressed && state.pointer_buttons_held == 0 { + state.pointer_button_grab = None; + } } /// Shared between the winit (nested) and udev (bare-TTY) backends: both diff --git a/crates/wayland/src/state/mod.rs b/crates/wayland/src/state/mod.rs index 5643ebc..084e407 100644 --- a/crates/wayland/src/state/mod.rs +++ b/crates/wayland/src/state/mod.rs @@ -475,6 +475,35 @@ pub(crate) struct CompState { /// event this session's earlier per-motion diagnostic-logging /// regression already proved is worth being careful around) needs one. pub(crate) last_idle_notify: Option<Instant>, + /// The Wayland-standard "implicit grab": once a pointer button goes + /// down over a surface, every subsequent motion/button event - no + /// matter where the pointer physically ends up - has to keep being + /// delivered to that *same* surface until every held button is + /// released, not whatever a fresh hit-test happens to land on next. + /// Nothing implemented this before: every motion event re-ran the same + /// popup/layer/content hit-test from scratch and called `pointer. + /// motion()` with whatever it found *right now*, so the moment a real + /// human's hand drifted even slightly outside the pressed surface's own + /// bounds mid-gesture (trivially easy during a fast drag - a mouse + /// does not move in a perfectly straight line), that client received an + /// unrequested `leave` in the middle of its own gesture. GTK's drag + /// recognizers (a `GtkHeaderBar`'s move-the-window gesture, concretely) + /// treat a mid-gesture `leave` as "this isn't coherent, abort" - which + /// reads as "dragging this window's title bar does nothing at all," + /// live-reproduced this session. `(surface, origin)` is captured once, + /// from the same resolution `refresh_pointer_focus` already computes, + /// the instant the held-button count goes from 0 to 1; `origin` is + /// reused for every event under the grab so surface-local coordinates + /// keep updating correctly even though the target surface no longer + /// does. + pub(crate) pointer_button_grab: Option<(WlSurface, Point<f64, Logical>)>, + /// How many pointer buttons are currently held - what actually decides + /// when [`Self::pointer_button_grab`] starts (0 -> 1) and ends (only + /// once every button, not just one of several held at once, comes back + /// up), matching the real Wayland implicit-grab rule instead of the + /// single-button assumption that would break as soon as a drag and a + /// second accidental button overlapped. + pub(crate) pointer_buttons_held: u32, /// Windows currently mid-tween - see `WindowAnim` and `sync_geometry`'s /// `anim_from` handling. Driven forward once per frame by /// `tick_animations`, called from both backends' poll loops. diff --git a/crates/wayland/src/udev/platform.rs b/crates/wayland/src/udev/platform.rs index 14dd677..88c0d8f 100644 --- a/crates/wayland/src/udev/platform.rs +++ b/crates/wayland/src/udev/platform.rs @@ -165,6 +165,8 @@ impl UdevPlatform { _idle_inhibit_manager_state: smithay::wayland::idle_inhibit::IdleInhibitManagerState::new::<CompState>(&display_handle), idle_inhibiting_surfaces: Vec::new(), last_idle_notify: None, + pointer_button_grab: None, + pointer_buttons_held: 0, window_anims: HashMap::new(), last_broadcast_flags: HashMap::new(), last_broadcast_workspace: None, diff --git a/crates/wayland/src/winit/connect.rs b/crates/wayland/src/winit/connect.rs index 41e9b2a..d18a489 100644 --- a/crates/wayland/src/winit/connect.rs +++ b/crates/wayland/src/winit/connect.rs @@ -135,6 +135,8 @@ impl WaylandPlatform { _idle_inhibit_manager_state: smithay::wayland::idle_inhibit::IdleInhibitManagerState::new::<CompState>(&dh), idle_inhibiting_surfaces: Vec::new(), last_idle_notify: None, + pointer_button_grab: None, + pointer_buttons_held: 0, window_anims: HashMap::new(), last_broadcast_flags: HashMap::new(), last_broadcast_workspace: None, |