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