Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
src/server/cursor.rs (218.3K)
1 use crate::ffi;
2 use crate::seat::{Seat, Focus};
3 use crate::server::{WlListener, wl_listener_remove, wl_signal_add, WlList};
4 use crate::scene_node_data::SceneNodeDataVal;
5 use crate::drag_icon::DragIcon;
6 use std::collections::{HashMap, HashSet};
7
8 pub use crate::touch::{TouchPoint, TouchRoute};
9
10 pub struct Cursor {
11 pub seat: *mut Seat,
12 pub wlr_cursor: *mut ffi::wlr_cursor,
13 pub xcursor_manager: *mut ffi::wlr_xcursor_manager,
14 pub constraint: *mut crate::pointer_constraint::PointerConstraint,
15
16 /// Trackpad-to-view-drag emulation for the apps `touchpad_view_apps`
17 /// names (see `ViewDrag`); `None` while no emulated drag is in progress.
18 pub view_drag: Option<ViewDrag>,
19 /// Trackpad-to-wheel emulation over those apps' popups (see
20 /// `PopupWheel`); `None` while no such gesture is in progress. The two
21 /// are mutually exclusive — a view drag needs a window under the
22 /// pointer, this one an override-redirect surface.
23 pub popup_wheel: Option<PopupWheel>,
24 /// Horizontal-scroll-as-Shift+vertical emulation over the apps
25 /// `touchpad_hscroll_shift_apps` names (see `HScrollShift`); `None`
26 /// while no such gesture is in progress.
27 pub hscroll_shift: Option<HScrollShift>,
28 /// Ends an emulated gesture — a `ViewDrag` or a `PopupWheel`, which
29 /// never run at once — that has seen no finger event for a while: a
30 /// two-finger scroll normally ends with a zero-delta axis event, but
31 /// not every path delivers one. Each arms it with its own timeout.
32 pub view_drag_timer: *mut ffi::wl_event_source,
33
34 /// Animated-XCursor playback. An XCursor theme may ship several images per
35 /// cursor with a per-frame `delay`; wlroots parses them but never advances
36 /// them — `wlr_xcursor_frame` has no callers inside wlroots, so driving the
37 /// animation is the compositor's job. This timer is the driver.
38 ///
39 /// `anim_xcursor` doubles as the "is anything animating" flag: it is null
40 /// for every static cursor, which is the overwhelmingly common case, and
41 /// the timer then stays disarmed and costs nothing. It borrows from
42 /// `xcursor_manager`'s theme, so it MUST be cleared before that manager is
43 /// destroyed (see `set_theme`) and whenever anything else takes over this
44 /// seat's cursor image (a client surface). Tablet tools are not such a
45 /// case: each drives its own `wlr_cursor`, never the seat's.
46 pub anim_timer: *mut ffi::wl_event_source,
47 pub anim_xcursor: *mut ffi::wlr_xcursor,
48 /// Wall-clock ms at which the current animation started, so elapsed time
49 /// (not absolute time) picks the frame and every cursor starts at frame 0.
50 pub anim_started_msec: u32,
51
52 pub motion_listener: ffi::wl_listener,
53 pub motion_absolute_listener: ffi::wl_listener,
54 pub button_listener: ffi::wl_listener,
55 pub axis_listener: ffi::wl_listener,
56 pub frame_listener: ffi::wl_listener,
57
58 pub tablet_tool_axis_listener: ffi::wl_listener,
59 pub tablet_tool_proximity_listener: ffi::wl_listener,
60 pub tablet_tool_tip_listener: ffi::wl_listener,
61 pub tablet_tool_button_listener: ffi::wl_listener,
62
63 /// Every finger on a touchscreen, by touch id: where it is (layout
64 /// coordinates; the drag icon of a touch drag follows it) and where its
65 /// events go, decided once at touch-down (`TouchRoute`).
66 pub touch_points: HashMap<i32, TouchPoint>,
67 /// The cursor image is off because the last input was a touchscreen.
68 /// A finger has no pointer to show, and the emulated pointer warping to
69 /// every tap would otherwise leave an arrow wherever the hand last was.
70 /// Real pointer motion brings it back (`unhide_after_touch`).
71 pub hidden_by_touch: bool,
72 /// A gesture the compositor has taken from the touchscreen: an edge
73 /// swipe, a desk pan/zoom, or a three/four-finger swipe or pinch
74 /// (`touch::Claim`). While one is live, every finger is its.
75 pub touch_claim: crate::touch::Claim,
76 /// The swipe being run through `handle_swipe_*` comes from the
77 /// touchscreen (`touch::Claim::Multi`), not a touchpad: the trackpad's
78 /// gesture switches do not gate it, and no client hears it as a
79 /// pointer gesture.
80 pub gesture_from_touch: bool,
81 pub pressed: HashMap<u32, Option<*mut crate::pointer_binding::PointerBinding>>,
82 /// Buttons whose PRESS was forwarded to the focused client. The paired
83 /// release must reach the client no matter what the compositor is doing
84 /// by then (seat op, overview, …) — an orphaned press wedges client-side
85 /// input state (widget routers keep a grab armed forever).
86 pub notified_pressed: HashSet<u32>,
87 /// Layout origin of the surface the first notified press landed on: the
88 /// implicit grab's frame of reference, so held-button motion stays
89 /// surface-relative wherever the pointer goes (passthrough's grab branch).
90 pub grab_origin: (f64, f64),
91 /// Surface units per layout pixel for the grabbed surface, frozen at
92 /// press: 1 for a buffer shown at its natural size, the output scale
93 /// for an X11 surface under xwayland_hidpi (physical-pixel buffer drawn
94 /// at 1/scale), 1/zoom in the overview. Without it a held-button drag
95 /// reached an X11 client at half speed.
96 pub grab_scale: f64,
97
98 pub touch_down_listener: ffi::wl_listener,
99 pub touch_motion_listener: ffi::wl_listener,
100 pub touch_up_listener: ffi::wl_listener,
101 pub touch_cancel_listener: ffi::wl_listener,
102 pub touch_frame_listener: ffi::wl_listener,
103
104 pub swipe_begin_listener: ffi::wl_listener,
105 pub swipe_update_listener: ffi::wl_listener,
106 pub swipe_end_listener: ffi::wl_listener,
107
108 pub pinch_begin_listener: ffi::wl_listener,
109 pub pinch_update_listener: ffi::wl_listener,
110 pub pinch_end_listener: ffi::wl_listener,
111
112 pub hold_begin_listener: ffi::wl_listener,
113 pub hold_end_listener: ffi::wl_listener,
114
115 pub gesture_dx: f64,
116 pub gesture_dy: f64,
117 pub gesture_scale: f64,
118 /// A bind has fired during the in-flight swipe or pinch. For a swipe
119 /// it does not end the gesture — the travel restarts and can fire
120 /// again — it records that clients were sent a cancelled end and hear
121 /// nothing more of it; a pinch fires once.
122 pub gesture_triggered: bool,
123 /// The in-flight swipe fired a bind that is not a step (anything
124 /// `action_navigates` rejects: the overview toggle, a spawn): the rest
125 /// of the gesture fires nothing more, so one swipe toggles the
126 /// overview once however far the fingers go. Cleared at swipe begin.
127 pub swipe_spent: bool,
128 /// A focus bind the in-flight swipe reached with no window that way
129 /// (`WindowManager::focus_toward_lands`): it does not fire, and while
130 /// the swipe keeps matching it the lean holds at its limit rather than
131 /// stepping. Cleared at swipe begin and by a step that lands.
132 pub swipe_dead_end: Option<crate::config::Action>,
133 /// Camera offset (virtual units, `[x, y]`) the in-flight swipe has
134 /// peeked the desktop by so far — see `swipe_peek_for`. Zero outside a
135 /// swipe, and restarts from zero at each fire, like the travel.
136 pub swipe_peek: [f64; 2],
137 /// Finger count of a staged injected swipe (`pointer-swipe begin`),
138 /// carried into its updates the way libinput repeats it per event.
139 pub inject_swipe_fingers: u32,
140 pub panning_gesture_active: bool,
141 /// What `pointer-scroll ... natural` sets: an injected finger scroll
142 /// (no device behind it) reads as coming from a natural-scrolling
143 /// touchpad, so the view drag's un-inversion can be exercised headlessly.
144 pub inject_natural: bool,
145 /// Finger-pan velocity estimate per axis (`[x, y]`, virtual units/s) and
146 /// the hardware timestamp of each axis's last finger event, for the
147 /// kinetic desktop coast on the lift.
148 pub pan_vel: [f64; 2],
149 pub pan_last_msec: [u32; 2],
150 /// A pinch that began over the desktop background zooms the camera
151 /// continuously instead of forwarding to a client or matching gesture
152 /// binds. Decided once at pinch begin; update/end must follow the same
153 /// branch so the client protocol never sees an update without a begin.
154 pub pinch_zoom_active: bool,
155 /// Camera zoom captured at pinch begin: libinput's pinch `scale` is
156 /// absolute w.r.t. the gesture start, so every update maps off this.
157 pub pinch_start_zoom: f64,
158 pub last_click_time: u32,
159 pub last_click_window: *mut crate::window::Window,
160 /// Window whose border currently draws highlighted, kept to un-highlight
161 /// on hover transitions. May dangle after a close — validate against
162 /// `wm.windows` before dereferencing.
163 pub hovered_border_window: *mut crate::window::Window,
164 /// Which of that window's handle discs is highlighted.
165 pub hovered_border_element: Option<crate::window::BorderElement>,
166 /// The toplevel under the pointer while adjust mode (overview, or Super
167 /// held) is on: the window the ring lands on and whose handles are live,
168 /// focused or not. Set by `passthrough` on every hover evaluation and
169 /// cleared when the pointer rests on nothing adjustable or the mode is
170 /// off — see `Window::is_adjust_target`. May dangle after a close:
171 /// compared by address only, and nulled in `Window::destroy`.
172 pub adjust_hover: *mut crate::window::Window,
173 pub right_click_on_bg: bool,
174 pub right_click_on_border: bool,
175 /// A left press on a window button (minimize / maximize / float-tile),
176 /// waiting for its release: a button acts on the release, and only when
177 /// the pointer is still on the same button of the same window. May
178 /// dangle after a close — validated against `wm.windows` at release.
179 pub button_press: Option<(*mut crate::window::Window, crate::window::BorderElement)>,
180 /// The grid surface's node mapping — (node_x, node_y, scale) — FROZEN at
181 /// the start of an implicit grab that landed on the grid, cleared when
182 /// the grab's last button releases. Motion during the grab is mapped
183 /// against these values, never the live node: the camera moves the node
184 /// per frame, and a flight mid-grab (overview enter/exit) read as
185 /// thousands of px of "pointer motion" under a live mapping — one click
186 /// on a desktop item during a flight flung it off the canvas.
187 pub grab_grid: Option<(f64, f64, f64)>,
188 }
189
190 impl Default for Cursor {
191 fn default() -> Self {
192 Self {
193 seat: std::ptr::null_mut(),
194 wlr_cursor: std::ptr::null_mut(),
195 xcursor_manager: std::ptr::null_mut(),
196 constraint: std::ptr::null_mut(),
197 anim_timer: std::ptr::null_mut(),
198 anim_xcursor: std::ptr::null_mut(),
199 anim_started_msec: 0,
200 motion_listener: unsafe { std::mem::zeroed() },
201 motion_absolute_listener: unsafe { std::mem::zeroed() },
202 button_listener: unsafe { std::mem::zeroed() },
203 axis_listener: unsafe { std::mem::zeroed() },
204 frame_listener: unsafe { std::mem::zeroed() },
205 tablet_tool_axis_listener: unsafe { std::mem::zeroed() },
206 tablet_tool_proximity_listener: unsafe { std::mem::zeroed() },
207 tablet_tool_tip_listener: unsafe { std::mem::zeroed() },
208 tablet_tool_button_listener: unsafe { std::mem::zeroed() },
209
210 touch_points: HashMap::new(),
211 hidden_by_touch: false,
212 touch_claim: crate::touch::Claim::None,
213 gesture_from_touch: false,
214 pressed: HashMap::new(),
215 notified_pressed: HashSet::new(),
216 grab_origin: (0.0, 0.0),
217 grab_scale: 1.0,
218
219 touch_down_listener: unsafe { std::mem::zeroed() },
220 touch_motion_listener: unsafe { std::mem::zeroed() },
221 touch_up_listener: unsafe { std::mem::zeroed() },
222 touch_cancel_listener: unsafe { std::mem::zeroed() },
223 touch_frame_listener: unsafe { std::mem::zeroed() },
224
225 swipe_begin_listener: unsafe { std::mem::zeroed() },
226 swipe_update_listener: unsafe { std::mem::zeroed() },
227 swipe_end_listener: unsafe { std::mem::zeroed() },
228
229 pinch_begin_listener: unsafe { std::mem::zeroed() },
230 pinch_update_listener: unsafe { std::mem::zeroed() },
231 pinch_end_listener: unsafe { std::mem::zeroed() },
232
233 hold_begin_listener: unsafe { std::mem::zeroed() },
234 hold_end_listener: unsafe { std::mem::zeroed() },
235
236 gesture_dx: 0.0,
237 gesture_dy: 0.0,
238 gesture_scale: 1.0,
239 gesture_triggered: false,
240 swipe_spent: false,
241 swipe_dead_end: None,
242 swipe_peek: [0.0, 0.0],
243 inject_swipe_fingers: 3,
244 panning_gesture_active: false,
245 inject_natural: false,
246 pan_vel: [0.0, 0.0],
247 pan_last_msec: [0, 0],
248 pinch_zoom_active: false,
249 pinch_start_zoom: 1.0,
250 view_drag: None,
251 popup_wheel: None,
252 hscroll_shift: None,
253 view_drag_timer: std::ptr::null_mut(),
254 last_click_time: 0,
255 last_click_window: std::ptr::null_mut(),
256 hovered_border_window: std::ptr::null_mut(),
257 hovered_border_element: None,
258 adjust_hover: std::ptr::null_mut(),
259 right_click_on_bg: false,
260 right_click_on_border: false,
261 button_press: None,
262 grab_grid: None,
263 }
264 }
265 }
266
267 impl Cursor {
268 pub unsafe fn init(
269 &mut self,
270 seat: *mut Seat,
271 output_layout: *mut ffi::wlr_output_layout,
272 ) -> Result<(), &'static str> {
273 let wlr_cursor = ffi::wlr_cursor_create();
274 if wlr_cursor.is_null() {
275 return Err("Failed to create wlr_cursor");
276 }
277 ffi::wlr_cursor_attach_output_layout(wlr_cursor, output_layout);
278
279 let xcursor_manager = ffi::wlr_xcursor_manager_create(std::ptr::null(), 24);
280 if xcursor_manager.is_null() {
281 ffi::wlr_cursor_destroy(wlr_cursor);
282 return Err("Failed to create wlr_xcursor_manager");
283 }
284
285 self.seat = seat;
286 self.wlr_cursor = wlr_cursor;
287 self.xcursor_manager = xcursor_manager;
288
289 // The animated-cursor driver. Created disarmed; `set_xcursor` arms it
290 // only for a cursor that actually has more than one frame.
291 let event_loop = ffi::wl_display_get_event_loop((*(*seat).server).wl_server);
292 let anim_timer = ffi::wl_event_loop_add_timer(
293 event_loop,
294 Some(handle_xcursor_anim),
295 self as *mut Cursor as *mut _,
296 );
297 if anim_timer.is_null() {
298 ffi::wlr_xcursor_manager_destroy(xcursor_manager);
299 ffi::wlr_cursor_destroy(wlr_cursor);
300 self.xcursor_manager = std::ptr::null_mut();
301 self.wlr_cursor = std::ptr::null_mut();
302 return Err("Failed to create xcursor animation timer");
303 }
304 self.anim_timer = anim_timer;
305 self.view_drag_timer = ffi::wl_event_loop_add_timer(
306 event_loop,
307 Some(handle_view_drag_timeout),
308 self as *mut Cursor as *mut _,
309 );
310
311 // Load default cursor theme
312 ffi::wlr_xcursor_manager_load(xcursor_manager, 1.0);
313 self.set_xcursor(b"default\0".as_ptr() as *const _);
314
315 // Setup listeners
316 let motion_ptr = &mut self.motion_listener as *mut ffi::wl_listener as *mut WlListener;
317 (*motion_ptr).notify = Some(handle_motion);
318 wl_signal_add(
319 ffi::river_wlr_cursor_get_motion_signal(wlr_cursor),
320 &mut self.motion_listener,
321 );
322
323 let absolute_ptr = &mut self.motion_absolute_listener as *mut ffi::wl_listener as *mut WlListener;
324 (*absolute_ptr).notify = Some(handle_motion_absolute);
325 wl_signal_add(
326 ffi::river_wlr_cursor_get_motion_absolute_signal(wlr_cursor),
327 &mut self.motion_absolute_listener,
328 );
329
330 let button_ptr = &mut self.button_listener as *mut ffi::wl_listener as *mut WlListener;
331 (*button_ptr).notify = Some(handle_button);
332 wl_signal_add(
333 ffi::river_wlr_cursor_get_button_signal(wlr_cursor),
334 &mut self.button_listener,
335 );
336
337 let axis_ptr = &mut self.axis_listener as *mut ffi::wl_listener as *mut WlListener;
338 (*axis_ptr).notify = Some(handle_axis);
339 wl_signal_add(
340 ffi::river_wlr_cursor_get_axis_signal(wlr_cursor),
341 &mut self.axis_listener,
342 );
343
344 let frame_ptr = &mut self.frame_listener as *mut ffi::wl_listener as *mut WlListener;
345 (*frame_ptr).notify = Some(handle_frame);
346 wl_signal_add(
347 ffi::river_wlr_cursor_get_frame_signal(wlr_cursor),
348 &mut self.frame_listener,
349 );
350
351 let tablet_axis_ptr = &mut self.tablet_tool_axis_listener as *mut ffi::wl_listener as *mut WlListener;
352 (*tablet_axis_ptr).notify = Some(handle_tablet_tool_axis);
353 wl_signal_add(
354 ffi::river_wlr_cursor_get_tablet_tool_axis_signal(wlr_cursor),
355 &mut self.tablet_tool_axis_listener,
356 );
357
358 let tablet_proximity_ptr = &mut self.tablet_tool_proximity_listener as *mut ffi::wl_listener as *mut WlListener;
359 (*tablet_proximity_ptr).notify = Some(handle_tablet_tool_proximity);
360 wl_signal_add(
361 ffi::river_wlr_cursor_get_tablet_tool_proximity_signal(wlr_cursor),
362 &mut self.tablet_tool_proximity_listener,
363 );
364
365 let tablet_tip_ptr = &mut self.tablet_tool_tip_listener as *mut ffi::wl_listener as *mut WlListener;
366 (*tablet_tip_ptr).notify = Some(handle_tablet_tool_tip);
367 wl_signal_add(
368 ffi::river_wlr_cursor_get_tablet_tool_tip_signal(wlr_cursor),
369 &mut self.tablet_tool_tip_listener,
370 );
371
372 let tablet_button_ptr = &mut self.tablet_tool_button_listener as *mut ffi::wl_listener as *mut WlListener;
373 (*tablet_button_ptr).notify = Some(handle_tablet_tool_button);
374 wl_signal_add(
375 ffi::river_wlr_cursor_get_tablet_tool_button_signal(wlr_cursor),
376 &mut self.tablet_tool_button_listener,
377 );
378
379 let touch_down_ptr = &mut self.touch_down_listener as *mut ffi::wl_listener as *mut WlListener;
380 (*touch_down_ptr).notify = Some(crate::touch::handle_touch_down);
381 wl_signal_add(
382 ffi::river_wlr_cursor_get_touch_down_signal(wlr_cursor),
383 &mut self.touch_down_listener,
384 );
385
386 let touch_motion_ptr = &mut self.touch_motion_listener as *mut ffi::wl_listener as *mut WlListener;
387 (*touch_motion_ptr).notify = Some(crate::touch::handle_touch_motion);
388 wl_signal_add(
389 ffi::river_wlr_cursor_get_touch_motion_signal(wlr_cursor),
390 &mut self.touch_motion_listener,
391 );
392
393 let touch_up_ptr = &mut self.touch_up_listener as *mut ffi::wl_listener as *mut WlListener;
394 (*touch_up_ptr).notify = Some(crate::touch::handle_touch_up);
395 wl_signal_add(
396 ffi::river_wlr_cursor_get_touch_up_signal(wlr_cursor),
397 &mut self.touch_up_listener,
398 );
399
400 let touch_cancel_ptr = &mut self.touch_cancel_listener as *mut ffi::wl_listener as *mut WlListener;
401 (*touch_cancel_ptr).notify = Some(crate::touch::handle_touch_cancel);
402 wl_signal_add(
403 ffi::river_wlr_cursor_get_touch_cancel_signal(wlr_cursor),
404 &mut self.touch_cancel_listener,
405 );
406
407 let touch_frame_ptr = &mut self.touch_frame_listener as *mut ffi::wl_listener as *mut WlListener;
408 (*touch_frame_ptr).notify = Some(crate::touch::handle_touch_frame);
409 wl_signal_add(
410 ffi::river_wlr_cursor_get_touch_frame_signal(wlr_cursor),
411 &mut self.touch_frame_listener,
412 );
413
414 let swipe_begin_ptr = &mut self.swipe_begin_listener as *mut ffi::wl_listener as *mut WlListener;
415 (*swipe_begin_ptr).notify = Some(handle_swipe_begin);
416 wl_signal_add(
417 ffi::river_wlr_cursor_get_swipe_begin_signal(wlr_cursor),
418 &mut self.swipe_begin_listener,
419 );
420
421 let swipe_update_ptr = &mut self.swipe_update_listener as *mut ffi::wl_listener as *mut WlListener;
422 (*swipe_update_ptr).notify = Some(handle_swipe_update);
423 wl_signal_add(
424 ffi::river_wlr_cursor_get_swipe_update_signal(wlr_cursor),
425 &mut self.swipe_update_listener,
426 );
427
428 let swipe_end_ptr = &mut self.swipe_end_listener as *mut ffi::wl_listener as *mut WlListener;
429 (*swipe_end_ptr).notify = Some(handle_swipe_end);
430 wl_signal_add(
431 ffi::river_wlr_cursor_get_swipe_end_signal(wlr_cursor),
432 &mut self.swipe_end_listener,
433 );
434
435 let pinch_begin_ptr = &mut self.pinch_begin_listener as *mut ffi::wl_listener as *mut WlListener;
436 (*pinch_begin_ptr).notify = Some(handle_pinch_begin);
437 wl_signal_add(
438 ffi::river_wlr_cursor_get_pinch_begin_signal(wlr_cursor),
439 &mut self.pinch_begin_listener,
440 );
441
442 let pinch_update_ptr = &mut self.pinch_update_listener as *mut ffi::wl_listener as *mut WlListener;
443 (*pinch_update_ptr).notify = Some(handle_pinch_update);
444 wl_signal_add(
445 ffi::river_wlr_cursor_get_pinch_update_signal(wlr_cursor),
446 &mut self.pinch_update_listener,
447 );
448
449 let pinch_end_ptr = &mut self.pinch_end_listener as *mut ffi::wl_listener as *mut WlListener;
450 (*pinch_end_ptr).notify = Some(handle_pinch_end);
451 wl_signal_add(
452 ffi::river_wlr_cursor_get_pinch_end_signal(wlr_cursor),
453 &mut self.pinch_end_listener,
454 );
455
456 let hold_begin_ptr = &mut self.hold_begin_listener as *mut ffi::wl_listener as *mut WlListener;
457 (*hold_begin_ptr).notify = Some(handle_hold_begin);
458 wl_signal_add(
459 ffi::river_wlr_cursor_get_hold_begin_signal(wlr_cursor),
460 &mut self.hold_begin_listener,
461 );
462
463 let hold_end_ptr = &mut self.hold_end_listener as *mut ffi::wl_listener as *mut WlListener;
464 (*hold_end_ptr).notify = Some(handle_hold_end);
465 wl_signal_add(
466 ffi::river_wlr_cursor_get_hold_end_signal(wlr_cursor),
467 &mut self.hold_end_listener,
468 );
469
470 Ok(())
471 }
472
473 pub unsafe fn deinit(&mut self) {
474 wl_listener_remove(&mut self.motion_listener);
475 wl_listener_remove(&mut self.motion_absolute_listener);
476 wl_listener_remove(&mut self.button_listener);
477 wl_listener_remove(&mut self.axis_listener);
478 wl_listener_remove(&mut self.frame_listener);
479
480 wl_listener_remove(&mut self.tablet_tool_axis_listener);
481 wl_listener_remove(&mut self.tablet_tool_proximity_listener);
482 wl_listener_remove(&mut self.tablet_tool_tip_listener);
483 wl_listener_remove(&mut self.tablet_tool_button_listener);
484
485 wl_listener_remove(&mut self.touch_down_listener);
486 wl_listener_remove(&mut self.touch_motion_listener);
487 wl_listener_remove(&mut self.touch_up_listener);
488 wl_listener_remove(&mut self.touch_cancel_listener);
489 wl_listener_remove(&mut self.touch_frame_listener);
490
491 wl_listener_remove(&mut self.swipe_begin_listener);
492 wl_listener_remove(&mut self.swipe_update_listener);
493 wl_listener_remove(&mut self.swipe_end_listener);
494
495 wl_listener_remove(&mut self.pinch_begin_listener);
496 wl_listener_remove(&mut self.pinch_update_listener);
497 wl_listener_remove(&mut self.pinch_end_listener);
498
499 wl_listener_remove(&mut self.hold_begin_listener);
500 wl_listener_remove(&mut self.hold_end_listener);
501
502 self.stop_xcursor_animation();
503 if !self.anim_timer.is_null() {
504 ffi::wl_event_source_remove(self.anim_timer);
505 self.anim_timer = std::ptr::null_mut();
506 }
507
508 if !self.xcursor_manager.is_null() {
509 ffi::wlr_xcursor_manager_destroy(self.xcursor_manager);
510 self.xcursor_manager = std::ptr::null_mut();
511 }
512 if !self.wlr_cursor.is_null() {
513 ffi::wlr_cursor_destroy(self.wlr_cursor);
514 self.wlr_cursor = std::ptr::null_mut();
515 }
516 }
517
518 pub unsafe fn x(&self) -> f64 {
519 ffi::river_wlr_cursor_get_x(self.wlr_cursor)
520 }
521
522 pub unsafe fn y(&self) -> f64 {
523 ffi::river_wlr_cursor_get_y(self.wlr_cursor)
524 }
525
526 pub unsafe fn update_hovered(&mut self) {
527 // Keyboard focus remains stable on pointer hover/motion.
528 // It is only changed on mapping, click, touch, or shortcut focus transitions.
529 }
530
531 pub unsafe fn update_state(&mut self) {
532 if !self.constraint.is_null() {
533 (*self.constraint).update_state();
534 }
535 self.update_hovered();
536 self.passthrough(crate::util::msec_timestamp());
537 }
538
539 /// Re-evaluate pointer focus at the current position after the scene
540 /// mapping changed beneath a STATIONARY cursor — a commit moved/resized a
541 /// surface or re-anchored its window geometry. No motion fired (the
542 /// cursor did not move), so enter/leave and the surface-local coordinates
543 /// would otherwise go stale and the next click could be dispatched
544 /// against the old mapping or dropped entirely (the cce-ui overflow-rim
545 /// popovers exposed exactly this). Skips seats mid-op: the compositor
546 /// owns the pointer during a drag/resize op and `op_end_pointer` restores
547 /// focus itself; an implicit client grab is already handled inside
548 /// `passthrough`.
549 pub unsafe fn refresh_after_scene_change(&mut self) {
550 if (*self.seat).op.is_some() {
551 return;
552 }
553 self.update_state();
554 }
555
556 pub unsafe fn update_drag_icons(&mut self) {
557 let drag_icons_tree = (*(*self.seat).server).scene.drag_icons;
558 let children_head = ffi::river_scene_tree_get_children(drag_icons_tree) as *mut WlList;
559 let mut curr = (*children_head).next;
560 while curr != children_head {
561 let next = (*curr).next;
562 let node = ffi::river_scene_node_from_children_link(curr as *mut ffi::wl_list);
563 let drag_icon_raw = ffi::river_scene_node_get_data(node) as *mut DragIcon;
564 if !drag_icon_raw.is_null() {
565 let drag_icon = &mut *drag_icon_raw;
566 let drag_seat = ffi::river_wlr_drag_get_seat((*drag_icon.wlr_drag_icon).drag);
567 if drag_seat == (*self.seat).wlr_seat {
568 drag_icon.update_position(self);
569 }
570 }
571 curr = next;
572 }
573 }
574
575 pub unsafe fn set_theme(
576 &mut self,
577 theme: *const std::os::raw::c_char,
578 size: u32,
579 ) -> Result<(), &'static str> {
580 let size = if size == 0 { 24 } else { size };
581
582 let xcursor_manager = ffi::wlr_xcursor_manager_create(theme, size);
583 if xcursor_manager.is_null() {
584 return Err("Failed to create wlr_xcursor_manager");
585 }
586
587 // If this cursor belongs to the default seat, update the Xwayland cursor to match the theme.
588 let default_seat = (*(*self.seat).server).input_manager.default_seat;
589 if self.seat == default_seat {
590 if !(*(*self.seat).server).xwayland.is_null() {
591 ffi::wlr_xcursor_manager_load(xcursor_manager, 1.0);
592 let wlr_xcursor = ffi::wlr_xcursor_manager_get_xcursor(
593 xcursor_manager,
594 b"default\0".as_ptr() as *const _,
595 1.0,
596 );
597 if !wlr_xcursor.is_null() && (*wlr_xcursor).image_count > 0 {
598 let image = *(*wlr_xcursor).images;
599 if !image.is_null() {
600 let buffer = ffi::wlr_xcursor_image_get_buffer(image);
601 ffi::wlr_xwayland_set_cursor(
602 (*(*self.seat).server).xwayland,
603 buffer,
604 (*image).hotspot_x as i32,
605 (*image).hotspot_y as i32,
606 );
607 }
608 }
609 }
610 }
611
612 // Before the old theme is freed: a running animation holds a pointer
613 // into its images.
614 self.stop_xcursor_animation();
615
616 if !self.xcursor_manager.is_null() {
617 ffi::wlr_xcursor_manager_destroy(self.xcursor_manager);
618 }
619 self.xcursor_manager = xcursor_manager;
620
621 ffi::wlr_xcursor_manager_load(self.xcursor_manager, 1.0);
622 self.set_xcursor(b"default\0".as_ptr() as *const _);
623
624 Ok(())
625 }
626
627 pub unsafe fn set_xcursor(&mut self, name: *const std::os::raw::c_char) {
628 if self.hidden_by_touch {
629 return;
630 }
631 ffi::wlr_cursor_set_xcursor(
632 self.wlr_cursor,
633 self.xcursor_manager,
634 name,
635 );
636 // `wlr_cursor_set_xcursor` paints frame 0 and stops there; if this
637 // cursor has more frames, take over from here.
638 self.start_xcursor_animation(name);
639 }
640
641 /// Stop any running xcursor animation and disarm the timer. Idempotent, and
642 /// safe to call when nothing is animating.
643 ///
644 /// Every path that hands this seat's cursor image to someone else must call
645 /// this: a client surface cursor, or a theme swap that frees the images
646 /// `anim_xcursor` borrows. The gate is deliberately on the
647 /// unsafe transitions rather than a list of states where animating is known
648 /// to be fine — a state nobody thought of must land on "stop", not on
649 /// "keep dereferencing a freed theme".
650 pub unsafe fn stop_xcursor_animation(&mut self) {
651 self.anim_xcursor = std::ptr::null_mut();
652 if !self.anim_timer.is_null() {
653 // 0 disarms a libwayland timer.
654 ffi::wl_event_source_timer_update(self.anim_timer, 0);
655 }
656 }
657
658 /// Begin driving `name`'s frames, if it has more than one. Static cursors —
659 /// every cursor in a stock theme — leave the timer disarmed.
660 unsafe fn start_xcursor_animation(&mut self, name: *const std::os::raw::c_char) {
661 if self.anim_timer.is_null() || self.xcursor_manager.is_null() {
662 self.stop_xcursor_animation();
663 return;
664 }
665
666 let xcursor = ffi::wlr_xcursor_manager_get_xcursor(self.xcursor_manager, name, 1.0);
667
668 // Already playing this exact cursor: leave its clock running. The
669 // compositor re-asserts its cursor constantly — `clear_focus` sets
670 // "default" on EVERY pointer motion over the background — and
671 // restarting here would re-arm the timer sooner than the frame delay
672 // and pin the animation on frame 0 for as long as the mouse moves.
673 // The pointer is stable per (theme, name, scale) for the manager's
674 // lifetime, and `set_theme` stops the animation before freeing the old
675 // manager, so a stale pointer can never compare equal.
676 if !xcursor.is_null() && xcursor == self.anim_xcursor {
677 return;
678 }
679
680 self.stop_xcursor_animation();
681 if xcursor.is_null() {
682 return;
683 }
684 // One image is a static cursor. `total_delay == 0` would also make
685 // `wlr_xcursor_frame`'s `time % total_delay` divide by zero, so a theme
686 // with all-zero delays must never reach the driver.
687 if (*xcursor).image_count <= 1 || (*xcursor).total_delay == 0 {
688 return;
689 }
690
691 self.anim_xcursor = xcursor;
692 self.anim_started_msec = crate::util::msec_timestamp();
693 self.arm_next_frame(0);
694 }
695
696 /// Arm the timer for the delay of frame `idx`, clamped to >= 1ms: a zero
697 /// delay on a single frame would re-arm instantly and spin the event loop.
698 unsafe fn arm_next_frame(&mut self, idx: u32) {
699 let xcursor = self.anim_xcursor;
700 if xcursor.is_null() || idx >= (*xcursor).image_count {
701 return;
702 }
703 let image = *(*xcursor).images.offset(idx as isize);
704 if image.is_null() {
705 return;
706 }
707 let delay = (*image).delay.max(1).min(i32::MAX as u32) as i32;
708 ffi::wl_event_source_timer_update(self.anim_timer, delay);
709 }
710
711 /// Timer body: pick the frame for the elapsed time and paint it.
712 ///
713 /// The frame is derived from elapsed wall-clock rather than a running
714 /// counter, so a late or coalesced timer resyncs to where the animation
715 /// should be instead of drifting further behind.
716 unsafe fn advance_xcursor_frame(&mut self) {
717 let xcursor = self.anim_xcursor;
718 if xcursor.is_null() || self.wlr_cursor.is_null() {
719 return;
720 }
721
722 let elapsed = crate::util::msec_timestamp().wrapping_sub(self.anim_started_msec);
723 let idx = ffi::wlr_xcursor_frame(xcursor, elapsed);
724 if idx < 0 || idx as u32 >= (*xcursor).image_count {
725 return;
726 }
727 let idx = idx as u32;
728
729 let image = *(*xcursor).images.offset(idx as isize);
730 if image.is_null() {
731 return;
732 }
733 let buffer = ffi::wlr_xcursor_image_get_buffer(image);
734 if !buffer.is_null() {
735 ffi::wlr_cursor_set_buffer(
736 self.wlr_cursor,
737 buffer,
738 (*image).hotspot_x as i32,
739 (*image).hotspot_y as i32,
740 1.0,
741 );
742 }
743 self.arm_next_frame(idx);
744 }
745
746 pub unsafe fn op_start_pointer(&mut self) {
747 if !self.constraint.is_null() {
748 if let crate::pointer_constraint::PointerConstraintState::Active { .. } = (*self.constraint).state {
749 (*self.constraint).deactivate();
750 }
751 }
752
753 log::debug!("entering cursor mode op");
754 ffi::wlr_seat_pointer_notify_clear_focus((*self.seat).wlr_seat);
755
756 // A grab does not focus the window it moves or resizes, and it was
757 // the press's `seat.focus` that used to raise a Floating one — so
758 // the raise happens here for whatever the op grabbed. (A window
759 // un-tiled by the drag is raised in `op_update` instead.)
760 let grabbed = match &(*self.seat).op {
761 Some(op) => op.window_ptr,
762 None => std::ptr::null_mut(),
763 };
764 if !grabbed.is_null()
765 && !(*grabbed).closed
766 && !(*grabbed).is_status_bar()
767 && (*grabbed).tiling_mode == crate::tiling::TilingMode::Floating
768 {
769 (*(*self.seat).server).wm.raise_window(grabbed);
770 (*(*self.seat).server).wm.dirty_windowing();
771 }
772 }
773
774 pub unsafe fn op_end_pointer(&mut self) {
775 if self.pressed.is_empty() {
776 log::debug!("entering cursor mode passthrough");
777 self.update_state();
778 } else {
779 log::debug!("entering cursor mode ignore");
780 }
781 }
782
783 /// Move the border hover highlight to `target`'s `element` (null/None to
784 /// clear): updates `hovered_border_element` on the old and new windows
785 /// and repaints their borders. The stored pointer may refer to a closed
786 /// window, so it is only dereferenced after checking it is still in
787 /// `wm.windows`.
788 pub unsafe fn set_border_hover(
789 &mut self,
790 target: *mut crate::window::Window,
791 element: Option<crate::window::BorderElement>,
792 ) {
793 if self.hovered_border_window == target && self.hovered_border_element == element {
794 return;
795 }
796 let old = self.hovered_border_window;
797 if !old.is_null() && old != target {
798 let wm = &(*(*self.seat).server).wm;
799 if wm.windows.iter().any(|&w| w == old) && !(*old).closed {
800 (*old).hovered_border_element = None;
801 }
802 }
803 self.hovered_border_window = target;
804 self.hovered_border_element = element;
805 if !target.is_null() {
806 (*target).hovered_border_element = element;
807 }
808 // Borders rest invisible and fade in, so the change in hover target is
809 // the start of an animation rather than a repaint: the fade timer
810 // repaints every affected window as it steps.
811 (*(*self.seat).server).wm.arm_border_fade();
812 }
813
814 /// Move the Super-held adjust target to `target` (null to clear). The
815 /// ring eases off the old window and onto the new one through the same
816 /// border fade a hover swap uses, so a change arms that timer.
817 pub unsafe fn set_adjust_hover(&mut self, target: *mut crate::window::Window) {
818 if self.adjust_hover == target {
819 return;
820 }
821 self.adjust_hover = target;
822 (*(*self.seat).server).wm.arm_border_fade();
823 }
824
825 pub unsafe fn passthrough(&mut self, time_msec: u32) {
826 let lx = self.x();
827 let ly = self.y();
828 let server = (*self.seat).server;
829
830 // Implicit grab (standard Wayland drag semantics): while any button
831 // the focused client saw pressed is still held, motion keeps flowing
832 // to that surface — relative to its origin at press time — wherever
833 // the pointer goes, so a client-side drag (slider, ramp key) tracks
834 // outside the window. Focus is neither re-evaluated nor cleared until
835 // the last such button releases; wlroots nulls the focused surface if
836 // it is destroyed mid-grab, which falls through to normal dispatch.
837 // A DRAG supersedes the implicit grab: wlroots' drag grab owns pointer
838 // focus for its duration, and the whole point is that focus follows
839 // the pointer onto whatever it is dragged over. Holding focus on the
840 // source here means the drag target never changes, so no drop is ever
841 // delivered anywhere — the source keeps receiving motion instead.
842 if !self.notified_pressed.is_empty() && (*self.seat).drag == crate::seat::DragState::None {
843 let focused =
844 ffi::river_wlr_seat_get_pointer_focused_surface((*self.seat).wlr_seat);
845 if !focused.is_null() {
846 // A grab held on the GRID maps through the surface node, not
847 // through the fixed origin: the offset formula assumes an
848 // unscaled surface, and the grid displays at the patch's
849 // resolution ratio — near 1, but off it whenever the zoom
850 // sits between pow2 quantization steps (most of overview) —
851 // so offset deltas would drag the item faster or slower than
852 // the pointer by exactly that ratio. The mapping is the one
853 // FROZEN at press time (`grab_grid`), never the live node:
854 // the camera moves the node per frame while the patch stays
855 // put, so a flight mid-grab (overview enter/exit — union
856 // patches are pre-issued, so the client's origin-change
857 // re-baseline never fires) read as the whole flight distance
858 // of "pointer motion" under a live mapping and flung the
859 // grabbed item off the canvas.
860 if let Some((nx, ny, scale)) = self.grab_grid {
861 // Still guarded on the grid owning the focus: if the
862 // grid remapped mid-grab (client restart), the frozen
863 // values must not map points for its replacement.
864 if let Some((gsurf, ..)) = grid_node_info(server) {
865 if gsurf == focused {
866 ffi::wlr_seat_pointer_notify_motion(
867 (*self.seat).wlr_seat,
868 time_msec,
869 (lx - nx) / scale,
870 (ly - ny) / scale,
871 );
872 return;
873 }
874 }
875 }
876 ffi::wlr_seat_pointer_notify_motion(
877 (*self.seat).wlr_seat,
878 time_msec,
879 (lx - self.grab_origin.0) * self.grab_scale,
880 (ly - self.grab_origin.1) * self.grab_scale,
881 );
882 return;
883 }
884 }
885
886 if let Some(result) = (*server).scene.at(lx, ly) {
887 let lock_state = (*server).lock_manager.state;
888 if lock_state != crate::lock_manager::LockState::Unlocked {
889 if !matches!(result.data, SceneNodeDataVal::LockSurface(_)) {
890 self.set_border_hover(std::ptr::null_mut(), None);
891 self.set_adjust_hover(std::ptr::null_mut());
892 self.clear_focus();
893 return;
894 }
895 } else {
896 if matches!(result.data, SceneNodeDataVal::LockSurface(_)) {
897 self.set_border_hover(std::ptr::null_mut(), None);
898 self.set_adjust_hover(std::ptr::null_mut());
899 self.clear_focus();
900 return;
901 }
902 }
903
904 let mut is_window = false;
905 let mut hovered_toplevel: *mut crate::window::Window = std::ptr::null_mut();
906 match result.data {
907 SceneNodeDataVal::Window(window) => {
908 // The grid is not a toplevel for hover purposes: hitting it
909 // means the pointer is on a desktop item (its input region
910 // covers nothing else), and the item drag needs the
911 // enter/motion delivery below — in overview too, where the
912 // is_window branch would clear pointer focus and try to
913 // focus-follow onto a surface seat.focus refuses.
914 if !(*window).is_status_bar()
915 && !(*window).is_wallpaper()
916 && !(*window).is_grid()
917 {
918 is_window = true;
919 hovered_toplevel = window;
920 }
921 // Adjust mode: the ring lands on the window under the
922 // pointer, focused or not. Set BEFORE the zone test
923 // below, so the band is live on the first hover. (Null
924 // for the status bar, wallpaper and grid.)
925 self.set_adjust_hover(if (*server).wm.window_adjust_active() {
926 hovered_toplevel
927 } else {
928 std::ptr::null_mut()
929 });
930 // No mode gate: the band scales with the window
931 // (get_border_zone is zoom-aware), so the resize/move
932 // controls reveal and work at any zoom, not just 1.
933 if !(*window).is_status_bar()
934 && !(*window).is_wallpaper()
935 && (*window).tiling_mode != crate::tiling::TilingMode::Popup
936 && (*window).tiling_mode != crate::tiling::TilingMode::Fullscreen
937 && (*window).tiling_mode != crate::tiling::TilingMode::Status
938 {
939 match get_border_zone(window, lx, ly) {
940 BorderZone::Resize(edges) => {
941 self.set_border_hover(window, Some(border_element_for_edges(edges)));
942 ffi::wlr_seat_pointer_notify_clear_focus((*self.seat).wlr_seat);
943 let cursor_name = get_resize_cursor_name(edges);
944 self.set_xcursor(cursor_name.as_ptr() as *const _);
945 return;
946 }
947 BorderZone::Move => {
948 self.set_border_hover(window, Some(crate::window::BorderElement::Top));
949 ffi::wlr_seat_pointer_notify_clear_focus((*self.seat).wlr_seat);
950 self.set_xcursor(b"grab\0".as_ptr() as *const _);
951 return;
952 }
953 BorderZone::Button(elem) => {
954 self.set_border_hover(window, Some(elem));
955 ffi::wlr_seat_pointer_notify_clear_focus((*self.seat).wlr_seat);
956 self.set_xcursor(b"pointer\0".as_ptr() as *const _);
957 return;
958 }
959 BorderZone::None => {}
960 }
961 }
962 }
963 SceneNodeDataVal::ShellSurface(_) | SceneNodeDataVal::OverrideRedirect(_) => {
964 is_window = true;
965 self.set_adjust_hover(std::ptr::null_mut());
966 }
967 _ => {
968 self.set_adjust_hover(std::ptr::null_mut());
969 }
970 }
971 self.set_border_hover(std::ptr::null_mut(), None);
972
973 // Chrome — a Popup (the cce-cloud launcher) or an Overlay dock —
974 // is live UI during overview, not a spatial thumbnail: the button
975 // path already lets its presses through to the app, and hover has
976 // to reach it the same way or its rows never highlight and a
977 // press lands on a surface that never saw an enter. So it skips
978 // the overview branch below and takes normal delivery.
979 let hovered_chrome = !hovered_toplevel.is_null()
980 && matches!(
981 (*hovered_toplevel).tiling_mode,
982 crate::tiling::TilingMode::Popup | crate::tiling::TilingMode::Overlay
983 );
984
985 if is_window
986 && !hovered_chrome
987 && (*server).wm.window_adjust_active()
988 {
989 // Focus follows the pointer in overview, so a click-less
990 // hover chooses the window a focus chord or the exit lands
991 // on. (The ring itself keys on `adjust_hover`, not focus.) (Super-held adjust mode takes this
992 // branch too, for the pointer-focus clear below, but not the
993 // refocus.) Guarded on an actual change —
994 // seat.focus raises a Floating window BEFORE its same-focus
995 // short-circuit, so an unguarded call would raise and relayout
996 // on every motion event. And with the pan suppressed: hovering
997 // must not move the camera, only the click and keyboard paths
998 // may. (Zoom was never at stake — focus_follow_pan pans at the
999 // current zoom — but a partially visible window would still
1000 // get dragged on-screen mid-hover.) And never while chrome
1001 // holds the keyboard: the cce-cloud launcher closes itself on
1002 // keyboard leave, and this path runs not only on real motion
1003 // but on the idle pointer refresh a commit schedules — the
1004 // launcher's own first configure — so a stationary pointer
1005 // resting on a world window was refocusing that window and
1006 // dismissing the launcher the instant it mapped.
1007 // Overview only: with Super held at zoom 1 focus stays put,
1008 // so a focus chord pressed next acts on the window the user
1009 // had — the ring follows the pointer through `adjust_hover`
1010 // instead (`Window::is_adjust_target`).
1011 if (*server).wm.mode == crate::window_manager::WindowManagerMode::Overview
1012 && !hovered_toplevel.is_null()
1013 && !(*self.seat).focus_is_chrome()
1014 && (*self.seat).focused
1015 != crate::seat::Focus::Window(hovered_toplevel)
1016 {
1017 let prev = (*self.seat).suppress_focus_pan;
1018 (*self.seat).suppress_focus_pan = true;
1019 (*self.seat).focus(crate::seat::Focus::Window(hovered_toplevel));
1020 (*self.seat).suppress_focus_pan = prev;
1021 }
1022 self.clear_focus();
1023 return;
1024 }
1025
1026 if !result.surface.is_null() {
1027 ffi::wlr_seat_pointer_notify_enter((*self.seat).wlr_seat, result.surface, result.sx, result.sy);
1028 ffi::wlr_seat_pointer_notify_motion((*self.seat).wlr_seat, time_msec, result.sx, result.sy);
1029 return;
1030 }
1031 }
1032
1033 // Nothing under the pointer: the desktop background. A DRAG in
1034 // progress is the one case that still needs a surface here — Wayland
1035 // delivers drops to surfaces, and the background is not one, so
1036 // without this every drag onto the desktop is cancelled on release.
1037 // The grid client stands in as the desktop's drop target: it already
1038 // covers the canvas and renders it, so it is the thing that can say
1039 // what "dropped at this spot" means. It stays input-transparent for
1040 // every other purpose (see `Scene::at`) — only the drag resolves onto
1041 // it, and only while the pointer is over the background, so clicks,
1042 // hover and the overview background-exit are untouched.
1043 if (*self.seat).drag != crate::seat::DragState::None {
1044 if let Some((surface, sx, sy)) = grid_surface_at(server, lx, ly) {
1045 log::debug!("[drag] focus -> grid at ({lx:.0}, {ly:.0})");
1046 ffi::wlr_seat_pointer_notify_enter((*self.seat).wlr_seat, surface, sx, sy);
1047 ffi::wlr_seat_pointer_notify_motion((*self.seat).wlr_seat, time_msec, sx, sy);
1048 return;
1049 }
1050 }
1051
1052 self.set_border_hover(std::ptr::null_mut(), None);
1053 self.set_adjust_hover(std::ptr::null_mut());
1054 self.clear_focus();
1055 }
1056
1057 /// Take the cursor image off for a touch (see `hidden_by_touch`).
1058 pub(crate) unsafe fn hide_for_touch(&mut self) {
1059 if self.hidden_by_touch {
1060 return;
1061 }
1062 self.hidden_by_touch = true;
1063 self.stop_xcursor_animation();
1064 ffi::wlr_cursor_unset_image(self.wlr_cursor);
1065 }
1066
1067 /// Bring the cursor image back once a real pointer moves. Called before
1068 /// the motion's passthrough, which re-enters the surface under the
1069 /// pointer: the clear here is what makes that enter fresh, so the
1070 /// client sets its cursor again — `handle_request_set_cursor` dropped
1071 /// whatever it asked for while the image was off. Not while a client
1072 /// holds an implicit grab: clearing pointer focus would end it.
1073 pub unsafe fn unhide_after_touch(&mut self) {
1074 if !self.hidden_by_touch {
1075 return;
1076 }
1077 self.hidden_by_touch = false;
1078 if self.notified_pressed.is_empty() {
1079 ffi::wlr_seat_pointer_notify_clear_focus((*self.seat).wlr_seat);
1080 }
1081 self.set_xcursor(b"default\0".as_ptr() as *const _);
1082 }
1083
1084 pub unsafe fn clear_focus(&mut self) {
1085 ffi::wlr_seat_pointer_notify_clear_focus((*self.seat).wlr_seat);
1086 self.set_xcursor(b"default\0".as_ptr() as *const _);
1087 }
1088
1089 // ── Synthetic pointer injection (`ccectl pointer-*`) ─────────────────────────
1090 // Each call builds the event a real device would deliver and runs it through the
1091 // REAL handler (via its listener — the same entry wlroots invokes), so compositor
1092 // policy — window ops, overview gating, grabs, focus, pointer constraints — treats
1093 // injected input exactly like hardware. Every handler null-checks `event.pointer`,
1094 // so a device-less event is safe. Buttons and axes finish with a frame, like a
1095 // real device batch. Press and release are separate entry points on purpose: a
1096 // held drag is press → any number of moves → release.
1097
1098 /// Warp to layout coordinates and run the motion tail (hover, drag icons, and
1099 /// op-update-or-passthrough, mirroring `handle_motion`). `wlr_cursor_warp` takes
1100 /// layout pixels — `wlr_cursor_warp_absolute` is 0..1-normalized, which is the
1101 /// bug the old `pointer-move-to` had.
1102 pub unsafe fn inject_motion_to(&mut self, x: f64, y: f64) {
1103 self.unhide_after_touch();
1104 self.warp_to(x, y);
1105 }
1106
1107 /// `inject_motion_to` without bringing a touch-hidden cursor back: the
1108 /// emulated pointer of a touch (`TouchRoute::Pointer`) moves this way.
1109 pub(crate) unsafe fn warp_to(&mut self, x: f64, y: f64) {
1110 (*self.seat).handle_activity();
1111 ffi::wlr_cursor_warp(self.wlr_cursor, std::ptr::null_mut(), x, y);
1112 self.update_hovered();
1113 self.update_drag_icons();
1114 let seat = &mut *self.seat;
1115 if seat.op.is_some() {
1116 let lx = (*self.wlr_cursor).x as i32;
1117 let ly = (*self.wlr_cursor).y as i32;
1118 seat.op_update(lx, ly);
1119 return;
1120 }
1121 self.passthrough(crate::util::msec_timestamp());
1122 // Real devices terminate every motion batch with a frame; sctk-based clients
1123 // queue pointer events until they see one.
1124 handle_frame(&mut self.frame_listener as *mut ffi::wl_listener, std::ptr::null_mut());
1125 }
1126
1127 pub unsafe fn inject_motion_by(&mut self, dx: f64, dy: f64) {
1128 let mut ev = ffi::wlr_pointer_motion_event {
1129 pointer: std::ptr::null_mut(),
1130 time_msec: crate::util::msec_timestamp(),
1131 delta_x: dx,
1132 delta_y: dy,
1133 unaccel_dx: dx,
1134 unaccel_dy: dy,
1135 };
1136 handle_motion(
1137 &mut self.motion_listener as *mut ffi::wl_listener,
1138 &mut ev as *mut ffi::wlr_pointer_motion_event as *mut std::ffi::c_void,
1139 );
1140 handle_frame(&mut self.frame_listener as *mut ffi::wl_listener, std::ptr::null_mut());
1141 }
1142
1143 pub unsafe fn inject_button(&mut self, button: u32, pressed: bool) {
1144 let state = if pressed {
1145 ffi::wl_pointer_button_state_WL_POINTER_BUTTON_STATE_PRESSED
1146 } else {
1147 ffi::wl_pointer_button_state_WL_POINTER_BUTTON_STATE_RELEASED
1148 };
1149 let mut ev = ffi::wlr_pointer_button_event {
1150 pointer: std::ptr::null_mut(),
1151 time_msec: crate::util::msec_timestamp(),
1152 button,
1153 state,
1154 };
1155 handle_button(
1156 &mut self.button_listener as *mut ffi::wl_listener,
1157 &mut ev as *mut ffi::wlr_pointer_button_event as *mut std::ffi::c_void,
1158 );
1159 handle_frame(&mut self.frame_listener as *mut ffi::wl_listener, std::ptr::null_mut());
1160 }
1161
1162 /// Wheel scroll; positive `dy` scrolls down (content up), matching a real wheel.
1163 /// One notch is 15 delta units / 120 `value120` steps (the libinput convention).
1164 /// `finger` injects a touchpad two-finger scroll (axis source FINGER, no
1165 /// discrete steps) instead of a wheel click, so a headless session can
1166 /// exercise the trackpad paths — the compositor's own desk pan and what
1167 /// X11/Wayland clients receive. A finger scroll ends with a zero-delta
1168 /// event, which `finger_stop` sends.
1169 pub unsafe fn inject_scroll(&mut self, dy: f64, dx: f64, finger: bool) {
1170 let time = crate::util::msec_timestamp();
1171 for (delta, orientation) in [
1172 (dy, ffi::wl_pointer_axis_WL_POINTER_AXIS_VERTICAL_SCROLL),
1173 (dx, ffi::wl_pointer_axis_WL_POINTER_AXIS_HORIZONTAL_SCROLL),
1174 ] {
1175 if delta == 0.0 {
1176 continue;
1177 }
1178 let mut ev = ffi::wlr_pointer_axis_event {
1179 pointer: std::ptr::null_mut(),
1180 time_msec: time,
1181 source: if finger {
1182 ffi::wl_pointer_axis_source_WL_POINTER_AXIS_SOURCE_FINGER
1183 } else {
1184 ffi::wl_pointer_axis_source_WL_POINTER_AXIS_SOURCE_WHEEL
1185 },
1186 orientation,
1187 relative_direction: ffi::wl_pointer_axis_relative_direction_WL_POINTER_AXIS_RELATIVE_DIRECTION_IDENTICAL,
1188 delta,
1189 delta_discrete: if finger { 0 } else { ((delta / 15.0) * 120.0) as i32 },
1190 };
1191 handle_axis(
1192 &mut self.axis_listener as *mut ffi::wl_listener,
1193 &mut ev as *mut ffi::wlr_pointer_axis_event as *mut std::ffi::c_void,
1194 );
1195 }
1196 handle_frame(&mut self.frame_listener as *mut ffi::wl_listener, std::ptr::null_mut());
1197 // Every libinput axis event is followed by a frame; clients
1198 // (Xwayland among them) deliver only on the frame, and wlroots
1199 // asserts if a later axis event changes source within one.
1200 ffi::wlr_seat_pointer_notify_frame((*self.seat).wlr_seat);
1201 }
1202
1203 /// The zero-delta event that ends a finger scroll (libinput sends one
1204 /// when the fingers lift); see `inject_scroll`.
1205 pub unsafe fn inject_finger_stop(&mut self) {
1206 let time = crate::util::msec_timestamp();
1207 for orientation in [
1208 ffi::wl_pointer_axis_WL_POINTER_AXIS_VERTICAL_SCROLL,
1209 ffi::wl_pointer_axis_WL_POINTER_AXIS_HORIZONTAL_SCROLL,
1210 ] {
1211 let mut ev = ffi::wlr_pointer_axis_event {
1212 pointer: std::ptr::null_mut(),
1213 time_msec: time,
1214 source: ffi::wl_pointer_axis_source_WL_POINTER_AXIS_SOURCE_FINGER,
1215 orientation,
1216 relative_direction: ffi::wl_pointer_axis_relative_direction_WL_POINTER_AXIS_RELATIVE_DIRECTION_IDENTICAL,
1217 delta: 0.0,
1218 delta_discrete: 0,
1219 };
1220 handle_axis(
1221 &mut self.axis_listener as *mut ffi::wl_listener,
1222 &mut ev as *mut ffi::wlr_pointer_axis_event as *mut std::ffi::c_void,
1223 );
1224 }
1225 // Every libinput axis event is followed by a frame; clients
1226 // (Xwayland among them) deliver only on the frame, and wlroots
1227 // asserts if a later axis event changes source within one.
1228 ffi::wlr_seat_pointer_notify_frame((*self.seat).wlr_seat);
1229 }
1230
1231 /// Inject a whole two-finger pinch: begin, `steps` updates easing the
1232 /// scale from 1 to `scale` (and the rotation to `rotation` degrees),
1233 /// end. Exercises the compositor's pinch policy and the
1234 /// pointer-gestures forward to clients headlessly.
1235 /// One stage of a pinch, for a test that paces the updates itself:
1236 /// `stage` is "begin", "update" (with `scale`/`rotation`) or "end".
1237 pub unsafe fn inject_pinch_stage(&mut self, stage: &str, scale: f64, rotation: f64) {
1238 let time = crate::util::msec_timestamp();
1239 match stage {
1240 "begin" => {
1241 let mut ev = ffi::wlr_pointer_pinch_begin_event { pointer: std::ptr::null_mut(), time_msec: time, fingers: 2 };
1242 handle_pinch_begin(&mut self.pinch_begin_listener as *mut ffi::wl_listener, &mut ev as *mut _ as *mut std::ffi::c_void);
1243 }
1244 "update" => {
1245 let mut ev = ffi::wlr_pointer_pinch_update_event { pointer: std::ptr::null_mut(), time_msec: time, fingers: 2, dx: 0.0, dy: 0.0, scale, rotation };
1246 handle_pinch_update(&mut self.pinch_update_listener as *mut ffi::wl_listener, &mut ev as *mut _ as *mut std::ffi::c_void);
1247 }
1248 _ => {
1249 let mut ev = ffi::wlr_pointer_pinch_end_event { pointer: std::ptr::null_mut(), time_msec: time, cancelled: false };
1250 handle_pinch_end(&mut self.pinch_end_listener as *mut ffi::wl_listener, &mut ev as *mut _ as *mut std::ffi::c_void);
1251 }
1252 }
1253 ffi::wlr_seat_pointer_notify_frame((*self.seat).wlr_seat);
1254 }
1255
1256 /// One stage of a touchpad swipe, paced by the caller (`pointer-swipe
1257 /// begin <fingers> | update <dx> <dy> | end`): what lets a shadow hold
1258 /// a swipe short of its threshold and read the camera's peek back.
1259 pub unsafe fn inject_swipe_stage(&mut self, stage: &str, fingers: u32, dx: f64, dy: f64) {
1260 let time = crate::util::msec_timestamp();
1261 match stage {
1262 "begin" => {
1263 self.inject_swipe_fingers = fingers;
1264 let mut ev = ffi::wlr_pointer_swipe_begin_event { pointer: std::ptr::null_mut(), time_msec: time, fingers };
1265 handle_swipe_begin(&mut self.swipe_begin_listener as *mut ffi::wl_listener, &mut ev as *mut _ as *mut std::ffi::c_void);
1266 }
1267 "update" => {
1268 let fingers = self.inject_swipe_fingers;
1269 let mut ev = ffi::wlr_pointer_swipe_update_event { pointer: std::ptr::null_mut(), time_msec: time, fingers, dx, dy };
1270 handle_swipe_update(&mut self.swipe_update_listener as *mut ffi::wl_listener, &mut ev as *mut _ as *mut std::ffi::c_void);
1271 }
1272 _ => {
1273 let mut ev = ffi::wlr_pointer_swipe_end_event { pointer: std::ptr::null_mut(), time_msec: time, cancelled: false };
1274 handle_swipe_end(&mut self.swipe_end_listener as *mut ffi::wl_listener, &mut ev as *mut _ as *mut std::ffi::c_void);
1275 }
1276 }
1277 ffi::wlr_seat_pointer_notify_frame((*self.seat).wlr_seat);
1278 }
1279
1280 /// Inject a whole touchpad swipe: begin with `fingers`, `steps` updates
1281 /// that together move the gesture centre by (`dx`, `dy`), end. Drives
1282 /// the gesture-bind table (`swipe3_left` in input.kdl) headlessly;
1283 /// the pointer-gestures forward to clients runs too.
1284 pub unsafe fn inject_swipe(&mut self, fingers: u32, dx: f64, dy: f64, steps: u32) {
1285 let time = crate::util::msec_timestamp();
1286 let mut begin = ffi::wlr_pointer_swipe_begin_event {
1287 pointer: std::ptr::null_mut(),
1288 time_msec: time,
1289 fingers,
1290 };
1291 handle_swipe_begin(
1292 &mut self.swipe_begin_listener as *mut ffi::wl_listener,
1293 &mut begin as *mut ffi::wlr_pointer_swipe_begin_event as *mut std::ffi::c_void,
1294 );
1295 ffi::wlr_seat_pointer_notify_frame((*self.seat).wlr_seat);
1296 let steps = steps.max(1);
1297 for i in 1..=steps {
1298 let mut update = ffi::wlr_pointer_swipe_update_event {
1299 pointer: std::ptr::null_mut(),
1300 time_msec: time + i,
1301 fingers,
1302 dx: dx / steps as f64,
1303 dy: dy / steps as f64,
1304 };
1305 handle_swipe_update(
1306 &mut self.swipe_update_listener as *mut ffi::wl_listener,
1307 &mut update as *mut ffi::wlr_pointer_swipe_update_event as *mut std::ffi::c_void,
1308 );
1309 ffi::wlr_seat_pointer_notify_frame((*self.seat).wlr_seat);
1310 }
1311 let mut end = ffi::wlr_pointer_swipe_end_event {
1312 pointer: std::ptr::null_mut(),
1313 time_msec: time + steps + 1,
1314 cancelled: false,
1315 };
1316 handle_swipe_end(
1317 &mut self.swipe_end_listener as *mut ffi::wl_listener,
1318 &mut end as *mut ffi::wlr_pointer_swipe_end_event as *mut std::ffi::c_void,
1319 );
1320 ffi::wlr_seat_pointer_notify_frame((*self.seat).wlr_seat);
1321 }
1322
1323 pub unsafe fn inject_pinch(&mut self, scale: f64, rotation: f64, steps: u32) {
1324 let time = crate::util::msec_timestamp();
1325 let mut begin = ffi::wlr_pointer_pinch_begin_event {
1326 pointer: std::ptr::null_mut(),
1327 time_msec: time,
1328 fingers: 2,
1329 };
1330 handle_pinch_begin(
1331 &mut self.pinch_begin_listener as *mut ffi::wl_listener,
1332 &mut begin as *mut ffi::wlr_pointer_pinch_begin_event as *mut std::ffi::c_void,
1333 );
1334 ffi::wlr_seat_pointer_notify_frame((*self.seat).wlr_seat);
1335 let steps = steps.max(1);
1336 for i in 1..=steps {
1337 let t = i as f64 / steps as f64;
1338 let mut update = ffi::wlr_pointer_pinch_update_event {
1339 pointer: std::ptr::null_mut(),
1340 time_msec: time + i,
1341 fingers: 2,
1342 dx: 0.0,
1343 dy: 0.0,
1344 scale: 1.0 + (scale - 1.0) * t,
1345 rotation: rotation * t,
1346 };
1347 handle_pinch_update(
1348 &mut self.pinch_update_listener as *mut ffi::wl_listener,
1349 &mut update as *mut ffi::wlr_pointer_pinch_update_event as *mut std::ffi::c_void,
1350 );
1351 ffi::wlr_seat_pointer_notify_frame((*self.seat).wlr_seat);
1352 }
1353 let mut end = ffi::wlr_pointer_pinch_end_event {
1354 pointer: std::ptr::null_mut(),
1355 time_msec: time + steps + 1,
1356 cancelled: false,
1357 };
1358 handle_pinch_end(
1359 &mut self.pinch_end_listener as *mut ffi::wl_listener,
1360 &mut end as *mut ffi::wlr_pointer_pinch_end_event as *mut std::ffi::c_void,
1361 );
1362 ffi::wlr_seat_pointer_notify_frame((*self.seat).wlr_seat);
1363 }
1364 }
1365
1366 unsafe extern "C" fn handle_motion(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
1367 let cursor = &mut *crate::container_of!(listener, Cursor, motion_listener);
1368 let event = data as *mut ffi::wlr_pointer_motion_event;
1369 // Pointer input is activity for the idle timeouts and the idle-notify
1370 // clients. Injected events (`ccectl pointer-*`) arrive here too, and
1371 // count: a script driving the pointer is someone using the desk.
1372 (*cursor.seat).handle_activity();
1373 // Real pointer motion ends an emulated view drag: the client must not
1374 // see the synthetic drag position and the true one interleaved.
1375 if cursor.view_drag.is_some() {
1376 cursor.end_view_drag("motion");
1377 }
1378 // Likewise the held Shift must not ride along to wherever the pointer
1379 // goes next.
1380 if cursor.hscroll_shift.is_some() {
1381 cursor.end_hscroll_shift("motion");
1382 }
1383
1384 cursor.unhide_after_touch();
1385
1386 let mut dx = (*event).delta_x;
1387 let mut dy = (*event).delta_y;
1388
1389 if !cursor.constraint.is_null() {
1390 (*cursor.constraint).confine(&mut dx, &mut dy);
1391 }
1392
1393 // Real libinput motion collapses the compositor to ~3fps, while an injected
1394 // warp at the SAME event rate sustains ~58fps — so the cost is somewhere in
1395 // this handler rather than in rendering or the scene. Time each stage.
1396 let t_start = if crate::output::frame_debug() {
1397 Some(std::time::Instant::now())
1398 } else {
1399 None
1400 };
1401
1402 ffi::wlr_cursor_move(cursor.wlr_cursor, std::ptr::null_mut(), dx, dy);
1403 let t_move = t_start.map(|s| s.elapsed().as_micros());
1404 cursor.update_hovered();
1405 let t_hovered = t_start.map(|s| s.elapsed().as_micros());
1406 cursor.update_drag_icons();
1407 let t_drag = t_start.map(|s| s.elapsed().as_micros());
1408
1409 let seat = &mut *cursor.seat;
1410 if (*seat).op.is_some() {
1411 let lx = (*cursor.wlr_cursor).x as i32;
1412 let ly = (*cursor.wlr_cursor).y as i32;
1413 (*seat).op_update(lx, ly);
1414 if let (Some(s), Some(m), Some(h), Some(d)) = (t_start, t_move, t_hovered, t_drag) {
1415 log::info!(
1416 "[cce-frame] t={} motion(op) total={}us move={}us hovered={}us drag={}us",
1417 crate::util::msec_timestamp() % 100000,
1418 s.elapsed().as_micros(), m, h - m, d - h
1419 );
1420 }
1421 return;
1422 }
1423
1424 cursor.passthrough((*event).time_msec);
1425
1426 if let (Some(s), Some(m), Some(h), Some(d)) = (t_start, t_move, t_hovered, t_drag) {
1427 let total = s.elapsed().as_micros();
1428 log::info!(
1429 "[cce-frame] t={} motion total={}us move={}us hovered={}us drag={}us passthrough={}us",
1430 crate::util::msec_timestamp() % 100000,
1431 total, m, h - m, d - h, total - d
1432 );
1433 }
1434 }
1435
1436 unsafe extern "C" fn handle_motion_absolute(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
1437 let cursor = &mut *crate::container_of!(listener, Cursor, motion_absolute_listener);
1438 let event = data as *mut ffi::wlr_pointer_motion_absolute_event;
1439 (*cursor.seat).handle_activity();
1440 cursor.unhide_after_touch();
1441
1442 let wlr_device = if (*event).pointer.is_null() {
1443 std::ptr::null_mut()
1444 } else {
1445 &mut (*(*event).pointer).base as *mut ffi::wlr_input_device
1446 };
1447 ffi::wlr_cursor_warp_absolute(cursor.wlr_cursor, wlr_device, (*event).x, (*event).y);
1448 cursor.update_hovered();
1449 cursor.update_drag_icons();
1450
1451 let seat = &mut *cursor.seat;
1452 if (*seat).op.is_some() {
1453 let lx = (*cursor.wlr_cursor).x as i32;
1454 let ly = (*cursor.wlr_cursor).y as i32;
1455 (*seat).op_update(lx, ly);
1456 return;
1457 }
1458
1459 cursor.passthrough((*event).time_msec);
1460 }
1461
1462 /// True when the layer surface's namespace marks it as cce-cloud chrome —
1463 /// the launcher and the desktop/app context menus. These are screen-anchored
1464 /// popups that stay interactive during overview and dismiss on click-away.
1465 unsafe fn is_cloud_layer(layer_surface: *mut crate::layer_shell::LayerSurface) -> bool {
1466 if layer_surface.is_null() {
1467 return false;
1468 }
1469 let wlr_layer_surface = (*layer_surface).wlr_layer_surface;
1470 if wlr_layer_surface.is_null() || (*wlr_layer_surface).namespace.is_null() {
1471 return false;
1472 }
1473 std::ffi::CStr::from_ptr((*wlr_layer_surface).namespace)
1474 .to_string_lossy()
1475 .starts_with("cce-cloud")
1476 }
1477
1478 /// Whether a click or touch on this layer surface may give it keyboard
1479 /// focus. Not when it asked for none: per wlr-layer-shell such a surface is
1480 /// never given keyboard focus, and the one that relies on it — an on-screen
1481 /// keyboard (cce-keyboard), typing into the window it was clicked over —
1482 /// cannot work if a click on its keys takes focus off that window.
1483 pub(crate) unsafe fn layer_takes_click_focus(layer_surface: *mut crate::layer_shell::LayerSurface) -> bool {
1484 if layer_surface.is_null() || (*layer_surface).wlr_layer_surface.is_null() {
1485 return false;
1486 }
1487 (*(*layer_surface).wlr_layer_surface).current.keyboard_interactive
1488 != ffi::zwlr_layer_surface_v1_keyboard_interactivity_ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_NONE
1489 }
1490
1491 /// What every press does before anyone handles it: close the menus it
1492 /// lands outside of. Shared by a pointer press (`handle_button`) and a
1493 /// touch that goes to a client as touch (`handle_touch_down`); an
1494 /// emulated touch arrives as a pointer press and runs it there.
1495 pub(crate) unsafe fn press_dismissals(server: *mut crate::server::Server, lx: f64, ly: f64) {
1496 // Click-away-close for in-surface status menus: if any status
1497 // segment is expanded (menu open) and this press did not land on it,
1498 // push a one-shot dismiss over the status socket. The line carries
1499 // the pressed segment's app_id so a press ON an expanded segment
1500 // exempts that segment (it handles its own clicks) while still
1501 // dismissing any other open menu.
1502 {
1503 let mut target_status: *mut crate::window::Window = std::ptr::null_mut();
1504 if let Some(result) = (*server).scene.at(lx, ly) {
1505 if let SceneNodeDataVal::Window(window) = result.data {
1506 if (*window).is_status_bar() {
1507 target_status = window;
1508 }
1509 }
1510 }
1511 let any_other_expanded = (*server).wm.any_expanded_status_segment(target_status);
1512 if any_other_expanded {
1513 let except = if target_status.is_null() {
1514 "-".to_string()
1515 } else {
1516 (*target_status).get_app_id_string().unwrap_or_else(|| "-".to_string())
1517 };
1518 if let Some(ref sender) = (*server).wm.status_sender {
1519 sender.send_menu_dismiss(&except);
1520 }
1521 }
1522 }
1523
1524 // Click-away for X11 popups. Xwayland only sees the pointer over
1525 // its own surfaces, so a popup menu an X11 app opened (Wine's, most
1526 // of all — a tray icon's menu opened through cce-xembed-tray) never
1527 // hears a press on a Wayland window and stays open; only a click on
1528 // one of the app's own X windows used to close it. So a press that
1529 // lands on no X11 surface while some override-redirect window is
1530 // showing is reported on the status socket's `clickaway` topic, and
1531 // the bridge closes the popup it opened. The tray bridge's own
1532 // containers have no scene tree, so they never count as showing.
1533 {
1534 let or_showing = (*server)
1535 .wm
1536 .override_redirects
1537 .iter()
1538 .any(|&or| !or.is_null() && !(*or).surface_tree.is_null());
1539 if or_showing {
1540 let on_x11 = match (*server).scene.at(lx, ly) {
1541 Some(result) => match result.data {
1542 SceneNodeDataVal::Window(window) => {
1543 matches!((*window).impl_type, crate::window::WindowImpl::Xwayland(_))
1544 }
1545 SceneNodeDataVal::OverrideRedirect(_) => true,
1546 _ => false,
1547 },
1548 None => false,
1549 };
1550 if !on_x11 {
1551 if let Some(ref sender) = (*server).wm.status_sender {
1552 sender.send_click_away();
1553 }
1554 }
1555 }
1556 }
1557 }
1558
1559 unsafe extern "C" fn handle_button(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
1560 let cursor = &mut *crate::container_of!(listener, Cursor, button_listener);
1561 let event = data as *mut ffi::wlr_pointer_button_event;
1562 (*cursor.seat).handle_activity();
1563 if cursor.view_drag.is_some() {
1564 cursor.end_view_drag("button");
1565 }
1566 // Before the button reaches the client, so a click on the popup is not
1567 // a Ctrl-click (see `PopupWheel`).
1568 cursor.end_popup_wheel("button");
1569 cursor.end_hscroll_shift("button");
1570
1571 let seat = &mut *cursor.seat;
1572 let lx = cursor.x();
1573 let ly = cursor.y();
1574 let server = seat.server;
1575
1576 // First deliberate input ends the session-restore settling phase (see
1577 // the focus gate in Window::map).
1578 if (*event).state == ffi::wl_pointer_button_state_WL_POINTER_BUTTON_STATE_PRESSED {
1579 (*server).wm.startup_input_seen = true;
1580 }
1581
1582 let mut is_app_surface = false;
1583 let mut is_overlay_window = false;
1584 if let Some(result) = (*server).scene.at(lx, ly) {
1585 match result.data {
1586 SceneNodeDataVal::Window(window) => {
1587 // The grid does not block: hitting it means the press is on a
1588 // desktop item (its input region covers nothing else), and
1589 // the item drag needs the press delivered in overview like
1590 // any chrome click.
1591 if !(*window).is_status_bar()
1592 && !(*window).is_wallpaper()
1593 && !(*window).is_grid()
1594 {
1595 is_app_surface = true;
1596 // Popup counts as chrome like Overlay: the cce-cloud
1597 // launcher must keep receiving clicks in overview.
1598 if (*window).tiling_mode == crate::tiling::TilingMode::Overlay
1599 || (*window).tiling_mode == crate::tiling::TilingMode::Popup
1600 {
1601 is_overlay_window = true;
1602 }
1603 }
1604 }
1605 SceneNodeDataVal::LayerSurface(layer_surface) => {
1606 if !layer_surface.is_null() {
1607 is_app_surface = true;
1608 if is_cloud_layer(layer_surface) {
1609 is_overlay_window = true;
1610 }
1611 }
1612 }
1613 SceneNodeDataVal::ShellSurface(_) | SceneNodeDataVal::OverrideRedirect(_) => {
1614 is_app_surface = true;
1615 }
1616 _ => {}
1617 }
1618 }
1619 let should_block_button = (*(*seat).server).wm.mode == crate::window_manager::WindowManagerMode::Overview && is_app_surface && !is_overlay_window;
1620
1621 if (*event).state == ffi::wl_pointer_button_state_WL_POINTER_BUTTON_STATE_PRESSED {
1622 if cursor.pressed.contains_key(&(*event).button) {
1623 log::error!("ignoring duplicate pointer button {} press", (*event).button);
1624 return;
1625 }
1626
1627 press_dismissals(server, lx, ly);
1628
1629 // Status-bar segments are dragged either in adjust-position mode or
1630 // directly with super+left-drag (0x40 = WLR_MODIFIER_LOGO).
1631 let super_held = {
1632 let wlr_keyboard = ffi::river_wlr_seat_get_keyboard(seat.wlr_seat);
1633 !wlr_keyboard.is_null() && (ffi::wlr_keyboard_get_modifiers(wlr_keyboard) & 0x40) != 0
1634 };
1635 if (*event).button == 0x110 && ((*(*seat).server).wm.adjust_position_mode || super_held) {
1636 let mut clicked_status: *mut crate::window::Window = std::ptr::null_mut();
1637 if let Some(result) = (*server).scene.at(lx, ly) {
1638 if let SceneNodeDataVal::Window(window) = result.data {
1639 if (*window).is_status_bar() {
1640 clicked_status = window;
1641 }
1642 }
1643 }
1644 // An EXPANDED segment (menu open) is never grabbed: its rows are
1645 // clicked, and a grab here would swallow the press the "Done"
1646 // row needs to leave adjust mode — the one control that ends the
1647 // mode from the bar would be unreachable while it is on.
1648 if !clicked_status.is_null() && !(*server).wm.is_expanded_status_segment(clicked_status) {
1649 (*server).wm.stop_panning_animation();
1650 let cursor_x = (*cursor.wlr_cursor).x;
1651 let cursor_y = (*cursor.wlr_cursor).y;
1652 seat.op = Some(crate::seat::SeatOp {
1653 sent_release: false,
1654 input: crate::seat::SeatOpInput::Pointer,
1655 start_x: cursor_x as i32,
1656 start_y: cursor_y as i32,
1657 x: cursor_x as i32,
1658 y: cursor_y as i32,
1659 window_ptr: clicked_status,
1660 op_type: crate::seat::PointerOpType::Move,
1661 start_win_x: (*clicked_status).box_geom.x,
1662 start_win_y: (*clicked_status).box_geom.y,
1663 start_win_w: (*clicked_status).box_geom.width as u32,
1664 start_win_h: (*clicked_status).box_geom.height as u32,
1665 start_win_virtual_x: (*clicked_status).virtual_x,
1666 start_win_virtual_y: (*clicked_status).virtual_y,
1667 start_was_tiled: false,
1668 start_pan_x: (*server).wm.desk_pan_x,
1669 start_pan_y: (*server).wm.desk_pan_y,
1670 start_tiling_mode: (*clicked_status).tiling_mode,
1671 start_mode_locked: (*clicked_status).mode_locked,
1672 started_in_overview: false,
1673 });
1674 cursor.op_start_pointer();
1675 cursor.pressed.insert((*event).button, None);
1676 cursor.set_xcursor(b"grab\0".as_ptr() as *const _);
1677 return;
1678 }
1679 }
1680
1681 // --- ADJUST-MODE CLICK HANDLING (overview, or Super held) ---
1682 // A press on a window's body grabs the whole window to move it; a
1683 // press on its ring falls through to the border path. Only in
1684 // overview does a background press mean anything (it exits); with
1685 // Super held at zoom 1 it falls through to the normal desktop press.
1686 let in_overview = (*(*seat).server).wm.mode == crate::window_manager::WindowManagerMode::Overview;
1687 if (*event).button == 0x110 && (*(*seat).server).wm.window_adjust_active() {
1688 let mut clicked_win: *mut crate::window::Window = std::ptr::null_mut();
1689 let mut clicked_cloud_layer = false;
1690 if let Some(result) = (*server).scene.at(lx, ly) {
1691 match result.data {
1692 SceneNodeDataVal::Window(window) => clicked_win = window,
1693 SceneNodeDataVal::LayerSurface(layer_surface) => {
1694 clicked_cloud_layer = is_cloud_layer(layer_surface);
1695 }
1696 _ => {}
1697 }
1698 }
1699
1700 // A press on the border band falls through to the normal
1701 // border path below (move/resize by zone, zoom-aware) — the
1702 // resize controls work at any zoom. Content presses grab the
1703 // whole window; true background presses exit overview.
1704 //
1705 // Chrome windows (Overlay docks, Popup surfaces like the
1706 // cce-cloud launcher) and cce-cloud layer surfaces (launcher,
1707 // desktop/app context menus) are neither: they stay
1708 // interactive UI during overview, so their presses fall
1709 // through to the normal path (focus + delivery to the app)
1710 // and overview stays up.
1711 let overview_chrome = clicked_cloud_layer
1712 || (!clicked_win.is_null()
1713 && matches!(
1714 (*clicked_win).tiling_mode,
1715 crate::tiling::TilingMode::Overlay | crate::tiling::TilingMode::Popup
1716 ));
1717 // Hitting the grid means the press landed on a DESKTOP ITEM —
1718 // its input region covers the item rects and nothing else — so
1719 // it is chrome-like: fall through to normal delivery and the
1720 // grid client starts its item drag, in overview exactly as in
1721 // normal mode. Bare canvas misses the grid entirely (that is
1722 // the input region again) and still exits overview below.
1723 let clicked_grid = !clicked_win.is_null() && (*clicked_win).is_grid();
1724 // The bare canvas counts as background in overview: a press on
1725 // it must exit overview like any desktop press, never grab the
1726 // canvas itself as if it were a window.
1727 let overview_win_valid = !clicked_win.is_null()
1728 && !(*clicked_win).is_status_bar()
1729 && !(*clicked_win).is_wallpaper()
1730 && !clicked_grid;
1731 let overview_border_zone = if overview_win_valid {
1732 get_border_zone(clicked_win, lx, ly)
1733 } else {
1734 BorderZone::None
1735 };
1736 // A window button is a click: nothing is grabbed, the client
1737 // never sees the press, and the release decides
1738 // (`Cursor::button_press`, answered in the release path).
1739 if let BorderZone::Button(elem) = overview_border_zone {
1740 cursor.button_press = Some((clicked_win, elem));
1741 cursor.pressed.insert((*event).button, None);
1742 return;
1743 }
1744 // A press on a SELECTED desktop image grabs the whole selection
1745 // — windows and images — from the image's side: the grid never
1746 // sees the press, the compositor moves everything and tells the
1747 // grid where its images went (`crate::selection`). A press on an
1748 // unselected image drops the selection, as one on an unselected
1749 // window does, and goes to the grid as before.
1750 if clicked_grid && in_overview {
1751 let (vx, vy) = (*server).wm.layout_to_virtual(lx, ly);
1752 if (*server).wm.selected_item_at(vx, vy).is_some() {
1753 (*server).wm.stop_panning_animation();
1754 seat.group_move.clear();
1755 seat.group_items.clear();
1756 // The first Tiled window carried is the snap anchor.
1757 let mut tiled_anchor: Option<(f64, f64)> = None;
1758 let carried: Vec<*mut crate::window::Window> = (*server)
1759 .wm
1760 .selection
1761 .windows
1762 .iter()
1763 .copied()
1764 .filter(|&w| (*server).wm.selectable(w))
1765 .collect();
1766 for w in carried {
1767 seat.group_move.push((w, (*w).virtual_x, (*w).virtual_y));
1768 if (*w).tiling_mode == crate::tiling::TilingMode::Tiled {
1769 if tiled_anchor.is_none() {
1770 tiled_anchor = Some(((*w).virtual_x, (*w).virtual_y));
1771 }
1772 } else if (*w).tiling_mode == crate::tiling::TilingMode::Floating {
1773 (*server).wm.raise_window(w);
1774 }
1775 }
1776 let ids: Vec<u64> = (*server).wm.selection.items.clone();
1777 for id in ids {
1778 if let Some(item) = (*server).wm.desktop_item(id) {
1779 seat.group_items.push((id, item.x, item.y));
1780 }
1781 }
1782 let cursor_x = (*cursor.wlr_cursor).x;
1783 let cursor_y = (*cursor.wlr_cursor).y;
1784 let (anchor_x, anchor_y) = tiled_anchor.unwrap_or((0.0, 0.0));
1785 seat.op = Some(crate::seat::SeatOp {
1786 sent_release: false,
1787 input: crate::seat::SeatOpInput::Pointer,
1788 start_x: cursor_x as i32,
1789 start_y: cursor_y as i32,
1790 x: cursor_x as i32,
1791 y: cursor_y as i32,
1792 window_ptr: std::ptr::null_mut(),
1793 op_type: crate::seat::PointerOpType::GroupMove,
1794 start_win_x: 0,
1795 start_win_y: 0,
1796 start_win_w: 0,
1797 start_win_h: 0,
1798 start_win_virtual_x: anchor_x,
1799 start_win_virtual_y: anchor_y,
1800 start_was_tiled: tiled_anchor.is_some(),
1801 start_pan_x: (*server).wm.desk_pan_x,
1802 start_pan_y: (*server).wm.desk_pan_y,
1803 start_tiling_mode: crate::tiling::TilingMode::Floating,
1804 start_mode_locked: false,
1805 started_in_overview: true,
1806 });
1807 cursor.op_start_pointer();
1808 cursor.pressed.insert((*event).button, None);
1809 cursor.set_xcursor(b"grab\0".as_ptr() as *const _);
1810 return;
1811 }
1812 (*server).wm.selection_clear();
1813 }
1814 if overview_chrome || clicked_grid {
1815 // fall through
1816 } else if overview_win_valid && matches!(overview_border_zone, BorderZone::None) {
1817 // The grab does NOT focus the window: moving a window is
1818 // not choosing it, and the ring already sits on it through
1819 // `adjust_hover`. A tap — press and release without motion
1820 // — is a click and focuses in `op_end`; a Floating window
1821 // is raised for the drag in `op_start_pointer`. (In
1822 // overview hover already focused it.)
1823 (*server).wm.stop_panning_animation();
1824 // A selected window carries the rest of the selection with
1825 // it; a press on any other window drops the selection, the
1826 // way a press on a node outside the region does in
1827 // cce-designer. The carried Floating windows are raised
1828 // here, in stacking order, and the grabbed one over them in
1829 // `op_start_pointer`.
1830 seat.group_move.clear();
1831 seat.group_items.clear();
1832 if (*server).wm.is_selected(clicked_win) {
1833 let carried: Vec<*mut crate::window::Window> = (*server)
1834 .wm
1835 .selection
1836 .windows
1837 .iter()
1838 .copied()
1839 .filter(|&w| w != clicked_win && (*server).wm.selectable(w))
1840 .collect();
1841 for w in carried {
1842 seat.group_move.push((w, (*w).virtual_x, (*w).virtual_y));
1843 if (*w).tiling_mode == crate::tiling::TilingMode::Floating {
1844 (*server).wm.raise_window(w);
1845 }
1846 }
1847 // And the selected desktop images, by the same offset.
1848 let ids: Vec<u64> = (*server).wm.selection.items.clone();
1849 for id in ids {
1850 if let Some(item) = (*server).wm.desktop_item(id) {
1851 seat.group_items.push((id, item.x, item.y));
1852 }
1853 }
1854 } else {
1855 (*server).wm.selection_clear();
1856 }
1857 let cursor_x = (*cursor.wlr_cursor).x;
1858 let cursor_y = (*cursor.wlr_cursor).y;
1859 seat.op = Some(crate::seat::SeatOp {
1860 sent_release: false,
1861 input: crate::seat::SeatOpInput::Pointer,
1862 start_x: cursor_x as i32,
1863 start_y: cursor_y as i32,
1864 x: cursor_x as i32,
1865 y: cursor_y as i32,
1866 window_ptr: clicked_win,
1867 op_type: crate::seat::PointerOpType::Move,
1868 start_win_x: (*clicked_win).box_geom.x,
1869 start_win_y: (*clicked_win).box_geom.y,
1870 start_win_w: (*clicked_win).box_geom.width as u32,
1871 start_win_h: (*clicked_win).box_geom.height as u32,
1872 start_win_virtual_x: (*clicked_win).virtual_x,
1873 start_win_virtual_y: (*clicked_win).virtual_y,
1874 start_was_tiled: (*clicked_win).tiling_mode == crate::tiling::TilingMode::Tiled,
1875 start_pan_x: (*server).wm.desk_pan_x,
1876 start_pan_y: (*server).wm.desk_pan_y,
1877 start_tiling_mode: (*clicked_win).tiling_mode,
1878 start_mode_locked: (*clicked_win).mode_locked,
1879 started_in_overview: in_overview,
1880 });
1881 cursor.op_start_pointer();
1882 cursor.pressed.insert((*event).button, None);
1883 cursor.set_xcursor(b"grab\0".as_ptr() as *const _);
1884 return;
1885 } else if !overview_win_valid && in_overview {
1886 // Click-away with a cce-cloud popup open (desktop context
1887 // menu, launcher): the press dismisses the popup and does
1888 // nothing else — overview stays up. Dropping keyboard focus
1889 // IS the dismissal: cce-cloud closes itself on keyboard
1890 // leave, the same signal a normal-mode click-away produces
1891 // through its focus change.
1892 if let crate::layer_shell::LayerShellSeatFocus::Exclusive(key) =
1893 seat.layer_shell.scheduled_focus
1894 {
1895 if let Some(&layer_surface) = (*server).layer_shell.surfaces.get(key) {
1896 if is_cloud_layer(layer_surface) {
1897 seat.focus(Focus::None);
1898 cursor.pressed.insert((*event).button, None);
1899 return;
1900 }
1901 }
1902 }
1903 // A press on the bare desktop is a click (which leaves
1904 // overview) or the start of a drag-selection, and only the
1905 // release can say which: it arms the selection, and the
1906 // release path answers a press that never travelled.
1907 (*server).wm.stop_panning_animation();
1908 (*server).wm.selection_press(lx, ly);
1909 seat.group_move.clear();
1910 let cursor_x = (*cursor.wlr_cursor).x;
1911 let cursor_y = (*cursor.wlr_cursor).y;
1912 seat.op = Some(crate::seat::SeatOp {
1913 sent_release: false,
1914 input: crate::seat::SeatOpInput::Pointer,
1915 start_x: cursor_x as i32,
1916 start_y: cursor_y as i32,
1917 x: cursor_x as i32,
1918 y: cursor_y as i32,
1919 window_ptr: std::ptr::null_mut(),
1920 op_type: crate::seat::PointerOpType::Select,
1921 start_win_x: 0,
1922 start_win_y: 0,
1923 start_win_w: 0,
1924 start_win_h: 0,
1925 start_win_virtual_x: 0.0,
1926 start_win_virtual_y: 0.0,
1927 start_was_tiled: false,
1928 start_pan_x: (*server).wm.desk_pan_x,
1929 start_pan_y: (*server).wm.desk_pan_y,
1930 start_tiling_mode: crate::tiling::TilingMode::Floating,
1931 start_mode_locked: false,
1932 started_in_overview: true,
1933 });
1934 cursor.op_start_pointer();
1935 cursor.pressed.insert((*event).button, None);
1936 cursor.set_xcursor(b"crosshair\0".as_ptr() as *const _);
1937 return;
1938 }
1939 }
1940
1941 let wlr_keyboard = ffi::river_wlr_seat_get_keyboard(seat.wlr_seat);
1942 let modifiers = if !wlr_keyboard.is_null() {
1943 ffi::wlr_keyboard_get_modifiers(wlr_keyboard)
1944 } else {
1945 0
1946 };
1947
1948 if (*event).button == 0x111 && modifiers == 0 {
1949 let mut clicked_interactive = false;
1950 if let Some(result) = (*server).scene.at(lx, ly) {
1951 match result.data {
1952 SceneNodeDataVal::Window(window) => {
1953 if !(*window).is_wallpaper() {
1954 clicked_interactive = true;
1955 }
1956 }
1957 SceneNodeDataVal::LayerSurface(_) | SceneNodeDataVal::ShellSurface(_) | SceneNodeDataVal::LockSurface(_) | SceneNodeDataVal::OverrideRedirect(_) => {
1958 clicked_interactive = true;
1959 }
1960 }
1961 }
1962 if !clicked_interactive {
1963 cursor.right_click_on_bg = true;
1964 let x = cursor.x() as i32;
1965 let y = cursor.y() as i32;
1966 let home = std::env::var("HOME").unwrap_or_default();
1967 let mut cmd = format!("{}/.local/bin/cce-desktop-menu -x {} -y {}", home, x, y);
1968 // The menu's "Window Mode" page acts on the FOCUSED window,
1969 // and focus is dropped right below (so the popup takes the
1970 // keyboard and a click-away dismisses it) — by the time the
1971 // script asks `ccectl windows` nothing is focused. Hand it
1972 // the window here instead. Only a window whose mode
1973 // `set-mode` accepts is worth naming.
1974 if let Focus::Window(w) = seat.focused {
1975 if !w.is_null()
1976 && matches!(
1977 (*w).tiling_mode,
1978 crate::tiling::TilingMode::Floating
1979 | crate::tiling::TilingMode::Tiled
1980 | crate::tiling::TilingMode::Fullscreen
1981 )
1982 {
1983 let app_id = (*w).get_app_id_string().unwrap_or_else(|| "unknown".to_string());
1984 cmd.push_str(&format!(
1985 " -i {} -a '{}'",
1986 (*w).ref_key.index,
1987 app_id.replace('\'', "'\\''")
1988 ));
1989 }
1990 }
1991 (*server).wm.execute_action(&crate::config::Action::Spawn, Some(&cmd));
1992
1993 seat.focus(Focus::None);
1994 (*(*seat).server).wm.dirty_windowing();
1995
1996 cursor.pressed.insert((*event).button, None);
1997 return;
1998 }
1999 }
2000
2001 let mut matched_pb: Option<crate::config::PointerBind> = None;
2002 for pb in &(*(*seat).server).wm.pointer_binds {
2003 if pb.button == (*event).button && pb.mods == modifiers {
2004 matched_pb = Some(pb.clone());
2005 break;
2006 }
2007 }
2008
2009 if let Some(pb) = matched_pb {
2010 let lx = cursor.x();
2011 let ly = cursor.y();
2012 let server = seat.server;
2013 let mut target_win: *mut crate::window::Window = std::ptr::null_mut();
2014 if let Some(result) = (*server).scene.at(lx, ly) {
2015 if let SceneNodeDataVal::Window(window) = result.data {
2016 target_win = window;
2017 }
2018 }
2019
2020 if !target_win.is_null() && !(*target_win).is_status_bar() && !(*target_win).is_wallpaper() {
2021 // Before the un-tile below: a tiled window's drag snaps hard
2022 // to whole squares, and by op_update the mode reads Floating.
2023 let grabbed_tiled =
2024 (*target_win).tiling_mode == crate::tiling::TilingMode::Tiled;
2025 if (*target_win).tiling_mode != crate::tiling::TilingMode::Floating
2026 && (*target_win).tiling_mode != crate::tiling::TilingMode::Popup
2027 && (*target_win).tiling_mode != crate::tiling::TilingMode::Fullscreen
2028 && (*target_win).tiling_mode != crate::tiling::TilingMode::Overlay
2029 // A drag moves a Utility window; it must not re-class it.
2030 && (*target_win).tiling_mode != crate::tiling::TilingMode::Utility
2031 {
2032 // A tiled window un-tiles for the drag but keeps its
2033 // cell-quantized geometry; landing grid-aligned re-tiles
2034 // it (op_end geometric detection).
2035 (*target_win).was_tiled = false;
2036 (*target_win).tiling_mode = crate::tiling::TilingMode::Floating;
2037 (*target_win).mode_locked = true;
2038 }
2039 // No focus on the grab: a bound move/resize drag acts on
2040 // the window under the pointer without choosing it (a tap
2041 // focuses in `op_end`, the raise is in `op_start_pointer`).
2042 let op_type = match pb.action {
2043 crate::config::Action::Move => Some(crate::seat::PointerOpType::Move),
2044 // The modifier binding is a resize path the border zones
2045 // never see, so it carries its own Utility rejection.
2046 crate::config::Action::Resize
2047 if (*target_win).tiling_mode == crate::tiling::TilingMode::Utility =>
2048 {
2049 None
2050 }
2051 crate::config::Action::Resize => {
2052 let edges = get_closest_edges(target_win, lx, ly);
2053 Some(crate::seat::PointerOpType::Resize { edges })
2054 }
2055 _ => None,
2056 };
2057
2058 if let Some(ot) = op_type {
2059 (*server).wm.stop_panning_animation();
2060 let cursor_x = (*cursor.wlr_cursor).x;
2061 let cursor_y = (*cursor.wlr_cursor).y;
2062 seat.op = Some(crate::seat::SeatOp {
2063 sent_release: false,
2064 input: crate::seat::SeatOpInput::Pointer,
2065 start_x: cursor_x as i32,
2066 start_y: cursor_y as i32,
2067 x: cursor_x as i32,
2068 y: cursor_y as i32,
2069 window_ptr: target_win,
2070 op_type: ot,
2071 start_win_x: (*target_win).box_geom.x,
2072 start_win_y: (*target_win).box_geom.y,
2073 start_win_w: (*target_win).box_geom.width as u32,
2074 start_win_h: (*target_win).box_geom.height as u32,
2075 start_win_virtual_x: (*target_win).virtual_x,
2076 start_win_virtual_y: (*target_win).virtual_y,
2077 start_tiling_mode: (*target_win).tiling_mode,
2078 start_was_tiled: grabbed_tiled,
2079 start_mode_locked: (*target_win).mode_locked,
2080 start_pan_x: (*(*seat).server).wm.desk_pan_x,
2081 start_pan_y: (*(*seat).server).wm.desk_pan_y,
2082 started_in_overview: (*(*seat).server).wm.mode == crate::window_manager::WindowManagerMode::Overview,
2083 });
2084 cursor.op_start_pointer();
2085 cursor.pressed.insert((*event).button, None);
2086
2087 match ot {
2088 crate::seat::PointerOpType::Resize { edges } => {
2089 let cursor_name = get_resize_cursor_name(edges);
2090 cursor.set_xcursor(cursor_name.as_ptr() as *const _);
2091 }
2092 crate::seat::PointerOpType::Move => {
2093 cursor.set_xcursor(b"grab\0".as_ptr() as *const _);
2094 }
2095 // A pointer bind is a move or a resize.
2096 crate::seat::PointerOpType::Select
2097 | crate::seat::PointerOpType::GroupMove => {}
2098 }
2099 return;
2100 }
2101 }
2102 }
2103
2104 let lx = cursor.x();
2105 let ly = cursor.y();
2106 let server = seat.server;
2107 let mut border_target_win: *mut crate::window::Window = std::ptr::null_mut();
2108 if let Some(result) = (*server).scene.at(lx, ly) {
2109 if let SceneNodeDataVal::Window(window) = result.data {
2110 border_target_win = window;
2111 }
2112 }
2113
2114 if !border_target_win.is_null() && !(*border_target_win).is_status_bar() && !(*border_target_win).is_wallpaper() && !(*border_target_win).is_grid() && (
2115 (*border_target_win).tiling_mode != crate::tiling::TilingMode::Popup
2116 && (*border_target_win).tiling_mode != crate::tiling::TilingMode::Fullscreen
2117 ) {
2118 let initial_mode = (*border_target_win).tiling_mode;
2119 let zone = get_border_zone(border_target_win, lx, ly);
2120 // The window context menu (`scripts/cce-app-menu`, a cce-cloud
2121 // popup like the desktop menu and cce-grid's item menu). A
2122 // right-click on a handle disc opens it in either adjust mode;
2123 // in OVERVIEW a right-click anywhere on the window does: the
2124 // client never sees buttons there (`should_block_button`), so
2125 // the press is the compositor's to spend, and the menu is how a
2126 // window's mode is set from the overview. Overlay docks are
2127 // chrome and keep their clicks (`overview_chrome` above);
2128 // Utility windows take no handles and have no mode to set.
2129 let menu_on_body = in_overview
2130 && !matches!(
2131 initial_mode,
2132 crate::tiling::TilingMode::Overlay | crate::tiling::TilingMode::Utility
2133 );
2134 if (*event).button == 0x111
2135 && modifiers == 0
2136 && (!matches!(zone, BorderZone::None) || menu_on_body)
2137 {
2138 cursor.right_click_on_border = true;
2139 let x = cursor.x() as i32;
2140 let y = cursor.y() as i32;
2141 let index = (*border_target_win).ref_key.index;
2142 let app_id = (*border_target_win).get_app_id_string().unwrap_or_else(|| "unknown".to_string());
2143 // The command runs under `sh -c`: quote the app_id, which
2144 // is client-chosen text.
2145 let app_id = format!("'{}'", app_id.replace('\'', "'\\''"));
2146 let home = std::env::var("HOME").unwrap_or_default();
2147 let cmd = format!("{}/.local/bin/cce-app-menu -x {} -y {} -i {} -a {}", home, x, y, index, app_id);
2148 (*server).wm.execute_action(&crate::config::Action::Spawn, Some(&cmd));
2149
2150 cursor.pressed.insert((*event).button, None);
2151 return;
2152 }
2153
2154 match zone {
2155 BorderZone::Resize(edges) => {
2156 if (*event).button == 0x110 { // BTN_LEFT
2157 // Unreachable for Utility (get_border_zone maps its
2158 // whole band to Move), and gated here regardless.
2159 if initial_mode == crate::tiling::TilingMode::Utility {
2160 return;
2161 }
2162 if initial_mode != crate::tiling::TilingMode::Floating {
2163 // A tiled window un-tiles for the drag but keeps its
2164 // cell-quantized geometry; landing grid-aligned re-tiles
2165 // it (op_end geometric detection).
2166 (*border_target_win).was_tiled = false;
2167 (*border_target_win).tiling_mode = crate::tiling::TilingMode::Floating;
2168 (*border_target_win).mode_locked = true;
2169 }
2170
2171 // No focus on the grab (see the body grab above).
2172 (*server).wm.stop_panning_animation();
2173 let cursor_x = (*cursor.wlr_cursor).x;
2174 let cursor_y = (*cursor.wlr_cursor).y;
2175 seat.op = Some(crate::seat::SeatOp {
2176 sent_release: false,
2177 input: crate::seat::SeatOpInput::Pointer,
2178 start_x: cursor_x as i32,
2179 start_y: cursor_y as i32,
2180 x: cursor_x as i32,
2181 y: cursor_y as i32,
2182 window_ptr: border_target_win,
2183 op_type: crate::seat::PointerOpType::Resize { edges },
2184 start_win_x: (*border_target_win).box_geom.x,
2185 start_win_y: (*border_target_win).box_geom.y,
2186 start_win_w: (*border_target_win).box_geom.width as u32,
2187 start_win_h: (*border_target_win).box_geom.height as u32,
2188 start_win_virtual_x: (*border_target_win).virtual_x,
2189 start_win_virtual_y: (*border_target_win).virtual_y,
2190 start_tiling_mode: (*border_target_win).tiling_mode,
2191 start_was_tiled: initial_mode == crate::tiling::TilingMode::Tiled,
2192 start_mode_locked: (*border_target_win).mode_locked,
2193 start_pan_x: (*(*seat).server).wm.desk_pan_x,
2194 start_pan_y: (*(*seat).server).wm.desk_pan_y,
2195 started_in_overview: (*(*seat).server).wm.mode == crate::window_manager::WindowManagerMode::Overview,
2196 });
2197 cursor.op_start_pointer();
2198 cursor.pressed.insert((*event).button, None);
2199
2200 let cursor_name = get_resize_cursor_name(edges);
2201 cursor.set_xcursor(cursor_name.as_ptr() as *const _);
2202 return;
2203 }
2204 }
2205 BorderZone::Move => {
2206 if (*event).button == 0x110 { // BTN_LEFT
2207 let current_time = (*event).time_msec;
2208 let is_titlebar = ly < (*border_target_win).box_geom.y as f64;
2209 let is_double_click = is_titlebar
2210 && border_target_win == cursor.last_click_window
2211 && current_time.saturating_sub(cursor.last_click_time) < 300;
2212
2213 cursor.last_click_time = current_time;
2214 cursor.last_click_window = border_target_win;
2215
2216 // A Utility window has no Tiled state to toggle;
2217 // the double-click falls through to an ordinary move.
2218 if is_double_click && initial_mode != crate::tiling::TilingMode::Utility {
2219 if initial_mode == crate::tiling::TilingMode::Tiled {
2220 (*border_target_win).tiling_mode = crate::tiling::TilingMode::Floating;
2221 (*border_target_win).mode_locked = true;
2222 } else {
2223 (*border_target_win).tiling_mode = crate::tiling::TilingMode::Tiled;
2224 (*border_target_win).mode_locked = true;
2225 }
2226 seat.focus(Focus::Window(border_target_win));
2227 (*server).wm.dirty_windowing();
2228 cursor.last_click_time = 0;
2229 cursor.last_click_window = std::ptr::null_mut();
2230 return;
2231 }
2232
2233 if initial_mode != crate::tiling::TilingMode::Floating
2234 && initial_mode != crate::tiling::TilingMode::Overlay
2235 // A drag moves a Utility window; it must not
2236 // re-class it.
2237 && initial_mode != crate::tiling::TilingMode::Utility
2238 {
2239 // A tiled window un-tiles for the drag but keeps its
2240 // cell-quantized geometry; landing grid-aligned re-tiles
2241 // it (op_end geometric detection).
2242 (*border_target_win).was_tiled = false;
2243 (*border_target_win).tiling_mode = crate::tiling::TilingMode::Floating;
2244 (*border_target_win).mode_locked = true;
2245 }
2246
2247 // No focus on the grab (see the body grab above).
2248 (*server).wm.stop_panning_animation();
2249 let cursor_x = (*cursor.wlr_cursor).x;
2250 let cursor_y = (*cursor.wlr_cursor).y;
2251 seat.op = Some(crate::seat::SeatOp {
2252 sent_release: false,
2253 input: crate::seat::SeatOpInput::Pointer,
2254 start_x: cursor_x as i32,
2255 start_y: cursor_y as i32,
2256 x: cursor_x as i32,
2257 y: cursor_y as i32,
2258 window_ptr: border_target_win,
2259 op_type: crate::seat::PointerOpType::Move,
2260 start_win_x: (*border_target_win).box_geom.x,
2261 start_win_y: (*border_target_win).box_geom.y,
2262 start_win_w: (*border_target_win).box_geom.width as u32,
2263 start_win_h: (*border_target_win).box_geom.height as u32,
2264 start_win_virtual_x: (*border_target_win).virtual_x,
2265 start_win_virtual_y: (*border_target_win).virtual_y,
2266 start_tiling_mode: (*border_target_win).tiling_mode,
2267 start_was_tiled: initial_mode == crate::tiling::TilingMode::Tiled,
2268 start_mode_locked: (*border_target_win).mode_locked,
2269 start_pan_x: (*(*seat).server).wm.desk_pan_x,
2270 start_pan_y: (*(*seat).server).wm.desk_pan_y,
2271 started_in_overview: (*(*seat).server).wm.mode == crate::window_manager::WindowManagerMode::Overview,
2272 });
2273 cursor.op_start_pointer();
2274 cursor.pressed.insert((*event).button, None);
2275
2276 cursor.set_xcursor(b"grab\0".as_ptr() as *const _);
2277 return;
2278 }
2279 }
2280 // Answered in the adjust-mode block above, which a button
2281 // zone always passes through (get_border_zone gives none
2282 // outside adjust mode).
2283 BorderZone::Button(_) => {}
2284 BorderZone::None => {}
2285 }
2286 }
2287
2288 if let Some(binding) = seat.match_pointer_binding((*event).button) {
2289 cursor.pressed.insert((*event).button, Some(binding));
2290 (*binding).pressed();
2291 return;
2292 }
2293
2294 cursor.pressed.insert((*event).button, None);
2295
2296 if !should_block_button {
2297 let first_grab_button = cursor.notified_pressed.is_empty();
2298 ffi::wlr_seat_pointer_notify_button(
2299 seat.wlr_seat,
2300 (*event).time_msec,
2301 (*event).button,
2302 (*event).state,
2303 );
2304 cursor.notified_pressed.insert((*event).button);
2305 // First grab button: record the pressed surface's layout origin
2306 // as the implicit grab's frame of reference (passthrough keeps
2307 // motion surface-relative through it while the button is held).
2308 if first_grab_button {
2309 let glx = cursor.x();
2310 let gly = cursor.y();
2311 if let Some(result) = (*seat.server).scene.at(glx, gly) {
2312 // A surface node's scene buffer begins with its node.
2313 let mut ratio = 1.0;
2314 if !result.surface.is_null() && !result.node.is_null() {
2315 let dest_w = ffi::river_scene_buffer_get_dest_width(result.node as *mut ffi::wlr_scene_buffer);
2316 let surf_w = ffi::river_wlr_surface_get_width(result.surface);
2317 if dest_w > 0 && surf_w > 0 {
2318 ratio = surf_w as f64 / dest_w as f64;
2319 }
2320 }
2321 cursor.grab_scale = ratio;
2322 cursor.grab_origin = (glx - result.sx / ratio, gly - result.sy / ratio);
2323 }
2324 // A grab that starts on the grid freezes its node mapping
2325 // here instead of using grab_origin: passthrough maps motion
2326 // against these values for the whole grab, so camera motion
2327 // (which moves the node every frame) reads as nothing rather
2328 // than as pointer motion. See the grab branch in passthrough.
2329 cursor.grab_grid = None;
2330 let grab_focused =
2331 ffi::river_wlr_seat_get_pointer_focused_surface(seat.wlr_seat);
2332 if !grab_focused.is_null() {
2333 if let Some((gsurf, nx, ny, scale, _, _)) = grid_node_info(seat.server) {
2334 if gsurf == grab_focused {
2335 cursor.grab_grid = Some((nx, ny, scale));
2336 }
2337 }
2338 }
2339 }
2340 }
2341
2342 // If pressed, update focus to window under cursor
2343 let lx = cursor.x();
2344 let ly = cursor.y();
2345 let server = seat.server;
2346 let mut clicked_something = false;
2347 if let Some(result) = (*server).scene.at(lx, ly) {
2348 match result.data {
2349 SceneNodeDataVal::Window(window) => {
2350 clicked_something = true;
2351 if !(*window).is_status_bar() && !(*window).is_wallpaper() {
2352 if !seat.object.is_null() {
2353 if !(*window).object.is_null() {
2354 ffi::wl_resource_post_event(seat.object, 4, (*window).object);
2355 (*(*seat).server).wm.dirty_windowing();
2356 }
2357 } else {
2358 seat.focus(Focus::Window(window));
2359 }
2360 }
2361 }
2362 SceneNodeDataVal::LayerSurface(layer_surface) => {
2363 clicked_something = true;
2364 if layer_takes_click_focus(layer_surface) {
2365 seat.focus(Focus::LayerSurface(result.surface));
2366 }
2367 }
2368 SceneNodeDataVal::ShellSurface(_) | SceneNodeDataVal::LockSurface(_) | SceneNodeDataVal::OverrideRedirect(_) => {
2369 clicked_something = true;
2370 }
2371 }
2372 }
2373
2374 if !clicked_something && (*event).button == 0x110 {
2375 // Restore placeholders are bare scene rects the hit-test can't
2376 // see, but they stand in for restored windows — clicking one
2377 // gets the same camera rules as clicking the real window.
2378 let wm = &mut (*server).wm;
2379 if let Some((pvx, pvy, pw, ph)) = wm.placeholder_at(lx, ly) {
2380 wm.pan_to_virtual_rect(pvx, pvy, pw, ph);
2381 } else {
2382 seat.focus(Focus::None);
2383 (*(*seat).server).wm.dirty_windowing();
2384 }
2385 }
2386 } else {
2387 assert_eq!((*event).state, ffi::wl_pointer_button_state_WL_POINTER_BUTTON_STATE_RELEASED);
2388 // Pair the release for the client BEFORE any compositor-side
2389 // consumption below: if the press was forwarded, the release always
2390 // is too, or the client is left with an orphaned press.
2391 if cursor.notified_pressed.remove(&(*event).button) {
2392 ffi::wlr_seat_pointer_notify_button(
2393 seat.wlr_seat,
2394 (*event).time_msec,
2395 (*event).button,
2396 (*event).state,
2397 );
2398 }
2399 // The implicit grab ends with its last button; the frozen grid
2400 // mapping must not outlive it into the next grab.
2401 if cursor.notified_pressed.is_empty() {
2402 cursor.grab_grid = None;
2403 }
2404 if seat.op.is_some() {
2405 let cursor_x = (*cursor.wlr_cursor).x;
2406 let cursor_y = (*cursor.wlr_cursor).y;
2407 seat.op_update(cursor_x as i32, cursor_y as i32);
2408
2409 let op = seat.op.unwrap();
2410
2411 if op.op_type == crate::seat::PointerOpType::Select {
2412 let dragged = (*server).wm.selection_release();
2413 // Released before the op ends, so its end re-evaluates the
2414 // pointer and the crosshair does not outlast the drag.
2415 cursor.pressed.remove(&(*event).button);
2416 seat.op_end();
2417 // A press that never travelled is a click on the desktop.
2418 // With windows selected it drops the selection, as a click
2419 // on empty grid does in cce-designer; with none it leaves
2420 // overview, as it always has.
2421 if !dragged && (*event).button == 0x110 {
2422 if (*server).wm.has_selection() {
2423 (*server).wm.selection_clear();
2424 } else {
2425 (*server).wm.execute_action(&crate::config::Action::Overview, None);
2426 }
2427 }
2428 return;
2429 }
2430
2431 #[allow(unused_assignments)]
2432 if !op.window_ptr.is_null()
2433 && (*op.window_ptr).is_status_bar()
2434 && op.op_type == crate::seat::PointerOpType::Move
2435 && (*event).button == 0x110
2436 {
2437 let win = op.window_ptr;
2438 let app_id = (*win).get_app_id_string().unwrap_or_default();
2439
2440 // A press that never travelled is a CLICK, not a drag: end
2441 // the grab without snapping — snapping classifies the
2442 // release point alone, so a still click on a top-edge
2443 // segment away from the corners re-homed it to top-center
2444 // — and replay press+release to the segment, which never
2445 // saw the press. The grab is taken on press so drag
2446 // feedback is immediate; this is where the two are told
2447 // apart.
2448 const STATUS_CLICK_TRAVEL: f64 = 6.0;
2449 let travel = (lx - op.start_x as f64).hypot(ly - op.start_y as f64);
2450 if travel < STATUS_CLICK_TRAVEL {
2451 log::info!("[StatusRelease] click (travel {:.1}px) on app_id={} — replayed, not snapped", travel, app_id);
2452 seat.op_end();
2453 cursor.pressed.remove(&(*event).button);
2454 let time = (*event).time_msec;
2455 // Re-evaluate pointer focus onto the surface under the
2456 // pointer (nothing was notified during the grab), then
2457 // deliver the click.
2458 cursor.passthrough(time);
2459 ffi::wlr_seat_pointer_notify_button(seat.wlr_seat, time, (*event).button, ffi::wl_pointer_button_state_WL_POINTER_BUTTON_STATE_PRESSED);
2460 ffi::wlr_seat_pointer_notify_frame(seat.wlr_seat);
2461 ffi::wlr_seat_pointer_notify_button(seat.wlr_seat, time, (*event).button, ffi::wl_pointer_button_state_WL_POINTER_BUTTON_STATE_RELEASED);
2462 ffi::wlr_seat_pointer_notify_frame(seat.wlr_seat);
2463 (*server).wm.dirty_windowing();
2464 return;
2465 }
2466
2467 let mut closest_edge = crate::window::StatusEdge::TopLeft;
2468 let mut min_dist = f64::MAX;
2469 log::info!("[StatusRelease] Released status window: app_id={}, lx={}, ly={}", app_id, lx, ly);
2470
2471 let outputs_list = &mut (*server).om.outputs as *mut ffi::wl_list as *mut WlList;
2472 let mut curr_out = (*outputs_list).next;
2473 let mut best_output: *mut crate::output::Output = std::ptr::null_mut();
2474 let mut min_output_dist = f64::MAX;
2475
2476 while curr_out != outputs_list {
2477 let output = crate::container_of!(curr_out, crate::output::Output, link);
2478 if (*output).sent.state == crate::output::OutputStateValue::Enabled {
2479 let wlr_box = (*output).sent.box_layout();
2480 let ox = wlr_box.x as f64;
2481 let oy = wlr_box.y as f64;
2482 let ow = wlr_box.width as f64;
2483 let oh = wlr_box.height as f64;
2484
2485 let clamp = |val: f64, min: f64, max: f64| {
2486 if val < min { min } else if val > max { max } else { val }
2487 };
2488 let cx = clamp(lx, ox, ox + ow);
2489 let cy = clamp(ly, oy, oy + oh);
2490 let dx = lx - cx;
2491 let dy = ly - cy;
2492 let dist = dx * dx + dy * dy;
2493 if dist < min_output_dist {
2494 min_output_dist = dist;
2495 best_output = output;
2496 }
2497 }
2498 curr_out = (*curr_out).next;
2499 }
2500
2501 let mut found_out = false;
2502 if !best_output.is_null() {
2503 found_out = true;
2504 let wlr_box = (*best_output).sent.box_layout();
2505 let ox = wlr_box.x as f64;
2506 let oy = wlr_box.y as f64;
2507 let ow = wlr_box.width as f64;
2508 let oh = wlr_box.height as f64;
2509
2510 log::info!("[StatusRelease] Checking best output: box_geom=({}, {}, {}, {})", ox, oy, ow, oh);
2511
2512 let dt = ly - oy;
2513 let db = (oy + oh) - ly;
2514 let dl = lx - ox;
2515 let dr = (ox + ow) - lx;
2516
2517 log::info!("[StatusRelease] Distances: top={}, bottom={}, left={}, right={}", dt, db, dl, dr);
2518 enum EdgeBasic { Top, Bottom, Left, Right }
2519 let mut edge = EdgeBasic::Top;
2520 if dt < min_dist { min_dist = dt; edge = EdgeBasic::Top; }
2521 if db < min_dist { min_dist = db; edge = EdgeBasic::Bottom; }
2522 if dl < min_dist { min_dist = dl; edge = EdgeBasic::Left; }
2523 if dr < min_dist { min_dist = dr; edge = EdgeBasic::Right; }
2524
2525 let corner_threshold = 120.0;
2526 let is_near_top = ly < oy + corner_threshold;
2527 let is_near_bottom = ly > oy + oh - corner_threshold;
2528 let is_near_left = lx < ox + corner_threshold;
2529 let is_near_right = lx > ox + ow - corner_threshold;
2530
2531 let semicircle_centers = [
2532 (crate::window::StatusEdge::TopLeft, ox + 60.0, oy + 0.0),
2533 (crate::window::StatusEdge::TopCenter, ox + ow / 2.0, oy + 0.0),
2534 (crate::window::StatusEdge::TopRight, ox + ow - 60.0, oy + 0.0),
2535 (crate::window::StatusEdge::BottomLeft, ox + 60.0, oy + oh),
2536 (crate::window::StatusEdge::BottomCenter, ox + ow / 2.0, oy + oh),
2537 (crate::window::StatusEdge::BottomRight, ox + ow - 60.0, oy + oh),
2538 (crate::window::StatusEdge::Left, ox + 0.0, oy + oh / 2.0),
2539 (crate::window::StatusEdge::Right, ox + ow, oy + oh / 2.0),
2540 ];
2541
2542 let mut snapped_to_semicircle = false;
2543 for (edge_type, cx, cy) in semicircle_centers {
2544 let dx = lx - cx;
2545 let dy = ly - cy;
2546 if dx * dx + dy * dy <= 60.0 * 60.0 {
2547 closest_edge = edge_type;
2548 snapped_to_semicircle = true;
2549 break;
2550 }
2551 }
2552
2553 if !snapped_to_semicircle {
2554 match edge {
2555 EdgeBasic::Top => {
2556 if is_near_left {
2557 closest_edge = crate::window::StatusEdge::TopLeft;
2558 } else if is_near_right {
2559 closest_edge = crate::window::StatusEdge::TopRight;
2560 } else {
2561 closest_edge = crate::window::StatusEdge::TopCenter;
2562 }
2563 }
2564 EdgeBasic::Bottom => {
2565 if is_near_left {
2566 closest_edge = crate::window::StatusEdge::BottomLeft;
2567 } else if is_near_right {
2568 closest_edge = crate::window::StatusEdge::BottomRight;
2569 } else {
2570 closest_edge = crate::window::StatusEdge::BottomCenter;
2571 }
2572 }
2573 EdgeBasic::Left => {
2574 if is_near_top {
2575 closest_edge = crate::window::StatusEdge::TopLeft;
2576 } else if is_near_bottom {
2577 closest_edge = crate::window::StatusEdge::BottomLeft;
2578 } else {
2579 closest_edge = crate::window::StatusEdge::Left;
2580 }
2581 }
2582 EdgeBasic::Right => {
2583 if is_near_top {
2584 closest_edge = crate::window::StatusEdge::TopRight;
2585 } else if is_near_bottom {
2586 closest_edge = crate::window::StatusEdge::BottomRight;
2587 } else {
2588 closest_edge = crate::window::StatusEdge::Right;
2589 }
2590 }
2591 }
2592 }
2593 }
2594
2595 log::info!("[StatusRelease] Snapping app_id={} closest_edge={:?}, found_out={}", app_id, closest_edge, found_out);
2596 (*win).status_edge = closest_edge;
2597
2598 let name = if let Some(stripped) = app_id.strip_prefix("cce-status-interface-left-").or_else(|| app_id.strip_prefix("cce-status-left-")) {
2599 stripped
2600 } else if let Some(stripped) = app_id.strip_prefix("cce-status-interface-right-").or_else(|| app_id.strip_prefix("cce-status-right-")) {
2601 stripped
2602 } else {
2603 &app_id
2604 };
2605 let edge_str = match closest_edge {
2606 crate::window::StatusEdge::Left => "left",
2607 crate::window::StatusEdge::Right => "right",
2608 crate::window::StatusEdge::TopLeft => "top-left",
2609 crate::window::StatusEdge::TopCenter => "top-center",
2610 crate::window::StatusEdge::TopRight => "top-right",
2611 crate::window::StatusEdge::BottomLeft => "bottom-left",
2612 crate::window::StatusEdge::BottomCenter => "bottom-center",
2613 crate::window::StatusEdge::BottomRight => "bottom-right",
2614 _ => "top-left",
2615 };
2616 let key_path = format!("layout.status_bar.{}", name);
2617 cce_core::config::write_config_value(
2618 &cce_core::config::get_config_path().to_string_lossy(),
2619 &key_path,
2620 &format!("\"{}\"", edge_str),
2621 "layout"
2622 );
2623
2624 seat.op_end();
2625 cursor.pressed.remove(&(*event).button);
2626 (*server).wm.dirty_windowing();
2627 return;
2628 }
2629 if op.started_in_overview && (*event).button == 0x110 {
2630 let moved = (cursor_x as i32 - op.start_x).abs() > 5 || (cursor_y as i32 - op.start_y).abs() > 5;
2631 if !moved && !op.window_ptr.is_null() {
2632 let win_ptr = op.window_ptr;
2633 // Restore original tiling mode & lock status
2634 (*win_ptr).tiling_mode = op.start_tiling_mode;
2635 (*win_ptr).mode_locked = op.start_mode_locked;
2636
2637 let server = seat.server;
2638 if !(*win_ptr).closed && !(*win_ptr).is_status_bar() && !(*win_ptr).is_wallpaper() {
2639 // The Overview toggle's exit path: it centers on the
2640 // HOVERED window (the cursor is on the clicked one),
2641 // focuses it, and ANIMATES the camera home along the
2642 // configured overview ramp — the same flight the
2643 // background-click exit takes, instead of the
2644 // instant cut this block used to hand-roll.
2645 (*server).wm.execute_action(&crate::config::Action::Overview, None);
2646 }
2647 }
2648 }
2649
2650 seat.op_end();
2651 cursor.pressed.remove(&(*event).button);
2652 return;
2653 }
2654 if let Some(binding_opt) = cursor.pressed.remove(&(*event).button) {
2655 if let Some(binding) = binding_opt {
2656 (*binding).released();
2657 if cursor.pressed.is_empty() && seat.op.is_some() {
2658 seat.op_release = true;
2659 (*(*seat).server).wm.dirty_windowing();
2660 }
2661 return;
2662 }
2663
2664 if (*event).button == 0x110 {
2665 if let Some((win, elem)) = cursor.button_press.take() {
2666 let alive = (*server).wm.windows.iter().any(|&w| w == win) && !(*win).closed;
2667 if alive && get_border_zone(win, lx, ly) == BorderZone::Button(elem) {
2668 (*server).wm.press_window_button(win, elem);
2669 }
2670 return;
2671 }
2672 }
2673
2674 if (*event).button == 0x111 && (cursor.right_click_on_bg || cursor.right_click_on_border) {
2675 cursor.right_click_on_bg = false;
2676 cursor.right_click_on_border = false;
2677 if cursor.pressed.is_empty() && seat.op.is_some() {
2678 seat.op_release = true;
2679 (*(*seat).server).wm.dirty_windowing();
2680 }
2681 return;
2682 }
2683
2684 // The client-facing release (when the press was forwarded) is
2685 // already paired at the top of the release path.
2686 if cursor.pressed.is_empty() && seat.op.is_some() {
2687 seat.op_release = true;
2688 (*(*seat).server).wm.dirty_windowing();
2689 }
2690 } else {
2691 log::error!("ignoring duplicate pointer button {} release", (*event).button);
2692 }
2693 }
2694 }
2695
2696 unsafe extern "C" fn handle_axis(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
2697 let cursor = &mut *crate::container_of!(listener, Cursor, axis_listener);
2698 let event = data as *mut ffi::wlr_pointer_axis_event;
2699 (*cursor.seat).handle_activity();
2700
2701 let seat = &mut *cursor.seat;
2702
2703 let mut delta = (*event).delta;
2704 let mut delta_discrete = (*event).delta_discrete;
2705
2706 if !(*event).pointer.is_null() {
2707 let wlr_device = &mut (*(*event).pointer).base as *mut ffi::wlr_input_device;
2708 let device_ptr = ffi::river_wlr_input_device_get_data(wlr_device) as *mut crate::input_device::InputDevice;
2709 if !device_ptr.is_null() {
2710 let factor = (*device_ptr).config.scroll_factor;
2711 delta *= factor;
2712 delta_discrete = (delta_discrete as f64 * factor) as i32;
2713 }
2714 }
2715
2716 let wlr_keyboard = ffi::river_wlr_seat_get_keyboard(seat.wlr_seat);
2717 // Modifiers held through `ccectl key-down` count too, so a headless
2718 // session can exercise the modifier branches below.
2719 let modifiers = if !wlr_keyboard.is_null() {
2720 ffi::wlr_keyboard_get_modifiers(wlr_keyboard)
2721 } else {
2722 0
2723 } | (*seat.server).wm.injected_key_mods;
2724
2725 if (modifiers & 0x44) == 0x44 {
2726 if (*event).orientation == ffi::wl_pointer_axis_WL_POINTER_AXIS_VERTICAL_SCROLL {
2727 if delta != 0.0 {
2728 let wm = &mut (*seat.server).wm;
2729 // Each notch advances the zoom TARGET (successive notches
2730 // accumulate into one glide); the animation tick eases the
2731 // zoom there in log space, pivoting about the cursor every
2732 // step. A pan glide or coast in flight yields to the zoom.
2733 let base = wm.target_desk_zoom.unwrap_or(wm.desk_zoom);
2734 let new_zoom = crate::policy::camera::wheel_zoom(base, delta);
2735 if new_zoom != base {
2736 let cx = cursor.x();
2737 let cy = cursor.y();
2738 let wlr_output = (*(*seat).server).om.output_at(cx, cy);
2739 let (phys_x, phys_y) = if !wlr_output.is_null() {
2740 let mut output_box = ffi::wlr_box { x: 0, y: 0, width: 0, height: 0 };
2741 ffi::wlr_output_layout_get_box((*(*seat).server).om.output_layout, wlr_output, &mut output_box);
2742 (output_box.x as f64, output_box.y as f64)
2743 } else {
2744 (0.0, 0.0)
2745 };
2746 wm.stop_panning_animation();
2747 wm.target_desk_zoom = Some(new_zoom);
2748 wm.zoom_anchor = Some((cx - phys_x, cy - phys_y));
2749 wm.set_mode(if crate::policy::camera::is_overview(new_zoom) { crate::window_manager::WindowManagerMode::Overview } else { crate::window_manager::WindowManagerMode::Normal });
2750 wm.start_panning_animation();
2751 }
2752 }
2753 }
2754 return;
2755 }
2756
2757 let (is_on_background, over_chrome) = {
2758 let lx = cursor.x();
2759 let ly = cursor.y();
2760 let server = seat.server;
2761 let mut over_interactive = false;
2762 // Chrome under the pointer — a Popup (the cce-cloud launcher) or an
2763 // Overlay dock, or a cce-cloud layer surface (context menu). Live UI
2764 // during overview, same as in the button and motion paths: a wheel
2765 // over the launcher's list scrolls the list, not the desktop.
2766 let mut over_chrome = false;
2767 if let Some(result) = (*server).scene.at(lx, ly) {
2768 match result.data {
2769 SceneNodeDataVal::Window(window) => {
2770 over_interactive = true;
2771 over_chrome = !window.is_null()
2772 && matches!(
2773 (*window).tiling_mode,
2774 crate::tiling::TilingMode::Popup | crate::tiling::TilingMode::Overlay
2775 );
2776 }
2777 SceneNodeDataVal::LayerSurface(layer_surface) => {
2778 over_interactive = true;
2779 over_chrome = is_cloud_layer(layer_surface);
2780 }
2781 SceneNodeDataVal::ShellSurface(_) | SceneNodeDataVal::LockSurface(_) | SceneNodeDataVal::OverrideRedirect(_) => {
2782 over_interactive = true;
2783 }
2784 }
2785 }
2786 (!over_interactive, over_chrome)
2787 };
2788 // Overview pans on any scroll — except over chrome, which takes the
2789 // event itself.
2790 let is_overview = (*(*seat).server).wm.mode == crate::window_manager::WindowManagerMode::Overview
2791 && !over_chrome;
2792
2793 let is_finger = (*event).source == ffi::wl_pointer_axis_source_WL_POINTER_AXIS_SOURCE_FINGER
2794 || (*event).source == ffi::wl_pointer_axis_source_WL_POINTER_AXIS_SOURCE_CONTINUOUS;
2795
2796 let mut was_panning = cursor.panning_gesture_active;
2797
2798 if is_finger {
2799 if delta == 0.0 {
2800 cursor.panning_gesture_active = false;
2801 } else if !cursor.panning_gesture_active && (is_on_background || is_overview) {
2802 cursor.panning_gesture_active = true;
2803 was_panning = true;
2804 }
2805 }
2806
2807 if (modifiers & 0x40) != 0 || is_on_background || is_overview || was_panning {
2808 let wm = &mut (*seat.server).wm;
2809 let step = delta / wm.desk_zoom;
2810 let vertical =
2811 (*event).orientation == ffi::wl_pointer_axis_WL_POINTER_AXIS_VERTICAL_SCROLL;
2812 if is_finger {
2813 let axis = if vertical { 1 } else { 0 };
2814 let now_ms = (*event).time_msec;
2815 if delta != 0.0 {
2816 // Finger/continuous scroll tracks 1:1 — the surface follows
2817 // the gesture directly, no easing between the two — while a
2818 // velocity estimate is kept for the coast on the lift.
2819 wm.stop_panning_animation();
2820 if vertical {
2821 wm.queue_pan(0.0, step);
2822 } else {
2823 wm.queue_pan(step, 0.0);
2824 }
2825 let dt_ms = now_ms.wrapping_sub(cursor.pan_last_msec[axis]).clamp(4, 100) as f64;
2826 let sample = step / (dt_ms / 1000.0);
2827 cursor.pan_vel[axis] = if cursor.pan_last_msec[axis] == 0 {
2828 sample
2829 } else {
2830 cursor.pan_vel[axis] * 0.65 + sample * 0.35
2831 };
2832 cursor.pan_last_msec[axis] = now_ms;
2833 wm.pan_finger_v[axis] = cursor.pan_vel[axis];
2834 } else if was_panning {
2835 // The lift (libinput's zero-delta finger event): fling on the
2836 // estimated velocity unless the finger had come to rest first
2837 // or kinetic scrolling is off. Both axes launch together on
2838 // the first lift event; the second axis's lift finds them
2839 // already cleared.
2840 let mut vx = cursor.pan_vel[0];
2841 let mut vy = cursor.pan_vel[1];
2842 for (a, v) in [(0usize, &mut vx), (1usize, &mut vy)] {
2843 let rest_ms = now_ms.wrapping_sub(cursor.pan_last_msec[a]);
2844 if cursor.pan_last_msec[a] == 0 || rest_ms > 80 {
2845 *v = 0.0;
2846 }
2847 }
2848 cursor.pan_vel = [0.0, 0.0];
2849 cursor.pan_last_msec = [0, 0];
2850 wm.pan_finger_v = [0.0, 0.0];
2851 if wm.kinetic_scroll() && (vx != 0.0 || vy != 0.0) {
2852 wm.pan_coast_vx = vx;
2853 wm.pan_coast_vy = vy;
2854 wm.start_panning_animation();
2855 }
2856 }
2857 } else {
2858 // Discrete wheel clicks glide: each click advances the pan
2859 // animation target, so successive clicks accumulate into one
2860 // smooth run instead of a stutter of jumps. A coast in flight
2861 // yields to the click.
2862 wm.pan_coast_vx = 0.0;
2863 wm.pan_coast_vy = 0.0;
2864 if vertical {
2865 let base = wm.target_desk_pan_y.unwrap_or(wm.desk_pan_y);
2866 wm.target_desk_pan_y = Some(base + step);
2867 } else {
2868 let base = wm.target_desk_pan_x.unwrap_or(wm.desk_pan_x);
2869 wm.target_desk_pan_x = Some(base + step);
2870 }
2871 wm.start_panning_animation();
2872 }
2873 return;
2874 }
2875
2876 // A two-finger scroll over an app in `touchpad_view_apps` becomes a
2877 // view drag instead of a scroll (see `ViewDrag`), and one over that
2878 // app's own popup becomes a wheel (see `PopupWheel`).
2879 if is_finger && cursor.view_drag_axis(event, delta, modifiers) {
2880 return;
2881 }
2882 if is_finger && cursor.popup_wheel_axis(event, delta, modifiers) {
2883 return;
2884 }
2885 // A horizontal one over an app in `touchpad_hscroll_shift_apps` is
2886 // delivered as Shift + vertical (see `HScrollShift`).
2887 if is_finger && cursor.hscroll_shift_axis(event, delta, delta_discrete, modifiers) {
2888 return;
2889 }
2890
2891 ffi::wlr_seat_pointer_notify_axis(
2892 seat.wlr_seat,
2893 (*event).time_msec,
2894 (*event).orientation,
2895 delta,
2896 delta_discrete,
2897 (*event).source,
2898 (*event).relative_direction,
2899 );
2900 }
2901
2902 /// An emulated view drag: trackpad input over a window turned into what
2903 /// a 3D app's view tool understands, a held key plus a button drag.
2904 ///
2905 /// Why this exists: Houdini's own trackpad gestures cannot work under X11.
2906 /// Xwayland attributes every scroll to a device Qt classifies as a
2907 /// TouchPad, Houdini's touchpad "slide" then moves the view by the wheel
2908 /// event's pixel deltas, and Qt's X11 backend never fills those in (it
2909 /// does so only for a scroll increment above 15; Xwayland's is 1, the
2910 /// libinput X driver's 15). Verified against Houdini 22 headless: the slide
2911 /// is a no-op and the mouse wheel is swallowed with it, while Space + a
2912 /// button drag tumbles, pans and dollies. So for apps listed in
2913 /// `window_manager.touchpad_view_apps` the compositor synthesises exactly
2914 /// that: Space down, button down, the finger deltas as pointer motion on
2915 /// the surface (the on-screen cursor never moves), button and Space up
2916 /// when the fingers lift. Two-finger swipe pans (middle button) or tumbles
2917 /// (left) per `touchpad_view_swipe`, Shift picks the other, a pinch
2918 /// dollies (right button, distance from the log of the scale), and Ctrl
2919 /// + swipe passes through as a plain scroll — the same modifier Houdini
2920 /// itself assigns to "simulate the mouse wheel" in gesture mode.
2921 ///
2922 /// Natural scrolling is undone here. libinput flips the sign of a finger
2923 /// delta before the compositor sees it, which is right for a scroll (the
2924 /// content follows the fingers) and wrong for a drag replayed as pointer
2925 /// motion: the view follows the pointer, so the pointer has to go where
2926 /// the fingers went. `axis_event_is_natural` asks the source device, and
2927 /// `touchpad_view_invert` then means "backwards" on top of that either way.
2928 pub struct ViewDrag {
2929 pub button: u32,
2930 pub surface: *mut ffi::wlr_surface,
2931 pub window: *mut crate::window::Window,
2932 /// Synthetic pointer position, surface-local.
2933 pub sx: f64,
2934 pub sy: f64,
2935 pub origin_sx: f64,
2936 pub origin_sy: f64,
2937 /// Surface units per layout pixel (X11 HiDPI buffers, overview zoom).
2938 pub ratio: f64,
2939 pub from_pinch: bool,
2940 /// The button goes down on the first motion, one event after Space:
2941 /// Houdini feeds Qt input to its UI thread through a generator thread,
2942 /// and a button that arrives in the same instant as Space can be
2943 /// interpreted before the key — a right button then reads as a pan,
2944 /// not a dolly.
2945 pub button_down: bool,
2946 /// The keyboard's repeat settings before the drag, restored at its end.
2947 /// While Space is held synthetically the seat's repeat is switched off:
2948 /// Xwayland autorepeats a held key as release/press pairs, and Houdini
2949 /// left view mode on the first release, mid-drag.
2950 pub repeat: Option<(i32, i32)>,
2951 }
2952
2953 const KEY_SPACE: u32 = 57;
2954 const BTN_LEFT: u32 = 0x110;
2955 const BTN_RIGHT: u32 = 0x111;
2956 const BTN_MIDDLE: u32 = 0x112;
2957 /// Safety net that ends a swipe drag when the lift event never came.
2958 /// Only that: libinput posts nothing at all while the fingers rest on the
2959 /// pad mid-gesture — a captured Houdini swipe paused 3.3 s between two
2960 /// halves of one scroll, the zero-delta lift arriving only at the true end
2961 /// — so a short timeout tears the drag down inside the gesture, and the
2962 /// Space release/re-press a resume then costs can drop Houdini out of view
2963 /// mode for the rest of the swipe (the ordering hazard `button_down`
2964 /// documents, now mid-swipe). The lift ends a drag; so, at once, do real
2965 /// pointer motion, a button and a key press.
2966 const VIEW_DRAG_IDLE_MS: i32 = 5000;
2967 /// Drag distance (layout px) per e-fold of pinch scale. Measured against
2968 /// Houdini 22 (depth of the world origin in view space, which is what a
2969 /// dolly changes — Houdini dollies toward the point under the pointer, so
2970 /// distances to a fixed pivot mislead): Space+RMB dollies on the VERTICAL
2971 /// drag, up is in, and 45 px up shortened the depth by a factor of 1.34,
2972 /// about 150 px per e-fold. So a pinch of scale s becomes an upward drag
2973 /// of ln(s) e-folds and the depth ends near 1/s.
2974 const VIEW_DRAG_PINCH_PX: f64 = 150.0;
2975
2976 impl Cursor {
2977 /// The window under the pointer, if `touchpad_view_apps` names its app.
2978 unsafe fn view_drag_target(&mut self) -> Option<(*mut crate::window::Window, *mut ffi::wlr_surface, f64, f64, f64)> {
2979 let server = (*self.seat).server;
2980 let wm = &(*server).wm;
2981 if wm.touchpad_view_apps.is_empty() {
2982 return None;
2983 }
2984 let result = (*server).scene.at(self.x(), self.y())?;
2985 let SceneNodeDataVal::Window(window) = result.data else { return None };
2986 if window.is_null() || result.surface.is_null() || result.node.is_null() {
2987 return None;
2988 }
2989 let app_id = (*window).get_app_id_string().unwrap_or_default();
2990 if !wm.touchpad_view_apps.iter().any(|p| crate::window_manager::app_id_matches(p, &app_id)) {
2991 return None;
2992 }
2993 // The app may have narrowed the drag to its own view panes (see
2994 // `touchpad-view-regions`); elsewhere the scroll passes through.
2995 // `result.sx`/`sy` are surface-local, the same pixels the client
2996 // measures its panes in.
2997 if let Some(regions) = &(*window).view_regions {
2998 if !crate::window_manager::point_in_view_regions(regions, result.sx, result.sy) {
2999 return None;
3000 }
3001 }
3002 let mut ratio = 1.0;
3003 let dest_w = ffi::river_scene_buffer_get_dest_width(result.node as *mut ffi::wlr_scene_buffer);
3004 let surf_w = ffi::river_wlr_surface_get_width(result.surface);
3005 if dest_w > 0 && surf_w > 0 {
3006 ratio = surf_w as f64 / dest_w as f64;
3007 }
3008 Some((window, result.surface, result.sx, result.sy, ratio))
3009 }
3010
3011 /// Whether the touchpad behind an axis event has natural scrolling on,
3012 /// i.e. its deltas arrive sign-flipped. An injected event has no device
3013 /// and answers with `inject_natural` (see `pointer-scroll ... natural`).
3014 unsafe fn axis_event_is_natural(&self, event: *const ffi::wlr_pointer_axis_event) -> bool {
3015 let pointer = (*event).pointer;
3016 if pointer.is_null() {
3017 return self.inject_natural;
3018 }
3019 let dev = &mut (*pointer).base as *mut ffi::wlr_input_device;
3020 if !ffi::wlr_input_device_is_libinput(dev) {
3021 return false;
3022 }
3023 let handle = ffi::wlr_libinput_get_device_handle(dev);
3024 !handle.is_null()
3025 && ffi::libinput_device_config_scroll_has_natural_scroll(handle) != 0
3026 && ffi::libinput_device_config_scroll_get_natural_scroll_enabled(handle) != 0
3027 }
3028
3029 unsafe fn begin_view_drag(&mut self, button: u32, from_pinch: bool) -> bool {
3030 let Some((window, surface, sx, sy, ratio)) = self.view_drag_target() else { return false };
3031 let seat = &mut *self.seat;
3032 // Space must reach the window: give it keyboard focus as a click would.
3033 if seat.focused != crate::seat::Focus::Window(window) {
3034 seat.focus(crate::seat::Focus::Window(window));
3035 }
3036 seat.ensure_synthetic_keyboard();
3037 let time = crate::util::msec_timestamp();
3038 let mut repeat = None;
3039 let kbd = ffi::river_wlr_seat_get_keyboard(seat.wlr_seat);
3040 if !kbd.is_null() {
3041 repeat = Some(((*kbd).repeat_info.rate, (*kbd).repeat_info.delay));
3042 ffi::wlr_keyboard_set_repeat_info(kbd, 0, 0);
3043 }
3044 ffi::wlr_seat_pointer_notify_enter(seat.wlr_seat, surface, sx, sy);
3045 ffi::wlr_seat_keyboard_notify_key(seat.wlr_seat, time, KEY_SPACE, ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_PRESSED);
3046 ffi::wlr_seat_pointer_notify_frame(seat.wlr_seat);
3047 log::info!("[ViewDrag] begin button={:#x} from_pinch={} at surface ({:.0}, {:.0}) ratio={}", button, from_pinch, sx, sy, ratio);
3048 self.view_drag = Some(ViewDrag { button, surface, window, sx, sy, origin_sx: sx, origin_sy: sy, ratio, from_pinch, button_down: false, repeat });
3049 self.arm_view_drag_timer();
3050 true
3051 }
3052
3053 unsafe fn arm_view_drag_timer(&mut self) {
3054 if !self.view_drag_timer.is_null() {
3055 ffi::wl_event_source_timer_update(self.view_drag_timer, VIEW_DRAG_IDLE_MS);
3056 }
3057 }
3058
3059 /// Move the synthetic pointer by layout pixels.
3060 unsafe fn move_view_drag(&mut self, dx: f64, dy: f64) {
3061 let Some(d) = self.view_drag.as_mut() else { return };
3062 let seat = &mut *self.seat;
3063 let time = crate::util::msec_timestamp();
3064 if !d.button_down {
3065 ffi::wlr_seat_pointer_notify_button(seat.wlr_seat, time, d.button, ffi::wl_pointer_button_state_WL_POINTER_BUTTON_STATE_PRESSED);
3066 ffi::wlr_seat_pointer_notify_frame(seat.wlr_seat);
3067 d.button_down = true;
3068 }
3069 d.sx += dx * d.ratio;
3070 d.sy += dy * d.ratio;
3071 let (sx, sy) = (d.sx, d.sy);
3072 ffi::wlr_seat_pointer_notify_motion(seat.wlr_seat, time, sx, sy);
3073 ffi::wlr_seat_pointer_notify_frame(seat.wlr_seat);
3074 self.arm_view_drag_timer();
3075 }
3076
3077 /// `reason` names what ended it — "lift", "idle", "motion", "button",
3078 /// "key", "ctrl" or "pinch" — so a stall reported later is diagnosable
3079 /// from the session log alone.
3080 pub unsafe fn end_view_drag(&mut self, reason: &str) {
3081 let Some(d) = self.view_drag.take() else { return };
3082 log::info!("[ViewDrag] end reason={} button={:#x} from_pinch={} at surface ({:.0}, {:.0})", reason, d.button, d.from_pinch, d.sx, d.sy);
3083 if !self.view_drag_timer.is_null() {
3084 ffi::wl_event_source_timer_update(self.view_drag_timer, 0);
3085 }
3086 let seat = &mut *self.seat;
3087 let time = crate::util::msec_timestamp();
3088 if d.button_down {
3089 ffi::wlr_seat_pointer_notify_button(seat.wlr_seat, time, d.button, ffi::wl_pointer_button_state_WL_POINTER_BUTTON_STATE_RELEASED);
3090 ffi::wlr_seat_pointer_notify_frame(seat.wlr_seat);
3091 }
3092 ffi::wlr_seat_keyboard_notify_key(seat.wlr_seat, time, KEY_SPACE, ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_RELEASED);
3093 if let Some((rate, delay)) = d.repeat {
3094 let kbd = ffi::river_wlr_seat_get_keyboard(seat.wlr_seat);
3095 if !kbd.is_null() {
3096 ffi::wlr_keyboard_set_repeat_info(kbd, rate, delay);
3097 }
3098 }
3099 // Put the client's idea of the pointer back where the cursor is.
3100 self.passthrough(time);
3101 ffi::wlr_seat_pointer_notify_frame((*self.seat).wlr_seat);
3102 }
3103
3104 /// A finger-source axis event over a `touchpad_view_apps` window.
3105 /// Returns true when it was consumed by the emulation.
3106 pub unsafe fn view_drag_axis(&mut self, event: *const ffi::wlr_pointer_axis_event, delta: f64, modifiers: u32) -> bool {
3107 const SHIFT: u32 = 0x1;
3108 const CTRL: u32 = 0x4;
3109 if matches!(&self.view_drag, Some(d) if d.from_pinch) {
3110 return false;
3111 }
3112 if delta == 0.0 {
3113 // The fingers lifted.
3114 if self.view_drag.is_some() {
3115 self.end_view_drag("lift");
3116 return true;
3117 }
3118 return false;
3119 }
3120 if modifiers & CTRL != 0 {
3121 // Houdini's own wheel modifier: a plain scroll.
3122 if self.view_drag.is_some() {
3123 self.end_view_drag("ctrl");
3124 }
3125 return false;
3126 }
3127 if self.view_drag.is_none() {
3128 let wm = &(*(*self.seat).server).wm;
3129 let tumble = wm.touchpad_view_swipe_tumble != (modifiers & SHIFT != 0);
3130 let button = if tumble { BTN_LEFT } else { BTN_MIDDLE };
3131 if !self.begin_view_drag(button, false) {
3132 return false;
3133 }
3134 }
3135 let wm = &(*(*self.seat).server).wm;
3136 let mut step = delta * wm.touchpad_view_sensitivity;
3137 if self.axis_event_is_natural(event) {
3138 step = -step;
3139 }
3140 if wm.touchpad_view_invert {
3141 step = -step;
3142 }
3143 if (*event).orientation == ffi::wl_pointer_axis_WL_POINTER_AXIS_VERTICAL_SCROLL {
3144 self.move_view_drag(0.0, step);
3145 } else {
3146 self.move_view_drag(step, 0.0);
3147 }
3148 true
3149 }
3150
3151 pub unsafe fn view_drag_pinch_begin(&mut self) -> bool {
3152 if self.view_drag.is_some() {
3153 self.end_view_drag("pinch");
3154 }
3155 self.begin_view_drag(BTN_RIGHT, true)
3156 }
3157
3158 pub unsafe fn view_drag_pinch_update(&mut self, scale: f64) -> bool {
3159 let Some(d) = self.view_drag.as_ref() else { return false };
3160 if !d.from_pinch {
3161 return false;
3162 }
3163 let wm = &(*(*self.seat).server).wm;
3164 // Pinch out (scale > 1) dollies in: an upward drag.
3165 let px = -scale.max(0.05).ln() * VIEW_DRAG_PINCH_PX * wm.touchpad_view_sensitivity;
3166 let target_sy = d.origin_sy + px * d.ratio;
3167 let dy_layout = (target_sy - d.sy) / d.ratio;
3168 self.move_view_drag(0.0, dy_layout);
3169 true
3170 }
3171
3172 pub unsafe fn view_drag_pinch_end(&mut self) -> bool {
3173 if matches!(&self.view_drag, Some(d) if d.from_pinch) {
3174 self.end_view_drag("lift");
3175 return true;
3176 }
3177 false
3178 }
3179 }
3180
3181 /// A trackpad scroll over one of those apps' own popups, turned into the
3182 /// wheel the popup understands.
3183 ///
3184 /// Why this exists: `ViewDrag` covers the app's window, but a menu is an
3185 /// X11 override-redirect surface, so a scroll over one falls through as a
3186 /// plain axis event — and Houdini with "Enable Trackpad Gestures" on routes
3187 /// every scroll, a mouse wheel included, into its touchpad slide, which
3188 /// moves the content by `QWheelEvent::pixelDelta`. Under Xwayland that is
3189 /// always zero, and no event the compositor can shape changes it: Qt's xcb
3190 /// backend synthesises pixelDelta only above a scroll increment of 15, and
3191 /// Xwayland hardcodes its XIScrollClass increment at 1 (measured on a
3192 /// shadow session — `xinput list --long` reports `increment: 1.000000` on
3193 /// both valuators, and a Qt6 probe logged `pixel=0` for wheel-source and
3194 /// finger-source scrolls alike, on a QMenu the compositor delivered to
3195 /// correctly). What does work is the app's own escape hatch: Houdini's
3196 /// `touchpadwheelmodifier` — "simulate the mouse wheel", Ctrl — makes it
3197 /// read a scroll as a wheel again. So over such a popup the compositor
3198 /// holds Ctrl for the gesture and forwards the finger deltas as whole
3199 /// notches, which is what the user otherwise has to do by hand.
3200 pub struct PopupWheel {
3201 /// The popup the gesture started on. It also ends the gesture: if the
3202 /// pointer focus moves off it (the menu closed, or the pointer left),
3203 /// the held Ctrl must not ride along onto whatever took its place.
3204 pub surface: *mut ffi::wlr_surface,
3205 /// Sub-notch remainder per axis (0 horizontal, 1 vertical), layout px.
3206 pub accum: [f64; 2],
3207 pub ctrl_down: bool,
3208 /// The keyboard's repeat settings before the gesture, restored at its
3209 /// end. Xwayland autorepeats a held key as release/press pairs, which
3210 /// would drop the modifier mid-swipe — `ViewDrag` hit the same thing
3211 /// with Space.
3212 pub repeat: Option<(i32, i32)>,
3213 }
3214
3215 const KEY_LEFTCTRL: u32 = 29;
3216 /// Layout pixels per emitted notch — the unit wl_pointer and `inject_scroll`
3217 /// already use for one wheel click.
3218 const POPUP_WHEEL_NOTCH_PX: f64 = 15.0;
3219 /// Finger silence that ends a popup gesture when the lift never came. Far
3220 /// shorter than the view drag's net, because the two failure modes are not
3221 /// alike: a resumed swipe only re-presses Ctrl, with no view mode to fall
3222 /// out of, while a modifier left held would turn the user's next click into
3223 /// a Ctrl-click.
3224 const POPUP_WHEEL_IDLE_MS: i32 = 400;
3225
3226 impl Cursor {
3227 /// The popup under the pointer, when it belongs to the same process as
3228 /// a window `touchpad_view_apps` names and the keyboard is inside that
3229 /// app. Houdini's menus carry no WM_CLASS of their own, so the pid is
3230 /// what ties one to its app — the same test
3231 /// `XwaylandOverrideRedirect::focus_if_desired` uses. The keyboard
3232 /// check is what keeps a synthetic Ctrl from landing in some other
3233 /// client: the popup takes focus itself when it wants it, otherwise
3234 /// focus stays on the window it belongs to.
3235 unsafe fn popup_wheel_target(&mut self) -> Option<*mut ffi::wlr_surface> {
3236 let server = (*self.seat).server;
3237 let wm = &(*server).wm;
3238 if wm.touchpad_view_apps.is_empty() {
3239 return None;
3240 }
3241 let result = (*server).scene.at(self.x(), self.y())?;
3242 let SceneNodeDataVal::OverrideRedirect(or) = result.data else { return None };
3243 if or.is_null() || result.surface.is_null() || (*or).xsurface.is_null() {
3244 return None;
3245 }
3246 let pid = (*(*or).xsurface).pid;
3247 let focused = ffi::river_wlr_seat_get_keyboard_focused_surface((*self.seat).wlr_seat);
3248 if focused.is_null() {
3249 return None;
3250 }
3251 for &window in wm.windows.iter() {
3252 if window.is_null() {
3253 continue;
3254 }
3255 let crate::window::WindowImpl::Xwayland(xwindow) = (*window).impl_type else { continue };
3256 if xwindow.is_null() || (*(*xwindow).xsurface).pid != pid {
3257 continue;
3258 }
3259 let app_id = (*window).get_app_id_string().unwrap_or_default();
3260 if !wm.touchpad_view_apps.iter().any(|p| crate::window_manager::app_id_matches(p, &app_id)) {
3261 continue;
3262 }
3263 if focused == result.surface || focused == (*window).root_surface() {
3264 return Some(result.surface);
3265 }
3266 }
3267 None
3268 }
3269
3270 /// Hold, or drop, the app's "simulate the mouse wheel" modifier on the
3271 /// client's behalf. The mask is OR'd over the keyboard's live state and
3272 /// never into `injected_key_mods`, so every modifier read the compositor
3273 /// makes for itself — the Ctrl branch in `view_drag_axis` among them —
3274 /// keeps seeing the user's real keys and not this one.
3275 unsafe fn hold_popup_ctrl(&mut self, down: bool) {
3276 match self.popup_wheel.as_mut() {
3277 Some(p) if p.ctrl_down != down => p.ctrl_down = down,
3278 _ => return,
3279 }
3280 self.hold_synthetic_modifier(KEY_LEFTCTRL, b"Control\0", down);
3281 }
3282
3283 /// Press or release `key` on the client's behalf and OR its xkb
3284 /// modifier (`xkb_name`, NUL-terminated) over the keyboard's live state.
3285 /// Never touches `injected_key_mods`, so the compositor's own modifier
3286 /// reads keep seeing the user's real keys. Shared by `PopupWheel`
3287 /// (Ctrl) and `HScrollShift` (Shift).
3288 unsafe fn hold_synthetic_modifier(&mut self, key: u32, xkb_name: &[u8], down: bool) {
3289 let seat = &mut *self.seat;
3290 let time = crate::util::msec_timestamp();
3291 let state = if down {
3292 ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_PRESSED
3293 } else {
3294 ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_RELEASED
3295 };
3296 ffi::wlr_seat_keyboard_notify_key(seat.wlr_seat, time, key, state);
3297 let kb = ffi::river_wlr_seat_get_keyboard(seat.wlr_seat);
3298 if !kb.is_null() && !(*kb).keymap.is_null() {
3299 let idx = ffi::xkb_keymap_mod_get_index((*kb).keymap, xkb_name.as_ptr() as *const _);
3300 if idx != ffi::XKB_MOD_INVALID {
3301 // Released sends the device's own state back, which is the
3302 // user's keys minus this bit.
3303 let mut mods = (*kb).modifiers;
3304 if down {
3305 mods.depressed |= 1u32 << idx;
3306 }
3307 ffi::wlr_seat_keyboard_notify_modifiers(seat.wlr_seat, &mut mods);
3308 }
3309 }
3310 }
3311
3312 unsafe fn arm_popup_wheel_timer(&mut self) {
3313 if !self.view_drag_timer.is_null() {
3314 ffi::wl_event_source_timer_update(self.view_drag_timer, POPUP_WHEEL_IDLE_MS);
3315 }
3316 }
3317
3318 /// `reason` names what ended it, as `end_view_drag`'s does.
3319 pub unsafe fn end_popup_wheel(&mut self, reason: &str) {
3320 if self.popup_wheel.is_none() {
3321 return;
3322 }
3323 self.hold_popup_ctrl(false);
3324 let Some(p) = self.popup_wheel.take() else { return };
3325 log::info!("[PopupWheel] end reason={}", reason);
3326 if !self.view_drag_timer.is_null() {
3327 ffi::wl_event_source_timer_update(self.view_drag_timer, 0);
3328 }
3329 if let Some((rate, delay)) = p.repeat {
3330 let kbd = ffi::river_wlr_seat_get_keyboard((*self.seat).wlr_seat);
3331 if !kbd.is_null() {
3332 ffi::wlr_keyboard_set_repeat_info(kbd, rate, delay);
3333 }
3334 }
3335 }
3336
3337 /// A finger-source axis event over such a popup, emitted to it as whole
3338 /// wheel notches under a held Ctrl. Returns true when consumed.
3339 pub unsafe fn popup_wheel_axis(&mut self, event: *const ffi::wlr_pointer_axis_event, delta: f64, modifiers: u32) -> bool {
3340 const CTRL: u32 = 0x4;
3341 if delta == 0.0 {
3342 // The fingers lifted.
3343 if self.popup_wheel.is_some() {
3344 self.end_popup_wheel("lift");
3345 return true;
3346 }
3347 return false;
3348 }
3349 if modifiers & CTRL != 0 {
3350 // The user is already holding the app's wheel modifier: the
3351 // scroll passes through as it does today.
3352 if self.popup_wheel.is_some() {
3353 self.end_popup_wheel("ctrl");
3354 }
3355 return false;
3356 }
3357 if self.popup_wheel.is_none() {
3358 let Some(surface) = self.popup_wheel_target() else { return false };
3359 let seat = &mut *self.seat;
3360 seat.ensure_synthetic_keyboard();
3361 let mut repeat = None;
3362 let kbd = ffi::river_wlr_seat_get_keyboard(seat.wlr_seat);
3363 if !kbd.is_null() {
3364 repeat = Some(((*kbd).repeat_info.rate, (*kbd).repeat_info.delay));
3365 ffi::wlr_keyboard_set_repeat_info(kbd, 0, 0);
3366 }
3367 log::info!("[PopupWheel] begin on popup surface {:p}", surface);
3368 self.popup_wheel = Some(PopupWheel { surface, accum: [0.0, 0.0], ctrl_down: false, repeat });
3369 self.hold_popup_ctrl(true);
3370 }
3371 // The gesture belongs to the popup it started on.
3372 let focused = ffi::river_wlr_seat_get_pointer_focused_surface((*self.seat).wlr_seat);
3373 if matches!(&self.popup_wheel, Some(p) if focused != p.surface) {
3374 self.end_popup_wheel("left-popup");
3375 return false;
3376 }
3377 let vertical = (*event).orientation == ffi::wl_pointer_axis_WL_POINTER_AXIS_VERTICAL_SCROLL;
3378 let axis = if vertical { 1usize } else { 0usize };
3379 let notches = {
3380 let Some(p) = self.popup_wheel.as_mut() else { return false };
3381 p.accum[axis] += delta;
3382 let whole = (p.accum[axis] / POPUP_WHEEL_NOTCH_PX).trunc();
3383 p.accum[axis] -= whole * POPUP_WHEEL_NOTCH_PX;
3384 whole
3385 };
3386 if notches != 0.0 {
3387 let seat = &mut *self.seat;
3388 let time = crate::util::msec_timestamp();
3389 let step = POPUP_WHEEL_NOTCH_PX * notches.signum();
3390 let discrete = 120 * notches.signum() as i32;
3391 for _ in 0..notches.abs() as i32 {
3392 ffi::wlr_seat_pointer_notify_axis(
3393 seat.wlr_seat,
3394 time,
3395 (*event).orientation,
3396 step,
3397 discrete,
3398 ffi::wl_pointer_axis_source_WL_POINTER_AXIS_SOURCE_WHEEL,
3399 (*event).relative_direction,
3400 );
3401 ffi::wlr_seat_pointer_notify_frame(seat.wlr_seat);
3402 }
3403 }
3404 self.arm_popup_wheel_timer();
3405 true
3406 }
3407 }
3408
3409 unsafe extern "C" fn handle_view_drag_timeout(data: *mut std::ffi::c_void) -> std::os::raw::c_int {
3410 let cursor = &mut *(data as *mut Cursor);
3411 cursor.end_view_drag("idle");
3412 cursor.end_popup_wheel("idle");
3413 cursor.end_hscroll_shift("idle");
3414 0
3415 }
3416
3417 /// A horizontal trackpad scroll over an app whose widgets cannot use one,
3418 /// turned into the Shift + vertical scroll they can.
3419 ///
3420 /// Why this exists: Houdini's native panes -- the geometry spreadsheet
3421 /// first of all -- read a wheel event's magnitude and ignore its axis: a
3422 /// horizontal wheel scrolls the rows, and the documented way to scroll the
3423 /// columns is to hold Shift while scrolling. Measured live: a horizontal
3424 /// two-finger swipe reached Houdini as a proper horizontal QWheelEvent
3425 /// (angleDelta x only) and moved the rows; the same swipe with Shift held
3426 /// moved the columns. So over such a window the compositor holds Shift for
3427 /// the gesture and re-emits each horizontal finger delta as a vertical one,
3428 /// same sign, same source. A vertical delta arriving mid-gesture (a swipe
3429 /// drifting off axis) is dropped rather than sent sideways; the gesture
3430 /// ends on the lift, on real pointer motion, a button, a key, or silence.
3431 ///
3432 /// The user already holding Shift or Ctrl passes through untouched: Shift
3433 /// means they are doing it by hand, Ctrl is the app's own wheel modifier.
3434 pub struct HScrollShift {
3435 /// The window's surface the gesture started on; pointer focus moving
3436 /// off it ends the gesture, so the held Shift never reaches another.
3437 pub surface: *mut ffi::wlr_surface,
3438 pub shift_down: bool,
3439 /// Keyboard repeat before the gesture, restored at its end (see
3440 /// `PopupWheel::repeat`).
3441 pub repeat: Option<(i32, i32)>,
3442 }
3443
3444 const KEY_LEFTSHIFT: u32 = 42;
3445 /// Finger silence that ends the gesture when the lift never came; a short
3446 /// one is safe here because nothing is held that a re-press would break
3447 /// (unlike `ViewDrag`'s Space).
3448 const HSCROLL_SHIFT_IDLE_MS: i32 = 300;
3449
3450 impl Cursor {
3451 /// The surface under the pointer, if it belongs to a window of an app
3452 /// in `touchpad_hscroll_shift_apps`.
3453 unsafe fn hscroll_shift_target(&mut self) -> Option<*mut ffi::wlr_surface> {
3454 let server = (*self.seat).server;
3455 let wm = &(*server).wm;
3456 if wm.touchpad_hscroll_shift_apps.is_empty() {
3457 return None;
3458 }
3459 let result = (*server).scene.at(self.x(), self.y())?;
3460 let SceneNodeDataVal::Window(window) = result.data else { return None };
3461 if window.is_null() || result.surface.is_null() {
3462 return None;
3463 }
3464 let app_id = (*window).get_app_id_string().unwrap_or_default();
3465 if !wm.touchpad_hscroll_shift_apps.iter().any(|p| crate::window_manager::app_id_matches(p, &app_id)) {
3466 return None;
3467 }
3468 Some(result.surface)
3469 }
3470
3471 unsafe fn hold_hscroll_shift(&mut self, down: bool) {
3472 match self.hscroll_shift.as_mut() {
3473 Some(h) if h.shift_down != down => h.shift_down = down,
3474 _ => return,
3475 }
3476 self.hold_synthetic_modifier(KEY_LEFTSHIFT, b"Shift\0", down);
3477 }
3478
3479 /// `reason` names what ended it, as `end_view_drag`'s does.
3480 pub unsafe fn end_hscroll_shift(&mut self, reason: &str) {
3481 if self.hscroll_shift.is_none() {
3482 return;
3483 }
3484 self.hold_hscroll_shift(false);
3485 let Some(h) = self.hscroll_shift.take() else { return };
3486 log::info!("[HScrollShift] end reason={}", reason);
3487 if !self.view_drag_timer.is_null() {
3488 ffi::wl_event_source_timer_update(self.view_drag_timer, 0);
3489 }
3490 if let Some((rate, delay)) = h.repeat {
3491 let kbd = ffi::river_wlr_seat_get_keyboard((*self.seat).wlr_seat);
3492 if !kbd.is_null() {
3493 ffi::wlr_keyboard_set_repeat_info(kbd, rate, delay);
3494 }
3495 }
3496 }
3497
3498 /// A finger-source axis event over such a window. Returns true when
3499 /// consumed (re-emitted as Shift + vertical, or dropped).
3500 pub unsafe fn hscroll_shift_axis(&mut self, event: *const ffi::wlr_pointer_axis_event, delta: f64, delta_discrete: i32, modifiers: u32) -> bool {
3501 const SHIFT: u32 = 0x1;
3502 const CTRL: u32 = 0x4;
3503 let vertical = (*event).orientation == ffi::wl_pointer_axis_WL_POINTER_AXIS_VERTICAL_SCROLL;
3504 if delta == 0.0 {
3505 // The fingers lifted. Both axes lift; the first one ends it and
3506 // the second finds nothing to do -- and neither must reach the
3507 // client as a stray horizontal event.
3508 if self.hscroll_shift.is_some() {
3509 self.end_hscroll_shift("lift");
3510 return true;
3511 }
3512 return false;
3513 }
3514 if modifiers & (SHIFT | CTRL) != 0 {
3515 if self.hscroll_shift.is_some() {
3516 self.end_hscroll_shift("modifier");
3517 }
3518 return false;
3519 }
3520 if self.hscroll_shift.is_none() {
3521 if vertical {
3522 return false;
3523 }
3524 let Some(surface) = self.hscroll_shift_target() else { return false };
3525 let seat = &mut *self.seat;
3526 seat.ensure_synthetic_keyboard();
3527 let mut repeat = None;
3528 let kbd = ffi::river_wlr_seat_get_keyboard(seat.wlr_seat);
3529 if !kbd.is_null() {
3530 repeat = Some(((*kbd).repeat_info.rate, (*kbd).repeat_info.delay));
3531 ffi::wlr_keyboard_set_repeat_info(kbd, 0, 0);
3532 }
3533 log::info!("[HScrollShift] begin on surface {:p}", surface);
3534 self.hscroll_shift = Some(HScrollShift { surface, shift_down: false, repeat });
3535 self.hold_hscroll_shift(true);
3536 }
3537 let focused = ffi::river_wlr_seat_get_pointer_focused_surface((*self.seat).wlr_seat);
3538 if matches!(&self.hscroll_shift, Some(h) if focused != h.surface) {
3539 self.end_hscroll_shift("left-window");
3540 return false;
3541 }
3542 if vertical {
3543 // Off-axis drift mid-gesture: under the held Shift it would
3544 // scroll sideways too. Swallow it.
3545 self.arm_hscroll_shift_timer();
3546 return true;
3547 }
3548 let seat = &mut *self.seat;
3549 ffi::wlr_seat_pointer_notify_axis(
3550 seat.wlr_seat,
3551 (*event).time_msec,
3552 ffi::wl_pointer_axis_WL_POINTER_AXIS_VERTICAL_SCROLL,
3553 delta,
3554 delta_discrete,
3555 (*event).source,
3556 (*event).relative_direction,
3557 );
3558 self.arm_hscroll_shift_timer();
3559 true
3560 }
3561
3562 unsafe fn arm_hscroll_shift_timer(&mut self) {
3563 if !self.view_drag_timer.is_null() {
3564 ffi::wl_event_source_timer_update(self.view_drag_timer, HSCROLL_SHIFT_IDLE_MS);
3565 }
3566 }
3567 }
3568
3569 unsafe extern "C" fn handle_frame(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
3570 let cursor = &mut *crate::container_of!(listener, Cursor, frame_listener);
3571
3572 let seat = &mut *cursor.seat;
3573 ffi::wlr_seat_pointer_notify_frame(seat.wlr_seat);
3574 }
3575
3576 /// Animated-xcursor tick. `data` is the `Cursor`, which lives inline in a
3577 /// `Box::into_raw`'d `Seat` and so never moves. Re-arms itself from
3578 /// `advance_xcursor_frame`; a stopped animation leaves the timer disarmed and
3579 /// this is not called again until something arms it.
3580 unsafe extern "C" fn handle_xcursor_anim(data: *mut std::ffi::c_void) -> std::os::raw::c_int {
3581 let cursor = &mut *(data as *mut Cursor);
3582 cursor.advance_xcursor_frame();
3583 0
3584 }
3585
3586 unsafe extern "C" fn handle_tablet_tool_axis(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
3587 let _cursor = &mut *crate::container_of!(listener, Cursor, tablet_tool_axis_listener);
3588 let event = data as *mut ffi::wlr_tablet_tool_axis_event;
3589
3590 let wlr_tablet = (*event).tablet;
3591 let wlr_device = &mut (*wlr_tablet).base as *mut ffi::wlr_input_device;
3592 let device = ffi::river_wlr_input_device_get_data(wlr_device) as *mut crate::input_device::InputDevice;
3593 if device.is_null() {
3594 return;
3595 }
3596 let seat = (*device).seat;
3597 (*seat).handle_activity();
3598
3599 let tablet = (*device).destroy_data as *mut crate::tablet::Tablet;
3600 if tablet.is_null() {
3601 return;
3602 }
3603
3604 if let Ok(tool) = crate::tablet_tool::TabletTool::get((*seat).wlr_seat, (*event).tool) {
3605 (*tool).axis(tablet, event);
3606 }
3607 }
3608
3609 unsafe extern "C" fn handle_tablet_tool_proximity(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
3610 let cursor = &mut *crate::container_of!(listener, Cursor, tablet_tool_proximity_listener);
3611 let event = data as *mut ffi::wlr_tablet_tool_proximity_event;
3612 // A stylus sets its own image (`TabletTool::proximity`); the touch
3613 // hide must not then block the compositor's next one.
3614 cursor.hidden_by_touch = false;
3615
3616 let wlr_tablet = (*event).tablet;
3617 let wlr_device = &mut (*wlr_tablet).base as *mut ffi::wlr_input_device;
3618 let device = ffi::river_wlr_input_device_get_data(wlr_device) as *mut crate::input_device::InputDevice;
3619 if device.is_null() {
3620 return;
3621 }
3622 let seat = (*device).seat;
3623 (*seat).handle_activity();
3624
3625 let tablet = (*device).destroy_data as *mut crate::tablet::Tablet;
3626 if tablet.is_null() {
3627 return;
3628 }
3629
3630 if let Ok(tool) = crate::tablet_tool::TabletTool::get((*seat).wlr_seat, (*event).tool) {
3631 (*tool).proximity(tablet, event);
3632 }
3633 }
3634
3635 unsafe extern "C" fn handle_tablet_tool_tip(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
3636 let _cursor = &mut *crate::container_of!(listener, Cursor, tablet_tool_tip_listener);
3637 let event = data as *mut ffi::wlr_tablet_tool_tip_event;
3638
3639 let wlr_tablet = (*event).tablet;
3640 let wlr_device = &mut (*wlr_tablet).base as *mut ffi::wlr_input_device;
3641 let device = ffi::river_wlr_input_device_get_data(wlr_device) as *mut crate::input_device::InputDevice;
3642 if device.is_null() {
3643 return;
3644 }
3645 let seat = (*device).seat;
3646 (*seat).handle_activity();
3647
3648 let tablet = (*device).destroy_data as *mut crate::tablet::Tablet;
3649 if tablet.is_null() {
3650 return;
3651 }
3652
3653 if let Ok(tool) = crate::tablet_tool::TabletTool::get((*seat).wlr_seat, (*event).tool) {
3654 (*tool).tip(tablet, event);
3655 }
3656 }
3657
3658 unsafe extern "C" fn handle_tablet_tool_button(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
3659 let _cursor = &mut *crate::container_of!(listener, Cursor, tablet_tool_button_listener);
3660 let event = data as *mut ffi::wlr_tablet_tool_button_event;
3661
3662 let wlr_tablet = (*event).tablet;
3663 let wlr_device = &mut (*wlr_tablet).base as *mut ffi::wlr_input_device;
3664 let device = ffi::river_wlr_input_device_get_data(wlr_device) as *mut crate::input_device::InputDevice;
3665 if device.is_null() {
3666 return;
3667 }
3668 let seat = (*device).seat;
3669 (*seat).handle_activity();
3670
3671 let tablet = (*device).destroy_data as *mut crate::tablet::Tablet;
3672 if tablet.is_null() {
3673 return;
3674 }
3675
3676 if let Ok(tool) = crate::tablet_tool::TabletTool::get((*seat).wlr_seat, (*event).tool) {
3677 (*tool).button(tablet, event);
3678 }
3679 }
3680
3681 /// A directional swipe bind fires once the accumulated travel (libinput
3682 /// units) passes `window_manager { swipe_threshold }`
3683 /// (`WindowManager::swipe_threshold`, default 70). Until then the camera
3684 /// *peeks*: it pans toward the swipe direction in proportion to the
3685 /// travel, up to `window_manager { swipe_peek }` screen px
3686 /// (`WindowManager::swipe_peek_px`, default 60) at the threshold, and
3687 /// eases back if the fingers lift short of it — so a hesitant
3688 /// three-finger swipe shows where it would go without going. Firing does
3689 /// not end the swipe: the travel restarts from zero, and each further
3690 /// `swipe_repeat_threshold` of travel (default four times the first) steps
3691 /// again — or back, on a reversal — until the fingers lift.
3692
3693 /// Does firing `action` carry the view in the swipe's direction? Only
3694 /// such binds peek the camera beforehand: an overview toggle or a spawn
3695 /// on a swipe has no direction the desktop could lean toward.
3696 fn action_navigates(action: crate::config::Action) -> bool {
3697 use crate::config::Action::*;
3698 matches!(action, FocusLeft | FocusRight | FocusUp | FocusDown | PanLeft | PanRight | PanUp | PanDown)
3699 }
3700
3701 /// The peek the accumulated travel `d` along one axis calls for, in
3702 /// virtual units: proportional and clamped at `threshold`, `peek_px` on
3703 /// screen there, and only toward a direction that has a navigating bind
3704 /// (`neg` / `pos`) — a swipe with nothing bound its way leaves the
3705 /// desktop still.
3706 fn swipe_peek_for(d: f64, neg: bool, pos: bool, threshold: f64, peek_px: f64, zoom: f64) -> f64 {
3707 if (d < 0.0 && neg) || (d > 0.0 && pos) {
3708 (d / threshold).clamp(-1.0, 1.0) * peek_px / zoom.max(1e-6)
3709 } else {
3710 0.0
3711 }
3712 }
3713
3714 fn is_directional_focus(action: crate::config::Action) -> bool {
3715 use crate::config::Action;
3716 matches!(action, Action::FocusLeft | Action::FocusRight | Action::FocusUp | Action::FocusDown)
3717 }
3718
3719 /// The sense a swipe along `finger_dir` ("left", "right", "up", "down")
3720 /// gives its axis when aiming focus: +1 when its bind focuses the way the
3721 /// fingers moved, -1 when it focuses the opposite way, `None` when the
3722 /// first bind that way is not a focus on that axis (so the axis does not
3723 /// aim). First match wins, as in the fire itself.
3724 fn focus_axis_sense(binds: &[crate::config::GestureBind], fingers: u32, mods: u32, finger_dir: &str) -> Option<f64> {
3725 use crate::config::Action;
3726 let gb = binds
3727 .iter()
3728 .find(|gb| gb.gesture_type == "swipe" && gb.fingers == fingers && gb.mods == mods && gb.direction == finger_dir)?;
3729 let focus_dir = match gb.action {
3730 Action::FocusLeft => "left",
3731 Action::FocusRight => "right",
3732 Action::FocusUp => "up",
3733 Action::FocusDown => "down",
3734 _ => return None,
3735 };
3736 let axis = |d: &str| if d == "left" || d == "right" { 0 } else { 1 };
3737 if axis(focus_dir) != axis(finger_dir) {
3738 return None;
3739 }
3740 Some(if focus_dir == finger_dir { 1.0 } else { -1.0 })
3741 }
3742
3743 /// The direction a focus swipe aims in, from this step's `travel`: each
3744 /// axis's travel times its sense (`focus_axis_sense`), zero on an axis
3745 /// with no focus bind the way the fingers went. `None` when neither axis
3746 /// aims.
3747 fn swipe_focus_vector(binds: &[crate::config::GestureBind], fingers: u32, mods: u32, travel: (f64, f64)) -> Option<(f64, f64)> {
3748 let (dx, dy) = travel;
3749 let sx = if dx != 0.0 { focus_axis_sense(binds, fingers, mods, if dx < 0.0 { "left" } else { "right" }) } else { None };
3750 let sy = if dy != 0.0 { focus_axis_sense(binds, fingers, mods, if dy < 0.0 { "up" } else { "down" }) } else { None };
3751 let v = (sx.map_or(0.0, |s| s * dx), sy.map_or(0.0, |s| s * dy));
3752 (v != (0.0, 0.0)).then_some(v)
3753 }
3754
3755 /// Slope (minor over major travel) below which a swipe counts as straight:
3756 /// tan 15°. Within it the lean stays on the dominant axis, since a hand
3757 /// swiping left drifts a little up or down, and leaning with that drift
3758 /// is a wobble, not a direction.
3759 const SWIPE_LEAN_STRAIGHT_SLOPE: f64 = 0.268;
3760
3761 /// The lean `[x, y]` (virtual units) the swipe's travel so far calls for.
3762 /// Its size is set by the dominant axis (`swipe_peek_for`: proportional,
3763 /// `peek_px` on screen at `threshold`), and its direction follows the
3764 /// fingers: the minor axis leans in proportion to the swipe's slope, so a
3765 /// 45° swipe leans 45°. The slope is measured past the straight band and
3766 /// rescaled, `SWIPE_LEAN_STRAIGHT_SLOPE` mapping to 0 and a diagonal to 1,
3767 /// so the lean turns smoothly off the axis rather than jumping at 15°.
3768 /// Each axis only leans toward a direction with a navigating bind
3769 /// (`navigates`: left, right, up, down), and nothing leans at all when the
3770 /// dominant direction has none.
3771 fn swipe_lean(dx: f64, dy: f64, navigates: [bool; 4], threshold: f64, peek_px: f64, zoom: f64) -> [f64; 2] {
3772 let horizontal = dx.abs() >= dy.abs();
3773 let (major, minor) = if horizontal { (dx, dy) } else { (dy, dx) };
3774 let (major_nav, minor_nav) = if horizontal {
3775 ((navigates[0], navigates[1]), (navigates[2], navigates[3]))
3776 } else {
3777 ((navigates[2], navigates[3]), (navigates[0], navigates[1]))
3778 };
3779 let lean_major = swipe_peek_for(major, major_nav.0, major_nav.1, threshold, peek_px, zoom);
3780 let minor_allowed = (minor < 0.0 && minor_nav.0) || (minor > 0.0 && minor_nav.1);
3781 let lean_minor = if lean_major != 0.0 && minor_allowed {
3782 let slope = minor.abs() / major.abs();
3783 let turn = ((slope - SWIPE_LEAN_STRAIGHT_SLOPE) / (1.0 - SWIPE_LEAN_STRAIGHT_SLOPE)).clamp(0.0, 1.0);
3784 lean_major.abs() * turn * minor.signum()
3785 } else {
3786 0.0
3787 };
3788 if horizontal { [lean_major, lean_minor] } else { [lean_minor, lean_major] }
3789 }
3790
3791 unsafe extern "C" fn handle_swipe_begin(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
3792 let cursor = &mut *crate::container_of!(listener, Cursor, swipe_begin_listener);
3793 let event = data as *mut ffi::wlr_pointer_swipe_begin_event;
3794
3795 let seat = &mut *cursor.seat;
3796 let wm = &(*seat.server).wm;
3797 let swipe_enabled = wm.input_config.touchpad.as_ref().and_then(|t| t.gestures.as_ref()).and_then(|g| g.swipe).unwrap_or(true)
3798 || cursor.gesture_from_touch;
3799 if !swipe_enabled {
3800 return;
3801 }
3802 seat.handle_activity();
3803 // A swipe or pinch is deliberate input, like a click or a key: it ends
3804 // the session-restore settling phase (`WindowManager::startup_input_seen`),
3805 // so the first focus of a restored window pans to it. Until 2026-09-26
3806 // only buttons and keys counted, and after a login navigated purely by
3807 // three-finger swipes, each restored window's first focus left it
3808 // wherever it sat, often half off screen. Holds do not count: one begins
3809 // whenever fingers merely rest on the pad.
3810 (*seat.server).wm.startup_input_seen = true;
3811
3812 cursor.gesture_dx = 0.0;
3813 cursor.gesture_dy = 0.0;
3814 cursor.gesture_triggered = false;
3815 cursor.swipe_spent = false;
3816 cursor.swipe_dead_end = None;
3817 cursor.swipe_peek = [0.0, 0.0];
3818
3819 log::info!("handle_swipe_begin: fingers={}", (*event).fingers);
3820
3821 let server = seat.server;
3822 let pointer_gestures = (*server).input_manager.pointer_gestures;
3823 if !pointer_gestures.is_null() && !cursor.gesture_from_touch {
3824 ffi::wlr_pointer_gestures_v1_send_swipe_begin(
3825 pointer_gestures,
3826 seat.wlr_seat,
3827 (*event).time_msec,
3828 (*event).fingers,
3829 );
3830 }
3831 }
3832
3833 unsafe extern "C" fn handle_swipe_update(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
3834 let cursor = &mut *crate::container_of!(listener, Cursor, swipe_update_listener);
3835 let event = data as *mut ffi::wlr_pointer_swipe_update_event;
3836
3837 let seat = &mut *cursor.seat;
3838 let wm = &(*seat.server).wm;
3839 let swipe_enabled = wm.input_config.touchpad.as_ref().and_then(|t| t.gestures.as_ref()).and_then(|g| g.swipe).unwrap_or(true)
3840 || cursor.gesture_from_touch;
3841 if !swipe_enabled {
3842 return;
3843 }
3844 seat.handle_activity();
3845
3846 // A step does not end the gesture: the travel restarts from zero at
3847 // the fire (below), so the fingers can keep going and step focus
3848 // again — or turn round and step back — without lifting.
3849 // `gesture_triggered` only records that clients were sent their
3850 // (cancelled) end, so they hear nothing more of this swipe. Any other
3851 // bind (the overview toggle, a spawn) fires once per gesture: after
3852 // it the swipe is spent and the rest of it is ignored.
3853 if cursor.swipe_spent {
3854 return;
3855 }
3856
3857 cursor.gesture_dx += (*event).dx;
3858 cursor.gesture_dy += (*event).dy;
3859
3860 log::debug!(
3861 "handle_swipe_update: fingers={}, dx={}, dy={}, accumulated_dx={}, accumulated_dy={}",
3862 (*event).fingers,
3863 (*event).dx,
3864 (*event).dy,
3865 cursor.gesture_dx,
3866 cursor.gesture_dy
3867 );
3868
3869 let wlr_keyboard = ffi::river_wlr_seat_get_keyboard(seat.wlr_seat);
3870 let modifiers = if !wlr_keyboard.is_null() {
3871 ffi::wlr_keyboard_get_modifiers(wlr_keyboard) & 0x4d
3872 } else {
3873 0
3874 };
3875
3876 // The first step of a swipe comes at `swipe_threshold`; every further
3877 // one at `swipe_repeat_threshold` (default four times that), so a swipe
3878 // that has just switched focus meets resistance before switching
3879 // again rather than running on through the next window. The lean
3880 // scales with the threshold in force, so it stays a preview of how
3881 // far the fingers are from the next step.
3882 let threshold = if cursor.gesture_triggered {
3883 (*seat.server).wm.swipe_repeat_threshold
3884 } else {
3885 (*seat.server).wm.swipe_threshold
3886 };
3887 let mut matched_action = crate::config::Action::None;
3888 let mut matched_command = None;
3889 // Which directions this finger count + chord could still fire a
3890 // navigating bind in — the directions the camera may peek toward.
3891 let mut navigates = [false; 4]; // left, right, up, down
3892
3893 for gb in &(*seat.server).wm.gesture_binds {
3894 if gb.gesture_type == "swipe" && gb.fingers == (*event).fingers && gb.mods == modifiers {
3895 let (matched, slot) = match gb.direction.as_str() {
3896 "left" => (cursor.gesture_dx < -threshold, Some(0)),
3897 "right" => (cursor.gesture_dx > threshold, Some(1)),
3898 "up" => (cursor.gesture_dy < -threshold, Some(2)),
3899 "down" => (cursor.gesture_dy > threshold, Some(3)),
3900 _ => (false, None),
3901 };
3902 // First match wins in this table. The scan goes on past it
3903 // because a focus step that turns out to have nowhere to go
3904 // (below) leans instead, and the lean needs every direction.
3905 if matched && matched_action == crate::config::Action::None {
3906 matched_action = gb.action;
3907 matched_command = gb.command.clone();
3908 }
3909 if let Some(i) = slot {
3910 if !navigates[i] && action_navigates(gb.action) {
3911 navigates[i] = true;
3912 }
3913 }
3914 }
3915 }
3916
3917 // A focus step with no window that way does not fire. Firing it moved
3918 // nothing but the camera: the step took the lean, restarted the
3919 // travel, and the camera eased back while the fingers were still
3920 // going out, then leaned out again toward the next threshold and
3921 // snapped back at it — a sawtooth for as long as the swipe ran on.
3922 // Instead the lean holds at its limit, as at a wall, and the lift eases
3923 // it out (`handle_swipe_end`). The travel is scaled back onto the
3924 // threshold, keeping its direction, so turning the fingers round
3925 // unwinds the lean at once rather than first spending the overshoot.
3926 // The answer is kept for the rest of the swipe, so the policy is asked
3927 // once per dead end, not once per event.
3928 if is_directional_focus(matched_action) {
3929 let travel = (cursor.gesture_dx, cursor.gesture_dy);
3930 let dead = cursor.swipe_dead_end == Some(matched_action) || {
3931 let v = swipe_focus_vector(&(*seat.server).wm.gesture_binds, (*event).fingers, modifiers, travel);
3932 !(*seat.server).wm.focus_toward_lands(v, &matched_action)
3933 };
3934 if dead {
3935 if cursor.swipe_dead_end != Some(matched_action) {
3936 log::info!("Swipe gesture {:?}: no window that way, holding the lean", matched_action);
3937 }
3938 cursor.swipe_dead_end = Some(matched_action);
3939 let over = travel.0.abs().max(travel.1.abs()) / threshold;
3940 if over > 1.0 {
3941 cursor.gesture_dx /= over;
3942 cursor.gesture_dy /= over;
3943 }
3944 matched_action = crate::config::Action::None;
3945 matched_command = None;
3946 }
3947 }
3948
3949 if matched_action != crate::config::Action::None {
3950 log::info!("Swipe gesture matched action: {:?}", matched_action);
3951 cursor.swipe_dead_end = None;
3952 // This step's travel, before it restarts below: a focus swipe
3953 // aims along it (`swipe_focus_vector`).
3954 let travel = (cursor.gesture_dx, cursor.gesture_dy);
3955 let first_fire = !cursor.gesture_triggered;
3956 if !action_navigates(matched_action) {
3957 cursor.swipe_spent = true;
3958 }
3959 cursor.gesture_triggered = true;
3960 // The next step needs a full threshold of fresh travel from here,
3961 // on both axes: a long swipe steps once per threshold, and a
3962 // reversal after a step goes back rather than first having to
3963 // undo the travel that got here.
3964 cursor.gesture_dx = 0.0;
3965 cursor.gesture_dy = 0.0;
3966
3967 // The lean is where the camera IS now: the action runs against
3968 // it, and whatever it asks for is applied from here. The one rule
3969 // is that the camera never reverses at the fire — see below.
3970 let lean = std::mem::replace(&mut cursor.swipe_peek, [0.0, 0.0]);
3971 if lean != [0.0, 0.0] {
3972 let wm = &mut (*seat.server).wm;
3973 wm.desk_pan_x += wm.pan_pending[0];
3974 wm.desk_pan_y += wm.pan_pending[1];
3975 wm.pan_pending = [0.0, 0.0];
3976 }
3977
3978 if matched_action == crate::config::Action::Overview && (*seat.server).wm.mode == crate::window_manager::WindowManagerMode::Overview {
3979 let lx = cursor.x();
3980 let ly = cursor.y();
3981 let mut hovered_win: *mut crate::window::Window = std::ptr::null_mut();
3982 if let Some(result) = (*seat.server).scene.at(lx, ly) {
3983 if let SceneNodeDataVal::Window(window) = result.data {
3984 hovered_win = window;
3985 }
3986 }
3987 if !hovered_win.is_null() && !(*hovered_win).is_status_bar() && !(*hovered_win).is_wallpaper() {
3988 seat.focus(Focus::Window(hovered_win));
3989 if !seat.object.is_null() && !(*hovered_win).object.is_null() {
3990 ffi::wl_resource_post_event(seat.object, 4, (*hovered_win).object);
3991 }
3992 }
3993 }
3994
3995 // The overview toggle lands on the hovered window, else on the
3996 // FOCUSED one — never on the empty desktop under the pointer.
3997 let matched_action = if matched_action == crate::config::Action::Overview {
3998 (*seat.server).wm.overview_action_for_gesture()
3999 } else {
4000 matched_action
4001 };
4002 // A focus swipe aims where the fingers went, not at one of four
4003 // directions: the bind table gives each axis its sense (a
4004 // `focus_left` on `swipe3_left` follows the fingers, one on
4005 // `swipe3_right` mirrors them), and the window manager picks the
4006 // nearest window center along the result.
4007 let focus_vector = swipe_focus_vector(&(*seat.server).wm.gesture_binds, (*event).fingers, modifiers, travel);
4008 match focus_vector {
4009 Some(v) if is_directional_focus(matched_action) => (*seat.server).wm.focus_toward(v, &matched_action),
4010 _ => (*seat.server).wm.execute_action(&matched_action, matched_command.as_deref()),
4011 }
4012
4013 // The action ran against the leaned camera. It set a pan target
4014 // only if the window it focused crosses a screen edge from there,
4015 // and only as far as bringing it in needs (`pan_into_view`), so a
4016 // window the lean left fully in view asks for nothing and the
4017 // camera stops right here rather than springing back. A target
4018 // that heads back against the lean is kept: it means the lean
4019 // pushed the window's near edge off screen, or leaned away from
4020 // the side the window sits on, and dropping it (as this did until
4021 // 2026-09-24, to keep the camera from ever reversing) left the
4022 // newly focused window clipped.
4023
4024 if first_fire {
4025 let pointer_gestures = (*seat.server).input_manager.pointer_gestures;
4026 if !pointer_gestures.is_null() && !cursor.gesture_from_touch {
4027 ffi::wlr_pointer_gestures_v1_send_swipe_end(
4028 pointer_gestures,
4029 seat.wlr_seat,
4030 (*event).time_msec,
4031 true, // cancelled: true
4032 );
4033 }
4034 }
4035 return;
4036 }
4037
4038 // Short of the threshold: lean the camera toward the bind the swipe
4039 // is heading for, 1:1 with the fingers like a two-finger pan (queued
4040 // for the frame, no easing), recomputed from the total travel so a
4041 // reversal leans back through zero. In the fingers' direction
4042 // (`swipe_lean`): a diagonal swipe leans diagonally, while a nearly
4043 // straight one stays on its dominant axis, because a hand swiping
4044 // left drifts a little up or down and leaning with that drift was a
4045 // wobble on top of the real move.
4046 {
4047 let wm = &mut (*seat.server).wm;
4048 // After a step the lean is slower as well as longer to fill: it
4049 // reaches `swipe_repeat_peek` (default half of `swipe_peek`) at the
4050 // repeat threshold, so a swipe that has just switched focus does
4051 // not tug the camera toward the next window as eagerly. With
4052 // animations off (`cce_core::motion`) there is no lean at all: the
4053 // camera stays put until the bind fires, then jumps (the step's
4054 // ease is instant then, `advance_camera_animation`). A lean
4055 // already showing when they were turned off goes back the same way.
4056 let peek_px = if !cce_core::motion::enabled() {
4057 0.0
4058 } else if cursor.gesture_triggered {
4059 wm.swipe_repeat_peek_px
4060 } else {
4061 wm.swipe_peek_px
4062 };
4063 let (dx, dy) = (cursor.gesture_dx, cursor.gesture_dy);
4064 let want = swipe_lean(dx, dy, navigates, threshold, peek_px, wm.desk_zoom);
4065 let delta = [want[0] - cursor.swipe_peek[0], want[1] - cursor.swipe_peek[1]];
4066 if delta != [0.0, 0.0] {
4067 if cursor.gesture_triggered {
4068 // After a step the camera may still be easing the window
4069 // it focused into view. The lean rides on top of that ease
4070 // rather than freezing it short: the ease's target moves
4071 // with the fingers, and the lift (`handle_swipe_end`)
4072 // moves it back.
4073 if let Some(t) = wm.target_desk_pan_x.as_mut() { *t += delta[0]; }
4074 if let Some(t) = wm.target_desk_pan_y.as_mut() { *t += delta[1]; }
4075 } else {
4076 wm.stop_panning_animation();
4077 }
4078 wm.queue_pan(delta[0], delta[1]);
4079 cursor.swipe_peek = want;
4080 }
4081 }
4082
4083 if cursor.gesture_triggered {
4084 return;
4085 }
4086 let server = seat.server;
4087 let pointer_gestures = (*server).input_manager.pointer_gestures;
4088 if !pointer_gestures.is_null() && !cursor.gesture_from_touch {
4089 ffi::wlr_pointer_gestures_v1_send_swipe_update(
4090 pointer_gestures,
4091 seat.wlr_seat,
4092 (*event).time_msec,
4093 (*event).dx,
4094 (*event).dy,
4095 );
4096 }
4097 }
4098
4099 unsafe extern "C" fn handle_swipe_end(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
4100 let cursor = &mut *crate::container_of!(listener, Cursor, swipe_end_listener);
4101 let event = data as *mut ffi::wlr_pointer_swipe_end_event;
4102
4103 let seat = &mut *cursor.seat;
4104 let wm = &(*seat.server).wm;
4105 let swipe_enabled = wm.input_config.touchpad.as_ref().and_then(|t| t.gestures.as_ref()).and_then(|g| g.swipe).unwrap_or(true)
4106 || cursor.gesture_from_touch;
4107 if !swipe_enabled {
4108 return;
4109 }
4110 seat.handle_activity();
4111
4112 log::info!("handle_swipe_end: cancelled={}", (*event).cancelled);
4113
4114 let fired = std::mem::replace(&mut cursor.gesture_triggered, false);
4115
4116 // Lifted short of a threshold — the first, or the next one after a
4117 // step — ease the lean back out: to where the swipe found the camera,
4118 // or, after a step, to where the step's own ease was heading before
4119 // the lean moved its target along (`handle_swipe_update`). The steps
4120 // themselves stay: the camera never returns to where the swipe began.
4121 let peek = std::mem::replace(&mut cursor.swipe_peek, [0.0, 0.0]);
4122 if peek != [0.0, 0.0] {
4123 let wm = &mut (*seat.server).wm;
4124 wm.desk_pan_x += wm.pan_pending[0];
4125 wm.desk_pan_y += wm.pan_pending[1];
4126 wm.pan_pending = [0.0, 0.0];
4127 wm.target_desk_pan_x = Some(wm.target_desk_pan_x.unwrap_or(wm.desk_pan_x) - peek[0]);
4128 wm.target_desk_pan_y = Some(wm.target_desk_pan_y.unwrap_or(wm.desk_pan_y) - peek[1]);
4129 wm.start_panning_animation();
4130 }
4131
4132 // Clients heard a cancelled end at the first step; the lift after one
4133 // is not theirs.
4134 if fired {
4135 return;
4136 }
4137
4138 let server = seat.server;
4139 let pointer_gestures = (*server).input_manager.pointer_gestures;
4140 if !pointer_gestures.is_null() && !cursor.gesture_from_touch {
4141 ffi::wlr_pointer_gestures_v1_send_swipe_end(
4142 pointer_gestures,
4143 seat.wlr_seat,
4144 (*event).time_msec,
4145 (*event).cancelled,
4146 );
4147 }
4148 }
4149
4150 /// The modifiers a gesture bind is matched under: the keyboard's, less
4151 /// Caps and Num Lock (`& 0x4d`), as for every gesture.
4152 pub(crate) unsafe fn gesture_mods(seat: &Seat) -> u32 {
4153 let wlr_keyboard = ffi::river_wlr_seat_get_keyboard(seat.wlr_seat);
4154 if wlr_keyboard.is_null() {
4155 0
4156 } else {
4157 ffi::wlr_keyboard_get_modifiers(wlr_keyboard) & 0x4d
4158 }
4159 }
4160
4161 /// The first gesture bind of `kind` ("swipe", "pinch", "edge") for this
4162 /// finger count and modifiers whose direction `hit` accepts. First match
4163 /// wins, as everywhere in the bind table.
4164 pub(crate) fn gesture_bind(
4165 wm: &crate::window_manager::WindowManager,
4166 kind: &str,
4167 fingers: u32,
4168 mods: u32,
4169 hit: impl Fn(&str) -> bool,
4170 ) -> Option<(crate::config::Action, Option<String>)> {
4171 wm.gesture_binds
4172 .iter()
4173 .find(|gb| gb.gesture_type == kind && gb.fingers == fingers && gb.mods == mods && hit(&gb.direction))
4174 .map(|gb| (gb.action, gb.command.clone()))
4175 }
4176
4177 /// Whether a pinch at `scale` (the fingers' spread relative to the start)
4178 /// has gone far enough to fire a bind on `direction`.
4179 pub(crate) fn pinch_hits(direction: &str, scale: f64) -> bool {
4180 match direction {
4181 "in" => scale < 0.7,
4182 "out" => scale > 1.3,
4183 _ => false,
4184 }
4185 }
4186
4187 /// Run a gesture's bound action. The overview toggle lands on the hovered
4188 /// window, else on the FOCUSED one — never on the empty desktop under the
4189 /// pointer.
4190 pub(crate) unsafe fn run_gesture_action(
4191 wm: &mut crate::window_manager::WindowManager,
4192 action: crate::config::Action,
4193 command: Option<&str>,
4194 ) {
4195 let action = if action == crate::config::Action::Overview {
4196 wm.overview_action_for_gesture()
4197 } else {
4198 action
4199 };
4200 wm.execute_action(&action, command);
4201 }
4202
4203 unsafe extern "C" fn handle_pinch_begin(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
4204 let cursor = &mut *crate::container_of!(listener, Cursor, pinch_begin_listener);
4205 let event = data as *mut ffi::wlr_pointer_pinch_begin_event;
4206
4207 let seat = &mut *cursor.seat;
4208 let wm = &(*seat.server).wm;
4209 let pinch_enabled = wm.input_config.touchpad.as_ref().and_then(|t| t.gestures.as_ref()).and_then(|g| g.pinch).unwrap_or(true);
4210 if !pinch_enabled {
4211 return;
4212 }
4213 seat.handle_activity();
4214 // A swipe or pinch is deliberate input, like a click or a key: it ends
4215 // the session-restore settling phase (`WindowManager::startup_input_seen`),
4216 // so the first focus of a restored window pans to it. Until 2026-09-26
4217 // only buttons and keys counted, and after a login navigated purely by
4218 // three-finger swipes, each restored window's first focus left it
4219 // wherever it sat, often half off screen. Holds do not count: one begins
4220 // whenever fingers merely rest on the pad.
4221 (*seat.server).wm.startup_input_seen = true;
4222
4223 cursor.gesture_scale = 1.0;
4224 cursor.gesture_triggered = false;
4225
4226 let server = seat.server;
4227
4228 // A pinch starting over the desktop background (wallpaper or bare
4229 // desktop, same test as the right-click context menu) zooms the camera
4230 // for the whole gesture. Clients never see a begin, so update/end stay
4231 // ours too.
4232 let mut on_background = true;
4233 if let Some(result) = (*server).scene.at(cursor.x(), cursor.y()) {
4234 match result.data {
4235 SceneNodeDataVal::Window(window) => {
4236 if !(*window).is_wallpaper() {
4237 on_background = false;
4238 }
4239 }
4240 SceneNodeDataVal::LayerSurface(_) | SceneNodeDataVal::ShellSurface(_) | SceneNodeDataVal::LockSurface(_) | SceneNodeDataVal::OverrideRedirect(_) => {
4241 on_background = false;
4242 }
4243 }
4244 }
4245 if on_background {
4246 let wm = &mut (*server).wm;
4247 wm.stop_panning_animation();
4248 cursor.pinch_zoom_active = true;
4249 cursor.pinch_start_zoom = wm.desk_zoom;
4250 return;
4251 }
4252
4253 // A pinch over an app in `touchpad_view_apps` becomes a dolly drag.
4254 if cursor.view_drag_pinch_begin() {
4255 return;
4256 }
4257
4258 let pointer_gestures = (*server).input_manager.pointer_gestures;
4259 if !pointer_gestures.is_null() {
4260 ffi::wlr_pointer_gestures_v1_send_pinch_begin(
4261 pointer_gestures,
4262 seat.wlr_seat,
4263 (*event).time_msec,
4264 (*event).fingers,
4265 );
4266 }
4267 }
4268
4269 unsafe extern "C" fn handle_pinch_update(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
4270 let cursor = &mut *crate::container_of!(listener, Cursor, pinch_update_listener);
4271 let event = data as *mut ffi::wlr_pointer_pinch_update_event;
4272
4273 let seat = &mut *cursor.seat;
4274 let wm = &(*seat.server).wm;
4275 let pinch_enabled = wm.input_config.touchpad.as_ref().and_then(|t| t.gestures.as_ref()).and_then(|g| g.pinch).unwrap_or(true);
4276 if !pinch_enabled {
4277 return;
4278 }
4279 seat.handle_activity();
4280
4281 if cursor.view_drag_pinch_update((*event).scale) {
4282 return;
4283 }
4284
4285 if cursor.pinch_zoom_active {
4286 let wm = &mut (*seat.server).wm;
4287 let new_zoom = crate::policy::camera::pinch_zoom(cursor.pinch_start_zoom, (*event).scale);
4288 let cx = cursor.x();
4289 let cy = cursor.y();
4290 let wlr_output = (*(*seat).server).om.output_at(cx, cy);
4291 let (phys_x, phys_y) = if !wlr_output.is_null() {
4292 let mut output_box = ffi::wlr_box { x: 0, y: 0, width: 0, height: 0 };
4293 ffi::wlr_output_layout_get_box((*(*seat).server).om.output_layout, wlr_output, &mut output_box);
4294 (output_box.x as f64, output_box.y as f64)
4295 } else {
4296 (0.0, 0.0)
4297 };
4298 // Applied on the next output frame, like finger pans: libinput
4299 // delivers pinch updates faster than the refresh rate, and stepping
4300 // the camera per event relaid out the desktop for frames nobody
4301 // saw and zoomed unevenly (two steps in one frame, one in the next).
4302 wm.queue_pinch(new_zoom, cx - phys_x, cy - phys_y);
4303 return;
4304 }
4305
4306 if cursor.gesture_triggered {
4307 return;
4308 }
4309
4310 cursor.gesture_scale = (*event).scale;
4311
4312 let modifiers = gesture_mods(seat);
4313 let scale = cursor.gesture_scale;
4314 let bind = gesture_bind(&(*seat.server).wm, "pinch", (*event).fingers, modifiers, |d| pinch_hits(d, scale));
4315
4316 if let Some((matched_action, matched_command)) = bind {
4317 cursor.gesture_triggered = true;
4318 run_gesture_action(&mut (*seat.server).wm, matched_action, matched_command.as_deref());
4319
4320 let pointer_gestures = (*seat.server).input_manager.pointer_gestures;
4321 if !pointer_gestures.is_null() {
4322 ffi::wlr_pointer_gestures_v1_send_pinch_end(
4323 pointer_gestures,
4324 seat.wlr_seat,
4325 (*event).time_msec,
4326 true, // cancelled: true
4327 );
4328 }
4329 return;
4330 }
4331
4332 let server = seat.server;
4333 let pointer_gestures = (*server).input_manager.pointer_gestures;
4334 if !pointer_gestures.is_null() {
4335 ffi::wlr_pointer_gestures_v1_send_pinch_update(
4336 pointer_gestures,
4337 seat.wlr_seat,
4338 (*event).time_msec,
4339 (*event).dx,
4340 (*event).dy,
4341 (*event).scale,
4342 (*event).rotation,
4343 );
4344 }
4345 }
4346
4347 unsafe extern "C" fn handle_pinch_end(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
4348 let cursor = &mut *crate::container_of!(listener, Cursor, pinch_end_listener);
4349 let event = data as *mut ffi::wlr_pointer_pinch_end_event;
4350
4351 let seat = &mut *cursor.seat;
4352 let wm = &(*seat.server).wm;
4353 let pinch_enabled = wm.input_config.touchpad.as_ref().and_then(|t| t.gestures.as_ref()).and_then(|g| g.pinch).unwrap_or(true);
4354 if !pinch_enabled {
4355 return;
4356 }
4357 seat.handle_activity();
4358
4359 if cursor.view_drag_pinch_end() {
4360 return;
4361 }
4362
4363 if cursor.pinch_zoom_active {
4364 // Camera zoom consumed the whole gesture; clients got no begin, so
4365 // they get no end. The camera simply stays where the fingers left it.
4366 cursor.pinch_zoom_active = false;
4367 return;
4368 }
4369
4370 if cursor.gesture_triggered {
4371 cursor.gesture_triggered = false;
4372 return;
4373 }
4374
4375 let server = seat.server;
4376 let pointer_gestures = (*server).input_manager.pointer_gestures;
4377 if !pointer_gestures.is_null() {
4378 ffi::wlr_pointer_gestures_v1_send_pinch_end(
4379 pointer_gestures,
4380 seat.wlr_seat,
4381 (*event).time_msec,
4382 (*event).cancelled,
4383 );
4384 }
4385 }
4386
4387 unsafe extern "C" fn handle_hold_begin(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
4388 let cursor = &mut *crate::container_of!(listener, Cursor, hold_begin_listener);
4389 let event = data as *mut ffi::wlr_pointer_hold_begin_event;
4390
4391 let seat = &mut *cursor.seat;
4392 seat.handle_activity();
4393
4394 let server = seat.server;
4395 let pointer_gestures = (*server).input_manager.pointer_gestures;
4396 if !pointer_gestures.is_null() {
4397 ffi::wlr_pointer_gestures_v1_send_hold_begin(
4398 pointer_gestures,
4399 seat.wlr_seat,
4400 (*event).time_msec,
4401 (*event).fingers,
4402 );
4403 }
4404 }
4405
4406 unsafe extern "C" fn handle_hold_end(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
4407 let cursor = &mut *crate::container_of!(listener, Cursor, hold_end_listener);
4408 let event = data as *mut ffi::wlr_pointer_hold_end_event;
4409
4410 let seat = &mut *cursor.seat;
4411 seat.handle_activity();
4412
4413 let server = seat.server;
4414 let pointer_gestures = (*server).input_manager.pointer_gestures;
4415 if !pointer_gestures.is_null() {
4416 ffi::wlr_pointer_gestures_v1_send_hold_end(
4417 pointer_gestures,
4418 seat.wlr_seat,
4419 (*event).time_msec,
4420 (*event).cancelled,
4421 );
4422 }
4423 }
4424
4425 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
4426 pub enum BorderZone {
4427 None,
4428 Move,
4429 Resize(crate::window::Edges),
4430 /// A window button disc (`BorderElement::is_button`).
4431 Button(crate::window::BorderElement),
4432 }
4433
4434 pub use crate::window::HOVER_BAND_MIN;
4435
4436 /// The grid client's surface and the surface-local coordinates of a layout
4437 /// point, ignoring the input region.
4438 ///
4439 /// Hit-testing cannot be used for this: the grid's input region covers only
4440 /// its desktop items, so a point over bare canvas — where drops mostly land —
4441 /// misses it by design. A drop target does not need to be hit-testable, only
4442 /// named, so the surface is resolved directly and the point is mapped through
4443 /// the surface node's own layout origin and the window's display scale, which
4444 /// is the same pair the renderer draws with.
4445 pub unsafe fn grid_surface_at(
4446 server: *mut crate::server::Server,
4447 lx: f64,
4448 ly: f64,
4449 ) -> Option<(*mut ffi::wlr_surface, f64, f64)> {
4450 let (surface, nx, ny, scale, bw, bh) = grid_node_info(server)?;
4451 let (sx, sy) = ((lx - nx) / scale, (ly - ny) / scale);
4452 // Only claim points that actually fall on the grid's patch. box_geom
4453 // is the surface's own logical size, which is the space sx/sy are in.
4454 if sx < 0.0 || sy < 0.0 || (bw > 0.0 && sx >= bw) || (bh > 0.0 && sy >= bh) {
4455 return None;
4456 }
4457 Some((surface, sx, sy))
4458 }
4459
4460 /// The grid window's surface plus the raw mapping ingredients: its surface
4461 /// tree's layout origin, its display scale, and its logical size. This is
4462 /// the node state a caller may need to FREEZE (the implicit-grab path
4463 /// captures it at press time), which is why it is exposed separately from
4464 /// the point mapping above.
4465 pub unsafe fn grid_node_info(
4466 server: *mut crate::server::Server,
4467 ) -> Option<(*mut ffi::wlr_surface, f64, f64, f64, f64, f64)> {
4468 for &w in (*server).wm.windows.iter() {
4469 if w.is_null() || (*w).closed || !(*w).is_grid() {
4470 continue;
4471 }
4472 if !matches!((*w).state, crate::window::WindowState::Mapped) {
4473 continue;
4474 }
4475 let surface = (*w).root_surface();
4476 if surface.is_null() {
4477 continue;
4478 }
4479 let node = (*w).surfaces.tree as *mut ffi::wlr_scene_node;
4480 let (mut nx, mut ny) = (0, 0);
4481 if !ffi::wlr_scene_node_coords(node, &mut nx, &mut ny) {
4482 continue;
4483 }
4484 let scale = if (*w).scale > 0.0 { (*w).scale } else { 1.0 };
4485 return Some((
4486 surface,
4487 nx as f64,
4488 ny as f64,
4489 scale,
4490 (*w).box_geom.width as f64,
4491 (*w).box_geom.height as f64,
4492 ));
4493 }
4494 None
4495 }
4496
4497 /// Which handle disc, if any, a layout point falls on.
4498 ///
4499 /// The handles are eight discs INSIDE the content rect — one on each side,
4500 /// one on each corner — plus, for a Floating or Tiled window, the three
4501 /// window buttons in the top row (`BorderZone::Button`), and exist only in
4502 /// adjust mode (overview, or Super held). Two consequences worth stating, because
4503 /// both are deliberate:
4504 ///
4505 /// - Outside adjust mode there are no handles at all, so a window cannot
4506 /// be resized with the pointer. The keyboard and IPC paths
4507 /// (`move_window_*`, `ccectl move-window`, a client repositioning
4508 /// itself) are untouched; this is only about dragging.
4509 /// - Every disc RESIZES, the top one included. Moving is what dragging
4510 /// the window's body does in adjust mode, so no handle has to be spent
4511 /// on it; between two discs the pointer belongs to the body.
4512 ///
4513 /// `window::handle_disc_layout` places the discs for the hit test here,
4514 /// the catchers in `draw_borders`, and (mirrored in the frame shader) the
4515 /// drawing, so the zones and the visuals cannot drift.
4516 pub unsafe fn get_border_zone(window: *mut crate::window::Window, lx: f64, ly: f64) -> BorderZone {
4517 // Overview, or Super held (window-adjust mode): the same ring either way.
4518 if !(*(*window).server).wm.window_adjust_active() {
4519 return BorderZone::None;
4520 }
4521 if !crate::window::window_takes_handles(window) {
4522 return BorderZone::None;
4523 }
4524 // The adjust target only — the window under the pointer — matching what
4525 // draw_borders draws. A window showing no ring has no band, and a grab
4526 // that is not drawn is the failure mode this file keeps warning about.
4527 // The pointer reaches a window's edge through its body, so the band is
4528 // live by the time it arrives.
4529 if !(*window).is_adjust_target() {
4530 return BorderZone::None;
4531 }
4532
4533 let bw_unscaled = crate::window::border_band_width((*window).rendering_requested.border.width);
4534 if bw_unscaled <= 0.0 {
4535 return BorderZone::None;
4536 }
4537
4538 // box_geom holds the UNSCALED content size; on screen the window covers
4539 // `size * scale`, and lx/ly are layout px — so the CONTENT extents scale
4540 // but the discs do NOT. The disc diameter is a screen size, matching
4541 // what draw_borders draws: overview is zoomed out, and a handle that
4542 // shrank with the window would be smallest exactly where it is the only
4543 // way to resize. Keep the two in step.
4544 let scale = if (*window).scale > 0.0 { (*window).scale } else { 1.0 };
4545 let geom = (*window).box_geom;
4546 let rx = lx - geom.x as f64;
4547 let ry = ly - geom.y as f64;
4548 let content_w = geom.width as f64 * scale;
4549 let content_h = geom.height as f64 * scale;
4550
4551 let bw = ((*(*window).server).wm.layout.border_handle_width as f64)
4552 .max(crate::window::HOVER_BAND_MIN)
4553 // The same fifth-of-the-short-side cap draw_borders applies, so the
4554 // grab zone never outgrows the disc the user can see.
4555 .min(content_w.min(content_h).max(1.0) * 0.2);
4556
4557 // Outside the window entirely: not ours.
4558 if rx < 0.0 || rx >= content_w || ry < 0.0 || ry >= content_h {
4559 return BorderZone::None;
4560 }
4561 // A client popover (set_popover_region) owns its rect outright: the menu
4562 // reads as in front of the chrome, so nothing under it may grab. Checked
4563 // before the discs — it beats them.
4564 if let Some(r) = (*window).popover_region {
4565 let (ex, ey) = (r.x as f64 * scale, r.y as f64 * scale);
4566 let (ew, eh) = (r.width as f64 * scale, r.height as f64 * scale);
4567 if rx >= ex && rx < ex + ew && ry >= ey && ry < ey + eh {
4568 return BorderZone::None;
4569 }
4570 }
4571
4572 // The discs, from the same on-screen size, silhouette radius and
4573 // diameter draw_borders hands the frame shader, so what is drawn is
4574 // what grabs. A pixel of slack covers the antialiased rim. The corner
4575 // discs place against the content radius: the widened root plate
4576 // radius the corner clip uses, on screen.
4577 let r_in = crate::window::widen_corner_radius(
4578 (*window).root_plate_radius_base(),
4579 geom.width,
4580 geom.height,
4581 ) as f64;
4582 let r_in = (r_in * scale) as i32 as f64;
4583 let (centres, r, live) = crate::window::handle_disc_layout(
4584 content_w,
4585 content_h,
4586 r_in,
4587 bw,
4588 crate::window::window_takes_buttons(window),
4589 );
4590 let reach = (r + 1.0) * (r + 1.0);
4591 for (i, &(cx, cy)) in centres[..live].iter().enumerate() {
4592 let (dx, dy) = (rx - cx, ry - cy);
4593 if dx * dx + dy * dy <= reach {
4594 let elem = crate::window::BorderElement::ALL[i];
4595 if elem.is_button() {
4596 return BorderZone::Button(elem);
4597 }
4598 return BorderZone::Resize(edges_for_border_element(elem));
4599 }
4600 }
4601 // In the body, between the discs: not ours. This is what leaves the
4602 // body drag-to-move working.
4603 BorderZone::None
4604 }
4605
4606 /// The resize edges a handle disc stands for — the inverse of
4607 /// `border_element_for_edges`.
4608 pub fn edges_for_border_element(element: crate::window::BorderElement) -> crate::window::Edges {
4609 use crate::window::BorderElement::*;
4610 let (top, bottom, left, right) = match element {
4611 Top => (true, false, false, false),
4612 Bottom => (false, true, false, false),
4613 Left => (false, false, true, false),
4614 Right => (false, false, false, true),
4615 TopLeft => (true, false, true, false),
4616 TopRight => (true, false, false, true),
4617 BottomLeft => (false, true, true, false),
4618 BottomRight => (false, true, false, true),
4619 // A button resizes nothing.
4620 Minimize | Maximize | ToggleTile => (false, false, false, false),
4621 };
4622 crate::window::Edges { top, bottom, left, right }
4623 }
4624
4625 /// Map a resize zone's edges to the border element that should highlight.
4626 pub fn border_element_for_edges(edges: crate::window::Edges) -> crate::window::BorderElement {
4627 use crate::window::BorderElement::*;
4628 match (edges.top, edges.bottom, edges.left, edges.right) {
4629 (true, _, true, _) => TopLeft,
4630 (true, _, _, true) => TopRight,
4631 (_, true, true, _) => BottomLeft,
4632 (_, true, _, true) => BottomRight,
4633 (_, true, _, _) => Bottom,
4634 (_, _, true, _) => Left,
4635 (_, _, _, true) => Right,
4636 _ => Top,
4637 }
4638 }
4639
4640 pub fn get_resize_cursor_name(edges: crate::window::Edges) -> &'static [u8] {
4641 if edges.top && edges.left {
4642 b"nw-resize\0"
4643 } else if edges.top && edges.right {
4644 b"ne-resize\0"
4645 } else if edges.bottom && edges.left {
4646 b"sw-resize\0"
4647 } else if edges.bottom && edges.right {
4648 b"se-resize\0"
4649 } else if edges.top {
4650 b"n-resize\0"
4651 } else if edges.bottom {
4652 b"s-resize\0"
4653 } else if edges.left {
4654 b"w-resize\0"
4655 } else if edges.right {
4656 b"e-resize\0"
4657 } else {
4658 b"default\0"
4659 }
4660 }
4661
4662 pub unsafe fn get_closest_edges(window: *mut crate::window::Window, lx: f64, ly: f64) -> crate::window::Edges {
4663 let geom = (*window).box_geom;
4664 let rx = lx - geom.x as f64;
4665 let ry = ly - geom.y as f64;
4666 let w = geom.width as f64;
4667 let h = geom.height as f64;
4668
4669 let left = rx < w / 2.0;
4670 let right = !left;
4671 let top = ry < h / 2.0;
4672 let bottom = !top;
4673
4674 crate::window::Edges { top, bottom, left, right }
4675 }
4676
4677
4678 #[cfg(test)]
4679 mod tests {
4680 use super::*;
4681
4682 const ALL: [bool; 4] = [true; 4];
4683 const EPS: f64 = 1e-9;
4684
4685 #[test]
4686 fn straight_swipe_leans_on_its_axis() {
4687 // Halfway to the threshold: half the peek, and no drift below 15°.
4688 assert_eq!(swipe_lean(35.0, 5.0, ALL, 70.0, 120.0, 1.0), [60.0, 0.0]);
4689 assert_eq!(swipe_lean(-4.0, -35.0, ALL, 70.0, 120.0, 1.0), [0.0, -60.0]);
4690 }
4691
4692 #[test]
4693 fn diagonal_swipe_leans_diagonally() {
4694 let [x, y] = swipe_lean(35.0, -35.0, ALL, 70.0, 120.0, 1.0);
4695 assert!((x - 60.0).abs() < EPS && (y + 60.0).abs() < EPS, "{x} {y}");
4696 }
4697
4698 #[test]
4699 fn lean_turns_smoothly_off_the_axis() {
4700 // Just past the straight band a little; halfway through it, half.
4701 let slope_mid = (SWIPE_LEAN_STRAIGHT_SLOPE + 1.0) / 2.0;
4702 let [x, y] = swipe_lean(70.0, 70.0 * slope_mid, ALL, 70.0, 120.0, 1.0);
4703 assert!((x - 120.0).abs() < EPS && (y - 60.0).abs() < 1e-6, "{x} {y}");
4704 let [_, y] = swipe_lean(70.0, 70.0 * (SWIPE_LEAN_STRAIGHT_SLOPE + 0.01), ALL, 70.0, 120.0, 1.0);
4705 assert!(y > 0.0 && y < 3.0, "{y}");
4706 }
4707
4708 #[test]
4709 fn lean_respects_bound_directions() {
4710 // No up bind: a right-and-up swipe leans only right.
4711 assert_eq!(swipe_lean(35.0, -35.0, [true, true, false, true], 70.0, 120.0, 1.0), [60.0, 0.0]);
4712 // Nothing bound right: no lean at all, not even the minor part.
4713 assert_eq!(swipe_lean(35.0, -30.0, [true, false, true, true], 70.0, 120.0, 1.0), [0.0, 0.0]);
4714 }
4715
4716 #[test]
4717 fn lean_is_clamped_and_zoom_scaled() {
4718 // Past the threshold the size stops growing; zoomed out, the same
4719 // screen distance is a longer virtual one.
4720 let [x, y] = swipe_lean(140.0, 140.0, ALL, 70.0, 120.0, 0.5);
4721 assert!((x - 240.0).abs() < EPS && (y - 240.0).abs() < EPS, "{x} {y}");
4722 }
4723
4724 fn bind(direction: &str, action: crate::config::Action) -> crate::config::GestureBind {
4725 crate::config::GestureBind {
4726 mods: 0,
4727 gesture_type: "swipe".into(),
4728 fingers: 3,
4729 direction: direction.into(),
4730 action,
4731 command: None,
4732 }
4733 }
4734
4735 #[test]
4736 fn focus_vector_follows_natural_binds() {
4737 use crate::config::Action::*;
4738 let binds = [bind("left", FocusLeft), bind("right", FocusRight), bind("up", FocusUp), bind("down", FocusDown)];
4739 assert_eq!(swipe_focus_vector(&binds, 3, 0, (50.0, -30.0)), Some((50.0, -30.0)));
4740 // Another finger count or chord has no binds here.
4741 assert_eq!(swipe_focus_vector(&binds, 4, 0, (50.0, -30.0)), Option::None);
4742 }
4743
4744 #[test]
4745 fn focus_vector_mirrors_mirrored_binds() {
4746 use crate::config::Action::*;
4747 let binds = [bind("left", FocusRight), bind("right", FocusLeft), bind("up", FocusDown), bind("down", FocusUp)];
4748 assert_eq!(swipe_focus_vector(&binds, 3, 0, (50.0, -30.0)), Some((-50.0, 30.0)));
4749 }
4750
4751 #[test]
4752 fn focus_vector_ignores_an_axis_without_focus_binds() {
4753 use crate::config::Action::*;
4754 // Up/down pan rather than focus: a diagonal swipe aims sideways only.
4755 let binds = [bind("left", FocusLeft), bind("right", FocusRight), bind("up", PanUp), bind("down", PanDown)];
4756 assert_eq!(swipe_focus_vector(&binds, 3, 0, (50.0, -30.0)), Some((50.0, 0.0)));
4757 assert_eq!(swipe_focus_vector(&binds, 3, 0, (0.0, -30.0)), Option::None);
4758 }
4759
4760 }