git.lucas.co / cce-compositor
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