srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/wayland/src/udev/session.rs
blob: bf6eb6716a83fd755a25b45a40f5b9b0f99c1d37 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
use super::*;

pub(crate) fn register_drm_fd(handle: &LoopHandle<'static, CompState>, card: &Rc<Card>) -> PlatformResult<()> {
    let raw = card.as_fd().as_raw_fd();
    // SAFETY: `FdWrapper` does not close `raw`; the owning `Card` lives in
    // `CompState::udev` for as long as this event source is registered.
    let wrapper = unsafe { FdWrapper::new(raw) };
    let source = Generic::new(wrapper, Interest::READ, CalloopMode::Level);
    handle
        .insert_source(source, move |_, _, data: &mut CompState| {
            let Some(udev) = data.udev.as_ref() else { return Ok(PostAction::Continue) };
            let card = udev.card.clone();
            match card.receive_events() {
                Ok(events) => {
                    // The event names the CRTC it came from, so with several
                    // monitors only that head advances - flipping all of
                    // them would desynchronise the others' buffers.
                    let mut flipped = false;
                    for event in events {
                        let DrmEvent::PageFlip(flip) = event else { continue };
                        if let Some(udev) = data.udev.as_mut() {
                            if let Some(head) = udev.heads.iter_mut().find(|h| h.crtc == flip.crtc) {
                                head.front = 1 - head.front;
                                head.flip_pending = false;
                                flipped = true;
                            }
                        }
                    }
                    if flipped {
                        data.render_udev_frame();
                    }
                }
                Err(e) => log::warn!("udev: receive_events failed: {e}"),
            }
            Ok(PostAction::Continue)
        })
        .map_err(|e| PlatformError::Other(format!("failed to register DRM fd: {e}")))?;
    Ok(())
}

/// Registers the `DrmDeviceNotifier` [`gpu::probe`](super::gpu::probe)
/// returns on success as its own calloop event source - a real,
/// independent `DrmDevice`/fd from the legacy one [`register_drm_fd`]
/// above already watches, so this is a second, parallel registration, not
/// a replacement. Only called from `udev/platform.rs`'s startup path when
/// `SRDWM_GPU=1` and the probe actually succeeded; a no-op (never called
/// at all) otherwise.
///
/// `DrmEvent::VBlank` marks the matching `GpuOutput` (if any - `udev.gpu`
/// might still be mid-construction, or the crtc might not be this
/// backend's GPU-driven one) as having had its frame actually scanned
/// out, via `frame_submitted()` - required by `DrmOutput::queue_frame`'s
/// own doc comment (see `render_udev_frame`'s GPU branch), or the
/// underlying swapchain eventually runs out of buffers. `DrmEvent::Error`
/// is logged, not treated as fatal - matching every other error-handling
/// choice in this GPU path, which always prefers "log and keep going" over
/// tearing down the whole session for an experimental, opt-in feature.
pub(crate) fn register_gpu_drm_notifier(handle: &LoopHandle<'static, CompState>, notifier: smithay::backend::drm::DrmDeviceNotifier) -> PlatformResult<()> {
    // Fully qualified, not the bare `DrmEvent` this module's own `use
    // super::*` already brings in - that name is `drm::control::Event`
    // (the legacy raw drm-rs event type `register_drm_fd` above reads via
    // `card.receive_events()`, with a `PageFlip` variant), a different
    // type from smithay's own higher-level `backend::drm::DrmEvent` (with
    // `Vblank`/`Error` variants) this notifier actually produces.
    handle
        .insert_source(notifier, move |event, _metadata, data: &mut CompState| match event {
            smithay::backend::drm::DrmEvent::VBlank(crtc) => {
                if let Some(udev) = data.udev.as_mut() {
                    if let Some(gpu) = udev.gpu.as_ref() {
                        if let Some(gpu_output) = gpu.output_for(crtc) {
                            if let Err(e) = gpu_output.frame_submitted() {
                                log::warn!("udev: SRDWM_GPU=1 frame_submitted failed for crtc {crtc:?}: {e:?}");
                            }
                        }
                    }
                }
            }
            smithay::backend::drm::DrmEvent::Error(e) => log::warn!("udev: SRDWM_GPU=1 DrmDevice error: {e:?}"),
        })
        .map_err(|e| PlatformError::Other(format!("failed to register GPU DRM notifier: {e}")))?;
    Ok(())
}

/// Registers the real libinput event source and hands back a second,
/// reference-counted handle onto the exact same underlying context (`Libinput`
/// wraps a `libinput_ref`/`libinput_unref`-counted C pointer - see its own
/// `Clone` impl - so this is the same live context `LibinputInputBackend`
/// dispatches events from, not a separate one) for `register_session_notifier`
/// to call `suspend()`/`resume()` on across a VT switch. `LibinputInputBackend`
/// itself only ever exposes an immutable `&Libinput` (`context()`), and is
/// moved into calloop's event source registration below with no way to get
/// a `&mut` back out afterward - cloning before that move is the only way
/// to keep a callable handle at all.
pub(crate) fn register_libinput(handle: &LoopHandle<'static, CompState>, session: &LibSeatSession, seat_name: &str) -> PlatformResult<Libinput> {
    let mut libinput_context = Libinput::new_with_udev::<LibinputSessionInterface<LibSeatSession>>(session.clone().into());
    libinput_context.udev_assign_seat(seat_name).map_err(|_| PlatformError::Other("udev: libinput udev_assign_seat failed".into()))?;
    let resume_handle = libinput_context.clone();
    let libinput_backend = LibinputInputBackend::new(libinput_context);

    handle
        .insert_source(libinput_backend, move |event, _, data: &mut CompState| {
            handle_libinput_event(data, event);
        })
        .map_err(|e| PlatformError::Other(format!("failed to register libinput backend: {e}")))?;
    Ok(resume_handle)
}

/// `libinput` is a second handle onto the exact same context
/// `register_libinput` gave its own event source - see that function's own
/// doc comment for why a clone is the only way to get one at all. Kept
/// alive by this closure for as long as this event source is registered
/// (the entire life of the process), purely so `suspend()`/`resume()` can be
/// called on it here.
pub(crate) fn register_session_notifier(handle: &LoopHandle<'static, CompState>, notifier: LibSeatSessionNotifier, mut libinput: Libinput) -> PlatformResult<()> {
    handle
        .insert_source(notifier, move |event, &mut (), data: &mut CompState| {
            let Some(udev) = data.udev.as_mut() else { return };
            match event {
                SessionEvent::PauseSession => {
                    log::info!("udev: session paused (VT switch away)");
                    udev.active = false;
                    // Not strictly required (the kernel revokes every input
                    // device's fd across a VT switch regardless), but this
                    // is the documented, correct way to tell libinput that's
                    // about to happen rather than let it discover revoked
                    // fds as surprise read errors - see `resume()`'s own
                    // doc comment below for why the other half of this pair
                    // is not optional at all.
                    libinput.suspend();
                    // `SRDWM_GPU=1` only - see `gpu::probe`'s own doc
                    // comment. `DrmOutputManager::pause` (-> `DrmDevice::
                    // pause`) drops this device's own DRM master lock and
                    // marks it inactive, the same pairing `anvil` itself
                    // uses around a VT switch - a separate device/fd from
                    // the legacy `Card` above, so this is additive, not a
                    // substitute for anything already happening here.
                    if let Some(gpu) = udev.gpu.as_mut() {
                        gpu.output_manager.pause();
                    }
                }
                SessionEvent::ActivateSession => {
                    log::info!("udev: session resumed (VT switch back)");
                    udev.active = true;
                    // Without this, libinput's own internal device list
                    // stays exactly as it was before the switch away, still
                    // holding the same file descriptors the kernel already
                    // revoked the moment this session lost the VT --
                    // `receive_events`/`dispatch` on a revoked fd doesn't
                    // error, it just silently never produces another event,
                    // forever. Confirmed live: after one real VT switch, no
                    // further keyboard or mouse input reached this
                    // compositor for the rest of the session (30+ minutes,
                    // multiple confirmed attempts) with literally nothing
                    // logged anywhere to explain it - rendering, the DRM/
                    // DPMS state above, and even libseat's own session
                    // activation all recovered correctly on their own, which
                    // is what made this the one piece actually missing
                    // rather than a repeat of the DPMS bug. `resume()`
                    // (`libinput_resume`) is libinput's own documented API
                    // for exactly this: it re-opens every device through
                    // the (now reactivated) session and resumes producing
                    // real events, the same call every other libinput-based
                    // compositor's session-resume path makes and this one
                    // never did.
                    if libinput.resume().is_err() {
                        log::warn!("udev: libinput resume failed after VT switch back - input devices may not recover; a full restart will be needed if so");
                    }
                    let card = udev.card.clone();

                    // A flip issued right before the VT switch away may
                    // never have completed while inactive (nothing was
                    // scanning out), and its completion event can still be
                    // sitting undelivered on the DRM fd. The kernel refuses
                    // a new page flip on a CRTC with one already
                    // unacknowledged (EBUSY) - drain and apply any such
                    // events now, before reasserting crtcs, so a stale flip
                    // from before the switch can't collide with the fresh
                    // one `render_udev_frame` is about to issue below.
                    match card.receive_events() {
                        Ok(events) => {
                            for event in events {
                                let DrmEvent::PageFlip(flip) = event else { continue };
                                if let Some(head) = udev.heads.iter_mut().find(|h| h.crtc == flip.crtc) {
                                    head.front = 1 - head.front;
                                    head.flip_pending = false;
                                }
                            }
                        }
                        Err(e) => log::debug!("udev: no pending flip events to drain on resume: {e}"),
                    }

                    // `SRDWM_GPU=1` only. `DrmOutputManager::activate` (->
                    // `DrmDevice::activate`) re-acquires this device's own
                    // DRM master lock - a separate device/fd from the
                    // legacy `Card` below, so this can run regardless of
                    // whether any legacy head also needs reasserting.
                    // Deliberately does *not* also force a fresh render
                    // here: `data.render_udev_frame()` at the end of this
                    // arm already reaches the GPU head unconditionally
                    // (`render.rs`'s GPU branch), and `DrmCompositor::
                    // render_frame` always issues a full state commit --
                    // atomic or legacy, whichever this specific device
                    // negotiated (see `gpu::probe`'s own doc comment) --
                    // not just a buffer swap, so that one call already
                    // reasserts mode-set and CRTC-active state together,
                    // unlike the legacy heads below, which need `set_crtc`
                    // called explicitly first because their own flip path
                    // (`copy_and_flip`) only ever swaps the buffer.
                    // `disable_connectors: false` - matches this
                    // compositor's own control over which connectors are
                    // actually driven (`disabled_connectors`), not
                    // something this resume path should reset.
                    if let Some(gpu) = udev.gpu.as_mut() {
                        if let Err(e) = gpu.output_manager.activate(false) {
                            log::warn!("udev: SRDWM_GPU=1 failed to reactivate DrmOutputManager on resume: {e}");
                        }
                    }
                    // Every crtc `SRDWM_GPU=1`'s `DrmOutputManager` is
                    // driving (if any - now possibly several, since it
                    // targets every head it could, not just one) --
                    // excluded from the legacy reassert loop below, since
                    // that loop's `set_crtc` runs through the *legacy*
                    // `Card`/fd, a completely different device handle than
                    // the GPU path's own `DrmDeviceFd`. Two separate fds
                    // issuing mode-set commands against the same physical
                    // CRTC is exactly the kind of conflict that produced
                    // this session's own worst VT-switch incidents when it
                    // was really one fd racing itself (`EBUSY` loops - see
                    // `UdevHead::flip_retry_after`'s own doc comment) - not
                    // a risk worth re-introducing here for a head this
                    // resume path already just reactivated correctly
                    // through its own, real API.
                    let gpu_crtcs: Vec<crtc::Handle> = udev.gpu.as_ref().map(|g| g.outputs.iter().map(|(c, _)| *c).collect()).unwrap_or_default();
                    // Some drivers reset mode-setting state across a VT
                    // switch; reassert every (non-GPU-driven) head before
                    // rendering again.
                    //
                    // The real connector and mode, not an empty connector
                    // list and no mode - that shape is DRM/KMS's own way
                    // to *disable* a CRTC, not reassert it, and was
                    // confirmed live to leave the screen black after
                    // switching back with no further VT switch, either
                    // direction, able to recover it. See `UdevHead::mode`'s
                    // own doc comment.
                    for head in udev.heads.iter_mut().filter(|h| !gpu_crtcs.contains(&h.crtc)) {
                        let fb = head.buffers[head.front].fb;
                        if let Err(e) = card.set_crtc(head.crtc, Some(fb), (0, 0), &[head.connector], Some(head.mode)) {
                            log::warn!("udev: failed to reassert crtc on resume: {e}");
                        }
                        // A real VT-switch-back can leave the panel itself
                        // powered down (DPMS off/standby) even once the CRTC
                        // above is genuinely re-driving it - mode-setting
                        // and display power are two separate pieces of KMS
                        // state, and only the former was ever touched here.
                        // Confirmed live: page flips kept succeeding with no
                        // error logged anywhere (this backend's own
                        // `copy_and_flip` never saw a failure) for the rest
                        // of a 30+ minute session after a real switch-away-
                        // and-back, and neither the screen nor keyboard
                        // input ever recovered on their own - every frame
                        // was genuinely being composited and flipped to a
                        // CRTC the panel simply wasn't lit to show. Setting
                        // "DPMS" straight to on here, unconditionally, is
                        // the same property `set_output_power` already
                        // writes for `zwlr_output_power_v1` - this is just
                        // that same write happening automatically on every
                        // resume instead of only when a client explicitly
                        // asks, since nothing else in this codebase ever
                        // calls it after a VT switch. A no-op on hardware
                        // that never needed it (the property is simply
                        // already `on`, or genuinely absent on some virtual/
                        // headless outputs - `find` below just yields
                        // nothing and this silently continues to the next
                        // head either way).
                        const DRM_MODE_DPMS_ON: u64 = 0;
                        if let Ok(props) = card.get_properties(head.connector) {
                            if let Some(dpms_prop) = props.as_props_and_values().0.iter().copied().find(|&h| card.get_property(h).is_ok_and(|info| info.name().to_str() == Ok("DPMS"))) {
                                if let Err(e) = card.set_property(head.connector, dpms_prop, DRM_MODE_DPMS_ON) {
                                    log::warn!("udev: failed to reassert DPMS-on for output {} on resume: {e}", head.output.name());
                                }
                            }
                        }
                        // Force a full repaint: contents are undefined after
                        // the VT switch (another VT's session may have
                        // scanned out something else entirely in between).
                        head.flip_pending = false;
                        head.ages = [0, 0];
                        head.flip_retry_after = None;
                    }
                    data.render_udev_frame();
                }
            }
        })
        .map_err(|e| PlatformError::Other(format!("failed to register session notifier: {e}")))?;
    Ok(())
}

/// Watches udev for DRM device changes. The kernel emits a `change` uevent
/// on the card when a connector is plugged or unplugged, which smithay
/// surfaces as [`UdevEvent::Changed`] - that is the hotplug signal.
///
/// `Added`/`Removed` refer to whole GPUs appearing or disappearing, which
/// this backend does not support (it binds one primary GPU at startup), so
/// they are logged and ignored rather than silently dropped.
pub(crate) fn register_udev_monitor(handle: &LoopHandle<'static, CompState>, seat_name: &str) -> PlatformResult<()> {
    let backend = UdevBackend::new(seat_name).map_err(err)?;
    handle
        .insert_source(backend, move |event, _, data: &mut CompState| match event {
            UdevEvent::Changed { .. } => {
                data.reprobe_outputs();
                data.render_udev_frame();
            }
            UdevEvent::Added { path, .. } => {
                log::info!("udev: new GPU {} appeared; multi-GPU is not supported, ignoring", path.display())
            }
            UdevEvent::Removed { .. } => log::info!("udev: a GPU was removed; multi-GPU is not supported, ignoring"),
        })
        .map_err(|e| PlatformError::Other(format!("failed to register udev monitor: {e}")))?;
    Ok(())
}

fn handle_libinput_event(state: &mut CompState, event: InputEvent<LibinputInputBackend>) {
    match event {
        InputEvent::Keyboard { event } => handle_keyboard_key_event(state, &event),
        InputEvent::PointerMotion { event } => {
            let Some(udev) = state.udev.as_mut() else { return };
            let delta = event.delta();
            // Clamped to the union of every head, so the pointer travels
            // between monitors instead of stopping at the first one's edge
            // - `min_x`/`min_y`, not a hardcoded `0.0` floor, so a head
            // placed at a negative origin (a real "extend left"/"extend
            // above" arrangement) is actually reachable. See `bounds`'s own
            // doc comment for the live bug this fixes.
            let (min_x, min_y, max_x, max_y) = udev.bounds();
            udev.pointer_pos.x = (udev.pointer_pos.x + delta.x).clamp(min_x, (max_x - 1.0).max(min_x));
            udev.pointer_pos.y = (udev.pointer_pos.y + delta.y).clamp(min_y, (max_y - 1.0).max(min_y));
            let pos = udev.pointer_pos;
            handle_pointer_position(state, pos, event.time_msec());
        }
        // Absolute-positioning devices (a touchscreen, a drawing tablet,
        // and - confirmed live via a `WAYLAND_DEBUG=1` trace from a peer
        // session - ydotool's virtual uinput device, used throughout this
        // whole debugging effort) had no handler here at all: this match
        // only ever covered `PointerMotion` (relative deltas), so every
        // `PointerMotionAbsolute` event fell through to the catch-all
        // below and was silently dropped. The winit (nested) backend
        // already handles this exact event via `event.position_transformed`
        // (see `winit/events.rs`'s matching arm); this is that same
        // pattern for the bare-metal backend, which never got it. Uses the
        // same union-of-every-head bounds `PointerMotion` above clamps
        // into, so a single absolute-positioning device still addresses
        // the whole multi-monitor span, not just the first head.
        InputEvent::PointerMotionAbsolute { event } => {
            let Some(udev) = state.udev.as_mut() else { return };
            // `position_transformed` maps the device's own normalized
            // [0,1] position into a `(0, 0)`-anchored size - offset by
            // `min_x`/`min_y` afterward, same reasoning as `PointerMotion`
            // above, so this still addresses a negative-origin head.
            let (min_x, min_y, max_x, max_y) = udev.bounds();
            let size = Size::from(((max_x - min_x) as i32, (max_y - min_y) as i32));
            let pos = event.position_transformed(size);
            udev.pointer_pos.x = (pos.x + min_x).clamp(min_x, (max_x - 1.0).max(min_x));
            udev.pointer_pos.y = (pos.y + min_y).clamp(min_y, (max_y - 1.0).max(min_y));
            let pos = udev.pointer_pos;
            handle_pointer_position(state, pos, event.time_msec());
        }
        InputEvent::PointerButton { event } => {
            let Some(pos) = state.udev.as_ref().map(|u| u.pointer_pos) else { return };
            let button = event.button_code();
            let pressed = event.state() == BackendButtonState::Pressed;
            handle_pointer_button(state, pos, button, pressed, event.time_msec());
        }
        // Laptop lid. libinput reports this as a switch toggle; without
        // handling it, closing the lid does nothing at all - no lock, no
        // suspend - which is a genuine problem on a laptop rather than a
        // missing nicety.
        InputEvent::SwitchToggle { event } => {
            // Fully qualified: libinput's own `Switch` is also in scope here.
            use smithay::backend::input::{SwitchState, SwitchToggleEvent};
            if matches!(event.switch(), Some(smithay::reexports::input::event::switch::Switch::Lid)) {
                let closed = event.state() == SwitchState::On;
                log::info!("lid {}", if closed { "closed" } else { "opened" });
                state.pending.borrow_mut().push(CoreEvent::LidSwitch { closed });
            }
        }
        InputEvent::PointerAxis { event } => {
            // Modifier+scroll switches workspace instead of reaching the
            // client - the `bind = SUPER, mouse_down/up, workspace, e+1/e-1`
            // gesture. Checked first so the client never sees these events;
            // forwarding them too would scroll the window under the cursor
            // as a side effect of changing workspace.
            if crate::input::handle_workspace_scroll(state, &event) {
                return;
            }
            // Otherwise: forwarded to the focused client via the pointer axis
            // frame, no WM-level handling.
            let Some(pointer) = state.seat.get_pointer() else { return };
            let source = event.source();
            let mut frame = AxisFrame::new(event.time_msec()).source(source);
            for axis in [Axis::Horizontal, Axis::Vertical] {
                match event.amount(axis) {
                    Some(value) => frame = frame.value(axis, value),
                    // `AxisSource::Finger` (a touchpad) *requires* a stop
                    // event on the frame where the finger lifts and the
                    // axis genuinely has no more motion - see `AxisFrame::
                    // source`'s own doc comment ("Using AxisSource::Finger
                    // requires a stop event to be sent, when the user lifts
                    // off the finger"). Never sending it left every
                    // two-finger scroll gesture with no way to tell Firefox/
                    // GTK it had actually ended, which is exactly the kind
                    // of thing that reads as "scrolling doesn't work" --
                    // not "no events arrive" (discrete wheel scrolling,
                    // which needs no stop event, was never affected) but
                    // kinetic/momentum scrolling and starting a fresh
                    // gesture right after a previous one never settling.
                    None if source == AxisSource::Finger => frame = frame.stop(axis),
                    None => {}
                }
                // Discrete wheel steps, additional to the pixel `value`
                // above - optional (`value` is the only event a client
                // strictly needs), but some clients use it to distinguish
                // "one physical click" from a smooth/high-resolution
                // scroll, so provide it whenever the device actually
                // reports one (real scroll wheels; never touchpads, which
                // have no discrete steps to report - `amount_v120` is
                // `None` for those, same guarantee `amount` gives the
                // other way around).
                if let Some(v120) = event.amount_v120(axis) {
                    frame = frame.v120(axis, v120 as i32);
                }
            }
            pointer.axis(state, frame);
            pointer.frame(state);
        }
        // 3+-finger swipe - claimed entirely for workspace switching, never
        // reaches a client. See `handle_gesture_swipe_end`'s doc comment.
        InputEvent::GestureSwipeBegin { event } => handle_gesture_swipe_begin(state, &event),
        InputEvent::GestureSwipeUpdate { event } => handle_gesture_swipe_update(state, &event),
        InputEvent::GestureSwipeEnd { event } => handle_gesture_swipe_end(state, &event),
        // Pinch/hold: no WM-level meaning, forwarded to the focused client
        // as-is (`wp_pointer_gestures`) - pinch-to-zoom in an image viewer
        // or PDF reader, the one real use either has. Same reasoning as the
        // `PointerAxis` forwarding above: nothing here should be silently
        // dropped just because this WM has no use for it itself.
        InputEvent::GesturePinchBegin { event } => {
            let Some(pointer) = state.seat.get_pointer() else { return };
            let fingers = event.fingers();
            pointer.gesture_pinch_begin(state, &GesturePinchBeginEvent { serial: SERIAL_COUNTER.next_serial(), time: event.time_msec(), fingers });
        }
        InputEvent::GesturePinchUpdate { event } => {
            let Some(pointer) = state.seat.get_pointer() else { return };
            let (delta, scale, rotation) = (event.delta(), event.scale(), event.rotation());
            pointer.gesture_pinch_update(state, &GesturePinchUpdateEvent { time: event.time_msec(), delta, scale, rotation });
        }
        InputEvent::GesturePinchEnd { event } => {
            let Some(pointer) = state.seat.get_pointer() else { return };
            let cancelled = event.cancelled();
            pointer.gesture_pinch_end(state, &GesturePinchEndEvent { serial: SERIAL_COUNTER.next_serial(), time: event.time_msec(), cancelled });
        }
        InputEvent::GestureHoldBegin { event } => {
            let Some(pointer) = state.seat.get_pointer() else { return };
            let fingers = event.fingers();
            pointer.gesture_hold_begin(state, &GestureHoldBeginEvent { serial: SERIAL_COUNTER.next_serial(), time: event.time_msec(), fingers });
        }
        InputEvent::GestureHoldEnd { event } => {
            let Some(pointer) = state.seat.get_pointer() else { return };
            let cancelled = event.cancelled();
            pointer.gesture_hold_end(state, &GestureHoldEndEvent { serial: SERIAL_COUNTER.next_serial(), time: event.time_msec(), cancelled });
        }
        _ => {}
    }
}