atagen

monolith

public
Star0
Watch0

Commit fb4bcf5b1d29

tarball

atagen <boss@atagen.co>2026-10-19 09:48 UTC

6 files changed+226-20
constraints: honour pointer lock and confine requests

The stub PointerConstraintsHandler never advertised the global, so Proton and SDL games could not capture the mouse. A constraint now activates only when its surface has pointer and keyboard focus, so a lock cannot trap the pointer on a window the keyboard has left, and it releases with the cursor position hint. While locked, focus-follows-mouse, mouse-follows-focus warps, Mod+drag and the resize hover are skipped.
Added file: crates/monolith/src/constraints.rs+202 additions
@@ -0,0 +1,202 @@
1+//! Pointer lock and confinement. A constraint is active while its surface
2+//! has both pointer and keyboard focus and the pointer is inside its region,
3+//! so a game that locks the mouse never traps it away from the window the
4+//! keyboard went to.
5+
6+use crate::idle::root_surface;
7+use crate::state::State;
8+use smithay::backend::input::InputTime;
9+use smithay::desktop::utils::bbox_from_surface_tree;
10+use smithay::input::pointer::PointerHandle;
11+use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
12+use smithay::utils::{Logical, Point, Rectangle};
13+use smithay::wayland::compositor::{RectangleKind, RegionAttributes};
14+use smithay::wayland::pointer_constraints::{
15+ ConstraintRemove, PointerConstraint, PointerConstraintsHandler, with_pointer_constraint,
16+};
17+use smithay::wayland::seat::WaylandFocus;
18+
19+pub enum Constraint {
20+ Locked,
21+ Confined {
22+ surface: WlSurface,
23+ region: Option<RegionAttributes>,
24+ },
25+}
26+
27+impl State {
28+ pub fn pointer_constraint(&self) -> Option<Constraint> {
29+ let pointer = self.input.seat.get_pointer()?;
30+ let surface = pointer.current_focus()?;
31+ with_pointer_constraint(&surface, &pointer, |c| {
32+ let c = c.filter(|c| c.is_active())?;
33+ Some(match &*c {
34+ PointerConstraint::Locked(_) => Constraint::Locked,
35+ PointerConstraint::Confined(_) => Constraint::Confined {
36+ surface: surface.clone(),
37+ region: c.region().cloned(),
38+ },
39+ })
40+ })
41+ }
42+
43+ pub fn pointer_locked(&self) -> bool {
44+ matches!(self.pointer_constraint(), Some(Constraint::Locked))
45+ }
46+
47+ /// Where a motion from `from` to `to` ends up inside a confinement,
48+ /// at the nearest allowed point when `to` falls outside the region.
49+ pub fn confine(
50+ &self,
51+ surface: &WlSurface,
52+ region: Option<&RegionAttributes>,
53+ from: Point<f64, Logical>,
54+ to: Point<f64, Logical>,
55+ ) -> Point<f64, Logical> {
56+ let Some(origin) = self.origin_of(surface, from) else {
57+ return to;
58+ };
59+ let bounds = bbox_from_surface_tree(surface, (0, 0));
60+ let allowed = |p: Point<f64, Logical>| {
61+ bounds.to_f64().contains(p) && region.is_none_or(|r| r.contains(p.to_i32_floor()))
62+ };
63+ let local = to - origin;
64+ if allowed(local) {
65+ return to;
66+ }
67+ let rects: Vec<Rectangle<i32, Logical>> = match region {
68+ Some(r) => r
69+ .rects
70+ .iter()
71+ .filter(|(kind, _)| matches!(kind, RectangleKind::Add))
72+ .filter_map(|(_, rect)| rect.intersection(bounds))
73+ .collect(),
74+ None => vec![bounds],
75+ };
76+ let nearest = rects
77+ .iter()
78+ .map(|r| {
79+ let r = r.to_f64();
80+ Point::from((
81+ local
82+ .x
83+ .clamp(r.loc.x, (r.loc.x + r.size.w - 1.0).max(r.loc.x)),
84+ local
85+ .y
86+ .clamp(r.loc.y, (r.loc.y + r.size.h - 1.0).max(r.loc.y)),
87+ ))
88+ })
89+ .filter(|p| allowed(*p))
90+ .min_by(|a: &Point<f64, Logical>, b| {
91+ let d = |p: &Point<f64, Logical>| (p.x - local.x).hypot(p.y - local.y);
92+ d(a).total_cmp(&d(b))
93+ });
94+ nearest.map_or(from, |p| p + origin)
95+ }
96+
97+ fn origin_of(
98+ &self,
99+ surface: &WlSurface,
100+ at: Point<f64, Logical>,
101+ ) -> Option<Point<f64, Logical>> {
102+ self.surface_under(at)
103+ .filter(|(s, _)| s == surface)
104+ .map(|(_, origin)| origin)
105+ }
106+
107+ fn constraint_eligible(&self, surface: &WlSurface) -> bool {
108+ !self.services.lock.active()
109+ && self.input.drag.is_none()
110+ && self
111+ .input
112+ .seat
113+ .get_keyboard()
114+ .and_then(|k| k.current_focus())
115+ .and_then(|f| f.wl_surface().map(|s| root_surface(&s)))
116+ .is_some_and(|root| root == root_surface(surface))
117+ }
118+
119+ /// Activates the pointer focus's constraint once it may apply, and
120+ /// deactivates it when keyboard focus has moved off the surface.
121+ pub fn sync_pointer_constraint(&mut self) {
122+ let Some(pointer) = self.input.seat.get_pointer() else {
123+ return;
124+ };
125+ let Some(surface) = pointer.current_focus() else {
126+ return;
127+ };
128+ let state = with_pointer_constraint(&surface, &pointer, |c| {
129+ c.map(|c| (c.is_active(), c.region().cloned()))
130+ });
131+ let Some((active, region)) = state else {
132+ return;
133+ };
134+ let eligible = self.constraint_eligible(&surface);
135+ if active && !eligible {
136+ self.release_constraint(&surface, &pointer);
137+ } else if !active && eligible && self.pointer_in(&surface, region.as_ref()) {
138+ let locked = with_pointer_constraint(&surface, &pointer, |c| {
139+ c.inspect(|c| c.activate())
140+ .is_some_and(|c| matches!(&*c, PointerConstraint::Locked(_)))
141+ });
142+ if locked {
143+ self.input.hover = None;
144+ self.dirty.cursor = true;
145+ }
146+ }
147+ }
148+
149+ fn pointer_in(&self, surface: &WlSurface, region: Option<&RegionAttributes>) -> bool {
150+ let pos = self.input.pointer_pos;
151+ let Some(origin) = self.origin_of(surface, pos) else {
152+ return false;
153+ };
154+ region.is_none_or(|r| r.contains((pos - origin).to_i32_floor()))
155+ }
156+
157+ fn release_constraint(&mut self, surface: &WlSurface, pointer: &PointerHandle<State>) {
158+ let hint = with_pointer_constraint(surface, pointer, |c| {
159+ let c = c?;
160+ let hint = match &*c {
161+ PointerConstraint::Locked(l) => l.cursor_position_hint(),
162+ PointerConstraint::Confined(_) => None,
163+ };
164+ c.deactivate();
165+ hint
166+ });
167+ if let Some(hint) = hint {
168+ self.warp_to_hint(surface, hint);
169+ }
170+ }
171+
172+ fn warp_to_hint(&mut self, surface: &WlSurface, hint: Point<f64, Logical>) {
173+ let Some(origin) = self.origin_of(surface, self.input.pointer_pos) else {
174+ return;
175+ };
176+ let target = origin + hint;
177+ if self.origin_of(surface, target).is_some() {
178+ self.warp_pointer(target, InputTime::now());
179+ }
180+ }
181+}
182+
183+impl PointerConstraintsHandler for State {
184+ fn new_constraint(&mut self, _surface: &WlSurface, _pointer: &PointerHandle<Self>) {
185+ self.sync_pointer_constraint();
186+ }
187+
188+ fn remove_constraint(
189+ &mut self,
190+ surface: &WlSurface,
191+ _pointer: &PointerHandle<Self>,
192+ remove: ConstraintRemove,
193+ ) {
194+ if let ConstraintRemove::Destroyed(constraint) = remove
195+ && constraint.is_active()
196+ && let PointerConstraint::Locked(lock) = &constraint
197+ && let Some(hint) = lock.cursor_position_hint()
198+ {
199+ self.warp_to_hint(surface, hint);
200+ }
201+ }
202+}
Modified file: crates/monolith/src/globals.rs+3 additions
@@ -21,6 +21,7 @@ use smithay::wayland::image_capture_source::{
2121 };
2222 use smithay::wayland::image_copy_capture::ImageCopyCaptureState;
2323 use smithay::wayland::output::OutputManagerState;
24+use smithay::wayland::pointer_constraints::PointerConstraintsState;
2425 use smithay::wayland::presentation::PresentationState;
2526 use smithay::wayland::relative_pointer::RelativePointerManagerState;
2627 use smithay::wayland::selection::data_device::DataDeviceState;
@@ -72,6 +73,7 @@ pub struct Protocols {
7273 _fractional_scale: FractionalScaleManagerState,
7374 _presentation: PresentationState,
7475 _relative_pointer: RelativePointerManagerState,
76+ _pointer_constraints: PointerConstraintsState,
7577 }
7678
7779 impl Protocols {
@@ -137,6 +139,7 @@ impl Protocols {
137139 _fractional_scale: FractionalScaleManagerState::new::<State>(dh),
138140 _presentation: PresentationState::new::<State>(dh, Monotonic::ID as u32),
139141 _relative_pointer: RelativePointerManagerState::new::<State>(dh),
142+ _pointer_constraints: PointerConstraintsState::new::<State>(dh),
140143 }
141144 }
142145 }
Modified file: crates/monolith/src/input.rs+18 additions−2 deletions
@@ -1,6 +1,7 @@
11 //! Input handling shared by every backend. Backends translate their events
22 //! into these calls, with pointer positions in global logical coordinates.
33
4+use crate::constraints::Constraint;
45 use crate::state::{Input, State};
56 use crate::wm::Resizing;
67 use monolith_layout::{Axis, Dir, Layout, MIN_FLOAT_SIZE, Rect, WindowId, clamp_float};
@@ -41,7 +42,19 @@ impl State {
4142 }
4243 Motion::Warp => Default::default(),
4344 };
44- let pos = wanted;
45+ let constraint = self.pointer_constraint();
46+ if let Some(Constraint::Locked) = constraint {
47+ self.activity();
48+ let under = self.surface_under(self.input.pointer_pos);
49+ self.relative_motion(under, delta, unaccel, time);
50+ return;
51+ }
52+ let pos = match &constraint {
53+ Some(Constraint::Confined { surface, region }) => {
54+ self.confine(surface, region.as_ref(), self.input.pointer_pos, wanted)
55+ }
56+ _ => wanted,
57+ };
4558 self.input.pointer_pos = pos;
4659 self.follow_pointer_scale();
4760 self.activity();
@@ -73,6 +86,7 @@ impl State {
7386 self.input.cursor_status = CursorImageStatus::default_named();
7487 self.dirty.redraw = true;
7588 }
89+ self.sync_pointer_constraint();
7690 }
7791
7892 fn relative_motion(
@@ -125,7 +139,8 @@ impl State {
125139 return;
126140 }
127141 }
128- if pressed == ButtonState::Pressed && !self.services.lock.active() {
142+ if pressed == ButtonState::Pressed && !self.services.lock.active() && !self.pointer_locked()
143+ {
129144 focus_under_pointer(self);
130145 if start_drag(self, button) {
131146 return;
@@ -297,6 +312,7 @@ impl State {
297312 || !self.settings.config.general.mouse_follows_focus
298313 || self.backend.drm().is_none()
299314 || self.services.lock.active()
315+ || self.pointer_locked()
300316 {
301317 return;
302318 }
Modified file: crates/monolith/src/main.rs+1 additions
@@ -4,6 +4,7 @@ mod backend;
44 mod capture;
55 mod commands;
66 mod commit;
7+mod constraints;
78 mod cursor;
89 mod devices;
910 mod drm;
Modified file: crates/monolith/src/seat.rs−17 deletions
@@ -169,20 +169,3 @@ impl WaylandDndGrabHandler for State {
169169 }
170170 }
171171 }
172-
173-impl smithay::wayland::pointer_constraints::PointerConstraintsHandler for State {
174- fn new_constraint(
175- &mut self,
176- _surface: &WlSurface,
177- _pointer: &smithay::input::pointer::PointerHandle<Self>,
178- ) {
179- }
180-
181- fn cursor_position_hint(
182- &mut self,
183- _surface: &WlSurface,
184- _pointer: &smithay::input::pointer::PointerHandle<Self>,
185- _location: smithay::utils::Point<f64, smithay::utils::Logical>,
186- ) {
187- }
188-}
Modified file: crates/monolith/src/state.rs+2 additions−1 deletions
@@ -468,8 +468,9 @@ impl State {
468468 if let Some(keyboard) = self.input.seat.get_keyboard()
469469 && keyboard.current_focus() != target
470470 {
471- keyboard.set_focus(self, target, SERIAL_COUNTER.next_serial());
471+ keyboard.set_focus(self, target.clone(), SERIAL_COUNTER.next_serial());
472472 }
473+ self.sync_pointer_constraint();
473474 }
474475
475476 pub fn reconfigure_keyboard(&mut self) {