srdusr
aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2025-02-13 01:14:00 +0200
committersrdusr <[email protected]>2025-02-13 01:14:00 +0200
commited0b8ecf6c08d920ffd1b52c9dc6a24a436ce977 (patch)
tree0841a1a8d13a5f36a89f611dde27e6cb9db31c5b
parentb1d69a552cdcc7370c4befd42ef2111c8b367c46 (diff)
downloadsrdwm-ed0b8ecf6c08d920ffd1b52c9dc6a24a436ce977.tar.gz
srdwm-ed0b8ecf6c08d920ffd1b52c9dc6a24a436ce977.zip
Implement the Wayland implicit pointer grab
Every pointer motion event re-ran the same popup/layer/content hit-test from scratch and delivered focus to whatever it found right now - there was no notion of "a button is held, keep delivering to the surface that received the press" at all, which is standard, expected Wayland compositor behavior (every real compositor does this; it's how dragging, text selection, and scrollbar-thumb dragging all stay coherent even when the pointer briefly leaves the widget's bounds mid-gesture). Without it, a real human's hand drifting even slightly outside the pressed surface mid-drag - trivially easy during a fast, non-perfectly- straight mouse motion - sent that client an unrequested `leave` event 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 by its title bar does nothing at all" - live-reproduced this work on Nemo, and consistent with move_request never having fired once all session despite real attempts. pointer_button_grab captures the (surface, origin) resolved on a button press once the held-button count goes from 0 to 1, and every event under the grab - motion or button, this press's or a later one overlapping it - is delivered there instead of wherever a fresh hit-test lands, until every held button is back up.
-rw-r--r--crates/wayland/src/input.rs64
-rw-r--r--crates/wayland/src/state/mod.rs29
-rw-r--r--crates/wayland/src/udev/platform.rs2
-rw-r--r--crates/wayland/src/winit/connect.rs2
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,