// SPDX-License-Identifier: LGPL-2.1-only // Copyright (C) 2026 Liberux Labs, S. L. use smithay_client_toolkit::seat::pointer::PointerEvent; use smithay_client_toolkit::reexports::client:: { protocol::wl_pointer::WlPointer, Connection, QueueHandle, }; use crate::app::App; use crate::event_loop::{ AppData, SurfaceFocus }; use crate::tree::{ find_handlers, find_widget_at }; use super::helpers::hover_affects_paint; impl AppData { pub( super ) fn on_pointer_motion( &mut self, _conn: &Connection, _qh: &QueueHandle, _pointer: &WlPointer, event: &PointerEvent, ) { let focus = self.focus_for_surface( &event.surface ) .unwrap_or( SurfaceFocus::Main ); let pp = self.surface( focus ).to_physical( event.position.0, event.position.1 ); self.pointer_pos = pp; self.app.on_pointer_move( pp.x, pp.y ); // Hover tracking — pointer-only (touch has no hover). // Runs before the gesture motion so the cache-dirty // below picks up any hover-dependent redraw request. let new_hover = find_widget_at( &self.surface( focus ).frame.widget_rects, pp ); let old_hover = self.surface( focus ).hovered_idx; if new_hover != old_hover { let redraw = hover_affects_paint( &self.surface( focus ).frame.widget_rects, old_hover ) || hover_affects_paint( &self.surface( focus ).frame.widget_rects, new_hover ); { let ss = self.surface_mut( focus ); ss.hovered_idx = new_hover; if redraw { ss.needs_redraw = true; } } match new_hover { Some( idx ) => self.arm_tooltip( focus, idx ), None => self.cancel_tooltip(), } } // Mouse drag-promotion: a left-button press whose hit // widget carries `on_drag_start` should arm a drag as // soon as the cursor moves past the threshold, without // waiting for the touch hold timer. Touch keeps its // hold-then-drag path because scroll / swipe gestures // need the in-between motion budget; mouse has a // dedicated right-click for the menu so left-button // can be drag-only. // // Threshold of 24 px (logical) sits comfortably above // the gesture machine's 6 px long-press cancel // tolerance and above crustace's 16 px drag-commit // threshold, so by the time we promote the next // motion sample is already past the app's commit // distance and drag mode latches without flashing // any half-state. // // Promotion is synchronous (`self.app.update(...)` // directly) so the app's drag state is armed BEFORE // the `on_drag_move` call below runs — otherwise the // seed coords land on a `dragging_item = None` // shell and get lost. We pay the cost of bypassing // `invalidate_after` for this one msg, but the next // frame will repaint everything anyway because the // drag is in flight. let promote = { let ss = self.surface( focus ); match ( ss.gesture.long_press_origin, ss.gesture.drag_start_msg.is_some() ) { ( Some( o ), true ) => ( pp.x - o.x ).hypot( pp.y - o.y ) > 24.0, _ => false, } }; if promote { let ( ds_msg, origin ) = { let ss = self.surface_mut( focus ); let m = ss.gesture.drag_start_msg.take().expect( "promote checked is_some" ); let o = ss.gesture.long_press_origin.expect( "promote checked Some(origin)" ); ss.gesture.long_press_start = None; ss.gesture.long_press_origin = None; ss.gesture.long_press_msg = None; ss.gesture.long_press_text_idx = None; ss.gesture.long_press_fired = true; ss.request_redraw(); ( m, o ) }; self.app.update( ds_msg ); let ( ox, oy ) = self.surface_offset_for( focus ); self.app.on_drag_move( origin.x + ox, origin.y + oy ); self.dirty_caches(); self.stop_button_repeat(); } // `global_drag` must be sampled AFTER the promotion // above — promotion flips `long_press_fired` and we // want the gesture machine to take the drag branch // for the same motion event that triggered the // promotion. let global_drag = self.has_active_long_press_drag(); let swipe = self.swipe_config( focus ); let outcome = { let ss = self.surface_mut( focus ); ss.gesture.on_move( pp, &ss.frame.widget_rects, &mut ss.frame.scroll_offsets, &swipe, global_drag ) }; self.apply_move_outcome( focus, outcome ); // Drag-to-select inside a TextEdit. Runs after the // gesture machine so the gesture's "did we leave // the press's hit rect?" reasoning still applies // to the press itself; for text fields the answer // is "fine, keep selecting" because we only widen // the selection while the pointer is still inside // the same widget rect. let pressed_text = self.surface( focus ).gesture.pressed_idx .and_then( |idx| { let is_text = find_handlers( &self.surface( focus ).frame.widget_rects, idx ) .map( |h| h.is_text_input() ).unwrap_or( false ); if is_text { Some( idx ) } else { None } } ); if let Some( idx ) = pressed_text { self.handle_text_pointer_drag( focus, idx, pp ); } // Cursor shape: hover may have changed → push the // new shape to the compositor (no-op when // unchanged). self.dispatch_cursor_shape( focus ); } }