smithay/wayland/
pointer_constraints.rs

1//! Protocol for confining the pointer.
2//!
3//! This provides a way for the client to request that the pointer is confined to a region or
4//! locked in place.
5use std::{
6    collections::{hash_map, HashMap},
7    ops,
8    sync::{
9        atomic::{AtomicBool, Ordering},
10        Mutex,
11    },
12};
13
14use wayland_protocols::wp::pointer_constraints::zv1::server::{
15    zwp_confined_pointer_v1::{self, ZwpConfinedPointerV1},
16    zwp_locked_pointer_v1::{self, ZwpLockedPointerV1},
17    zwp_pointer_constraints_v1::{self, Lifetime, ZwpPointerConstraintsV1},
18};
19use wayland_server::{
20    backend::GlobalId, protocol::wl_surface::WlSurface, Client, DataInit, Dispatch, DisplayHandle,
21    GlobalDispatch, New, Resource, WEnum,
22};
23
24use super::compositor::{self, RegionAttributes};
25use crate::{
26    input::{pointer::PointerHandle, SeatHandler},
27    utils::{Logical, Point},
28    wayland::seat::PointerUserData,
29};
30
31const VERSION: u32 = 1;
32
33/// Handler for pointer constraints
34pub trait PointerConstraintsHandler: SeatHandler {
35    /// Pointer lock or confinement constraint created for `pointer` on `surface`
36    ///
37    /// Use [`with_pointer_constraint`] to access the constraint.
38    fn new_constraint(&mut self, surface: &WlSurface, pointer: &PointerHandle<Self>);
39
40    /// The client holding a LockedPointer has commited a cursor position hint.
41    ///
42    /// This is emitted upon a surface commit if the cursor position hint has been updated.
43    ///
44    /// Use [`with_pointer_constraint`] to access the constraint and check if it is active.
45    fn cursor_position_hint(
46        &mut self,
47        surface: &WlSurface,
48        pointer: &PointerHandle<Self>,
49        location: Point<f64, Logical>,
50    );
51}
52
53/// Constraint confining pointer to a region of the surface
54#[derive(Debug)]
55pub struct ConfinedPointer {
56    handle: zwp_confined_pointer_v1::ZwpConfinedPointerV1,
57    region: Option<RegionAttributes>,
58    pending_region: Option<RegionAttributes>,
59    lifetime: WEnum<Lifetime>,
60    active: AtomicBool,
61}
62
63impl ConfinedPointer {
64    /// Region in which to confine the pointer
65    pub fn region(&self) -> Option<&RegionAttributes> {
66        self.region.as_ref()
67    }
68}
69
70/// Constraint locking pointer in place
71#[derive(Debug)]
72pub struct LockedPointer {
73    handle: zwp_locked_pointer_v1::ZwpLockedPointerV1,
74    region: Option<RegionAttributes>,
75    pending_region: Option<RegionAttributes>,
76    lifetime: WEnum<Lifetime>,
77    cursor_position_hint: Option<Point<f64, Logical>>,
78    pending_cursor_position_hint: Option<Point<f64, Logical>>,
79    active: AtomicBool,
80}
81
82impl LockedPointer {
83    /// Region in which to activate the lock
84    pub fn region(&self) -> Option<&RegionAttributes> {
85        self.region.as_ref()
86    }
87
88    /// Position the client is rendering a cursor, if any
89    pub fn cursor_position_hint(&self) -> Option<Point<f64, Logical>> {
90        self.cursor_position_hint
91    }
92}
93
94/// A constraint imposed on the pointer instance
95#[derive(Debug)]
96pub enum PointerConstraint {
97    /// Pointer is confined to a region of the surface
98    Confined(ConfinedPointer),
99    /// Pointer is locked in place
100    Locked(LockedPointer),
101}
102
103/// A reference to a pointer constraint that can be activated or deactivated.
104///
105/// The derefs to `[PointerConstraint]`.
106#[derive(Debug)]
107pub struct PointerConstraintRef<'a, D: SeatHandler + 'static> {
108    entry: hash_map::OccupiedEntry<'a, PointerHandle<D>, PointerConstraint>,
109}
110
111impl<D: SeatHandler + 'static> ops::Deref for PointerConstraintRef<'_, D> {
112    type Target = PointerConstraint;
113
114    fn deref(&self) -> &Self::Target {
115        self.entry.get()
116    }
117}
118
119impl<D: SeatHandler + 'static> PointerConstraintRef<'_, D> {
120    /// Send `locked`/`unlocked`
121    ///
122    /// This is not sent automatically since compositors may have different
123    /// policies about when to allow and activate constraints.
124    pub fn activate(&self) {
125        match self.entry.get() {
126            PointerConstraint::Confined(confined) => {
127                confined.handle.confined();
128                confined.active.store(true, Ordering::SeqCst);
129            }
130            PointerConstraint::Locked(locked) => {
131                locked.handle.locked();
132                locked.active.store(true, Ordering::SeqCst);
133            }
134        }
135    }
136
137    /// Send `unlocked`/`unconfined`
138    ///
139    /// For oneshot constraints, will destroy the constraint.
140    ///
141    /// This is sent automatically when the surface loses pointer focus, but
142    /// may also be invoked while the surface is focused.
143    pub fn deactivate(self) {
144        match self.entry.get() {
145            PointerConstraint::Confined(confined) => {
146                confined.handle.unconfined();
147                confined.active.store(false, Ordering::SeqCst);
148            }
149            PointerConstraint::Locked(locked) => {
150                locked.handle.unlocked();
151                locked.active.store(false, Ordering::SeqCst);
152            }
153        }
154
155        if self.lifetime() == WEnum::Value(Lifetime::Oneshot) {
156            self.entry.remove_entry();
157        }
158    }
159}
160
161impl PointerConstraint {
162    /// Constraint is active
163    pub fn is_active(&self) -> bool {
164        match self {
165            PointerConstraint::Confined(confined) => &confined.active,
166            PointerConstraint::Locked(locked) => &locked.active,
167        }
168        .load(Ordering::SeqCst)
169    }
170
171    /// Region in which to lock or confine the pointer
172    pub fn region(&self) -> Option<&RegionAttributes> {
173        match self {
174            PointerConstraint::Confined(confined) => confined.region(),
175            PointerConstraint::Locked(locked) => locked.region(),
176        }
177    }
178
179    fn lifetime(&self) -> WEnum<Lifetime> {
180        match self {
181            PointerConstraint::Confined(confined) => confined.lifetime,
182            PointerConstraint::Locked(locked) => locked.lifetime,
183        }
184    }
185
186    /// Commits the pending state of the constraint, and returns the cursor position hint if it has changed.
187    fn commit(&mut self) -> Option<Point<f64, Logical>> {
188        match self {
189            Self::Confined(confined) => {
190                confined.region.clone_from(&confined.pending_region);
191                None
192            }
193            Self::Locked(locked) => {
194                locked.region.clone_from(&locked.pending_region);
195                locked.pending_cursor_position_hint.take().inspect(|hint| {
196                    locked.cursor_position_hint = Some(*hint);
197                })
198            }
199        }
200    }
201}
202
203/// Pointer constraints state.
204#[derive(Debug)]
205pub struct PointerConstraintsState {
206    global: GlobalId,
207}
208
209impl PointerConstraintsState {
210    /// Create a new pointer constraints global
211    pub fn new<D>(display: &DisplayHandle) -> Self
212    where
213        D: GlobalDispatch<ZwpPointerConstraintsV1, ()>,
214        D: Dispatch<ZwpPointerConstraintsV1, ()>,
215        D: Dispatch<ZwpConfinedPointerV1, PointerConstraintUserData<D>>,
216        D: Dispatch<ZwpLockedPointerV1, PointerConstraintUserData<D>>,
217        D: SeatHandler,
218        D: 'static,
219    {
220        let global = display.create_global::<D, ZwpPointerConstraintsV1, _>(VERSION, ());
221
222        Self { global }
223    }
224
225    /// Get the id of ZwpPointerConstraintsV1 global
226    pub fn global(&self) -> GlobalId {
227        self.global.clone()
228    }
229}
230
231#[doc(hidden)]
232#[derive(Debug)]
233pub struct PointerConstraintUserData<D: SeatHandler> {
234    surface: WlSurface,
235    pointer: Option<PointerHandle<D>>,
236}
237
238struct PointerConstraintData<D: SeatHandler + 'static> {
239    constraints: HashMap<PointerHandle<D>, PointerConstraint>,
240}
241
242// TODO Public method to get current constraints for surface/seat
243/// Get constraint for surface and pointer, if any
244pub fn with_pointer_constraint<
245    D: SeatHandler + 'static,
246    T,
247    F: FnOnce(Option<PointerConstraintRef<'_, D>>) -> T,
248>(
249    surface: &WlSurface,
250    pointer: &PointerHandle<D>,
251    f: F,
252) -> T {
253    with_constraint_data::<D, _, _>(surface, |data| {
254        let constraint = data.and_then(|data| match data.constraints.entry(pointer.clone()) {
255            hash_map::Entry::Occupied(entry) => Some(PointerConstraintRef { entry }),
256            hash_map::Entry::Vacant(_) => None,
257        });
258        f(constraint)
259    })
260}
261
262fn commit_hook<D: SeatHandler + PointerConstraintsHandler + 'static>(
263    state: &mut D,
264    _dh: &DisplayHandle,
265    surface: &WlSurface,
266) {
267    // `with_constraint_data` locks the pointer constraints,
268    // so we collect the hints first into a Vec, then release the mutex
269    // and only once the mutex is released, we call the handler method.
270    //
271    // This is to avoid deadlocks when the handler method might try to access the constraints again.
272    // It's not a hypothetical, it bit me while implementing the position hint functionality.
273    let position_hints = with_constraint_data::<D, _, _>(surface, |data| {
274        let data = data.unwrap();
275        data.constraints
276            .iter_mut()
277            .filter_map(|(pointer, constraint)| constraint.commit().map(|hint| (pointer.clone(), hint)))
278            .collect::<Vec<_>>()
279    });
280
281    for (pointer, hint) in position_hints {
282        state.cursor_position_hint(surface, &pointer, hint);
283    }
284}
285
286/// Get `PointerConstraintData` associated with a surface, if any.
287fn with_constraint_data<
288    D: SeatHandler + 'static,
289    T,
290    F: FnOnce(Option<&mut PointerConstraintData<D>>) -> T,
291>(
292    surface: &WlSurface,
293    f: F,
294) -> T {
295    compositor::with_states(surface, |states| {
296        let data = states.data_map.get::<Mutex<PointerConstraintData<D>>>();
297        if let Some(data) = data {
298            f(Some(&mut data.lock().unwrap()))
299        } else {
300            f(None)
301        }
302    })
303}
304
305/// Add constraint for surface, or raise protocol error if one exists
306fn add_constraint<D: SeatHandler + PointerConstraintsHandler + 'static>(
307    pointer_constraints: &ZwpPointerConstraintsV1,
308    surface: &WlSurface,
309    pointer: &PointerHandle<D>,
310    constraint: PointerConstraint,
311) {
312    let mut added = false;
313    compositor::with_states(surface, |states| {
314        added = states.data_map.insert_if_missing_threadsafe(|| {
315            Mutex::new(PointerConstraintData::<D> {
316                constraints: HashMap::new(),
317            })
318        });
319        let data = states.data_map.get::<Mutex<PointerConstraintData<D>>>().unwrap();
320        let mut data = data.lock().unwrap();
321
322        if data.constraints.contains_key(pointer) {
323            pointer_constraints.post_error(
324                zwp_pointer_constraints_v1::Error::AlreadyConstrained,
325                "pointer constrait already exists for surface and seat",
326            );
327        } else {
328            data.constraints.insert(pointer.clone(), constraint);
329        }
330    });
331
332    if added {
333        compositor::add_post_commit_hook(surface, commit_hook::<D>);
334    }
335}
336
337fn remove_constraint<D: SeatHandler + 'static>(surface: &WlSurface, pointer: &PointerHandle<D>) {
338    with_constraint_data::<D, _, _>(surface, |data| {
339        if let Some(data) = data {
340            data.constraints.remove(pointer);
341        }
342    });
343}
344
345impl<D> Dispatch<ZwpPointerConstraintsV1, (), D> for PointerConstraintsState
346where
347    D: Dispatch<ZwpPointerConstraintsV1, ()>,
348    D: Dispatch<ZwpConfinedPointerV1, PointerConstraintUserData<D>>,
349    D: Dispatch<ZwpLockedPointerV1, PointerConstraintUserData<D>>,
350    D: SeatHandler,
351    D: PointerConstraintsHandler,
352    D: 'static,
353{
354    fn request(
355        state: &mut D,
356        _client: &wayland_server::Client,
357        pointer_constraints: &ZwpPointerConstraintsV1,
358        request: zwp_pointer_constraints_v1::Request,
359        _data: &(),
360        _dh: &DisplayHandle,
361        data_init: &mut wayland_server::DataInit<'_, D>,
362    ) {
363        match request {
364            zwp_pointer_constraints_v1::Request::LockPointer {
365                id,
366                surface,
367                pointer,
368                region,
369                lifetime,
370            } => {
371                let region = region.as_ref().map(compositor::get_region_attributes);
372                let pointer = pointer.data::<PointerUserData<D>>().unwrap().handle.clone();
373                let handle = data_init.init(
374                    id,
375                    PointerConstraintUserData {
376                        surface: surface.clone(),
377                        pointer: pointer.clone(),
378                    },
379                );
380                if let Some(pointer) = pointer {
381                    add_constraint(
382                        pointer_constraints,
383                        &surface,
384                        &pointer,
385                        PointerConstraint::Locked(LockedPointer {
386                            handle,
387                            region: region.clone(),
388                            pending_region: region,
389                            lifetime,
390                            cursor_position_hint: None,
391                            pending_cursor_position_hint: None,
392                            active: AtomicBool::new(false),
393                        }),
394                    );
395                    state.new_constraint(&surface, &pointer);
396                }
397            }
398            zwp_pointer_constraints_v1::Request::ConfinePointer {
399                id,
400                surface,
401                pointer,
402                region,
403                lifetime,
404            } => {
405                let region = region.as_ref().map(compositor::get_region_attributes);
406                let pointer = pointer.data::<PointerUserData<D>>().unwrap().handle.clone();
407                let handle = data_init.init(
408                    id,
409                    PointerConstraintUserData {
410                        surface: surface.clone(),
411                        pointer: pointer.clone(),
412                    },
413                );
414                if let Some(pointer) = pointer {
415                    add_constraint(
416                        pointer_constraints,
417                        &surface,
418                        &pointer,
419                        PointerConstraint::Confined(ConfinedPointer {
420                            handle,
421                            region: region.clone(),
422                            pending_region: region,
423                            lifetime,
424                            active: AtomicBool::new(false),
425                        }),
426                    );
427                    state.new_constraint(&surface, &pointer);
428                }
429            }
430            zwp_pointer_constraints_v1::Request::Destroy => {}
431            _ => unreachable!(),
432        }
433    }
434}
435
436impl<D> GlobalDispatch<ZwpPointerConstraintsV1, (), D> for PointerConstraintsState
437where
438    D: GlobalDispatch<ZwpPointerConstraintsV1, ()>
439        + Dispatch<ZwpPointerConstraintsV1, ()>
440        + SeatHandler
441        + 'static,
442{
443    fn bind(
444        _state: &mut D,
445        _dh: &DisplayHandle,
446        _client: &Client,
447        resource: New<ZwpPointerConstraintsV1>,
448        _global_data: &(),
449        data_init: &mut DataInit<'_, D>,
450    ) {
451        data_init.init(resource, ());
452    }
453}
454
455impl<D> Dispatch<ZwpConfinedPointerV1, PointerConstraintUserData<D>, D> for PointerConstraintsState
456where
457    D: Dispatch<ZwpConfinedPointerV1, PointerConstraintUserData<D>>,
458    D: SeatHandler,
459    D: 'static,
460{
461    fn request(
462        _state: &mut D,
463        _client: &wayland_server::Client,
464        _confined_pointer: &ZwpConfinedPointerV1,
465        request: zwp_confined_pointer_v1::Request,
466        data: &PointerConstraintUserData<D>,
467        _dh: &DisplayHandle,
468        _data_init: &mut wayland_server::DataInit<'_, D>,
469    ) {
470        let Some(pointer) = &data.pointer else {
471            return;
472        };
473
474        match request {
475            zwp_confined_pointer_v1::Request::SetRegion { region } => {
476                with_pointer_constraint(&data.surface, pointer, |constraint| {
477                    if let Some(PointerConstraint::Confined(confined)) =
478                        constraint.map(|x| x.entry.into_mut())
479                    {
480                        confined.pending_region = region.as_ref().map(compositor::get_region_attributes);
481                    }
482                });
483            }
484            zwp_confined_pointer_v1::Request::Destroy => {}
485            _ => unreachable!(),
486        }
487    }
488
489    fn destroyed(
490        _state: &mut D,
491        _client: wayland_server::backend::ClientId,
492        _resource: &ZwpConfinedPointerV1,
493        data: &PointerConstraintUserData<D>,
494    ) {
495        let Some(pointer) = &data.pointer else {
496            return;
497        };
498
499        remove_constraint(&data.surface, pointer);
500    }
501}
502
503impl<D> Dispatch<ZwpLockedPointerV1, PointerConstraintUserData<D>, D> for PointerConstraintsState
504where
505    D: Dispatch<ZwpLockedPointerV1, PointerConstraintUserData<D>>,
506    D: SeatHandler,
507    D: 'static,
508{
509    fn request(
510        _state: &mut D,
511        _client: &wayland_server::Client,
512        _locked_pointer: &ZwpLockedPointerV1,
513        request: zwp_locked_pointer_v1::Request,
514        data: &PointerConstraintUserData<D>,
515        _dh: &DisplayHandle,
516        _data_init: &mut wayland_server::DataInit<'_, D>,
517    ) {
518        let Some(pointer) = &data.pointer else {
519            return;
520        };
521
522        match request {
523            zwp_locked_pointer_v1::Request::SetCursorPositionHint { surface_x, surface_y } => {
524                with_pointer_constraint(&data.surface, pointer, |constraint| {
525                    if let Some(PointerConstraint::Locked(locked)) = constraint.map(|x| x.entry.into_mut()) {
526                        locked.pending_cursor_position_hint = Some((surface_x, surface_y).into());
527                    }
528                });
529            }
530            zwp_locked_pointer_v1::Request::SetRegion { region } => {
531                with_pointer_constraint(&data.surface, pointer, |constraint| {
532                    if let Some(PointerConstraint::Locked(locked)) = constraint.map(|x| x.entry.into_mut()) {
533                        locked.pending_region = region.as_ref().map(compositor::get_region_attributes);
534                    }
535                });
536            }
537            zwp_locked_pointer_v1::Request::Destroy => {}
538            _ => unreachable!(),
539        }
540    }
541
542    fn destroyed(
543        _state: &mut D,
544        _client: wayland_server::backend::ClientId,
545        _resource: &ZwpLockedPointerV1,
546        data: &PointerConstraintUserData<D>,
547    ) {
548        let Some(pointer) = &data.pointer else {
549            return;
550        };
551
552        remove_constraint(&data.surface, pointer);
553    }
554}
555
556#[allow(missing_docs)]
557#[macro_export]
558macro_rules! delegate_pointer_constraints {
559    ($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
560        $crate::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
561            $crate::reexports::wayland_protocols::wp::pointer_constraints::zv1::server::zwp_pointer_constraints_v1::ZwpPointerConstraintsV1: ()
562        ] => $crate::wayland::pointer_constraints::PointerConstraintsState);
563        $crate::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
564            $crate::reexports::wayland_protocols::wp::pointer_constraints::zv1::server::zwp_pointer_constraints_v1::ZwpPointerConstraintsV1: ()
565        ] => $crate::wayland::pointer_constraints::PointerConstraintsState);
566        $crate::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
567            $crate::reexports::wayland_protocols::wp::pointer_constraints::zv1::server::zwp_confined_pointer_v1::ZwpConfinedPointerV1: $crate::wayland::pointer_constraints::PointerConstraintUserData<Self>
568        ] => $crate::wayland::pointer_constraints::PointerConstraintsState);
569        $crate::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
570            $crate::reexports::wayland_protocols::wp::pointer_constraints::zv1::server::zwp_locked_pointer_v1::ZwpLockedPointerV1: $crate::wayland::pointer_constraints::PointerConstraintUserData<Self>
571        ] => $crate::wayland::pointer_constraints::PointerConstraintsState);
572    };
573}