git.lucas.co / cce-compositor
Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git

src/server/cursor.rs (218.3K)

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