// SPDX-License-Identifier: LGPL-2.1-only // Copyright (C) 2026 Liberux Labs, S. L. use smithay_client_toolkit::seat::pointer::{ PointerEvent, PointerEventKind }; 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 crate::widget::WidgetHandlers; impl AppData { pub( super ) fn on_pointer_right_click( &mut self, _conn: &Connection, qh: &QueueHandle, _pointer: &WlPointer, event: &PointerEvent, ) { let focus = self.focus_for_surface( &event.surface ) .unwrap_or( SurfaceFocus::Main ); // Right-click: the desktop equivalent of a touch // long-press. Three cases on the press target: // // * Widget with `on_long_press` (Button or // Pressable that opted in) — fire the message. // The drag-arm slot (`on_drag_start`) is NOT // consumed: right-click never enters drag mode, // so an icon's context menu opens but no drag // is armed. // * TextEdit with no user `on_long_press` — open // the built-in Copy / Cut / Paste menu near the // click. Selection is preserved (we deliberately // skip `handle_text_pointer_down`) so the common // "select text → right-click → Copy" flow works. // * Anywhere else — dismiss any already-open menu. let pos = self.surface( focus ).to_physical( event.position.0, event.position.1 ); self.pointer_pos = pos; self.app.on_pointer_move( pos.x, pos.y ); let hit_idx = find_widget_at( &self.surface( focus ).frame.widget_rects, pos ); let lp_msg = hit_idx.and_then( |idx| find_handlers( &self.surface( focus ).frame.widget_rects, idx ) .and_then( |h| h.long_press_msg() ) ); if let Some( msg ) = lp_msg { self.pending_msgs.push( msg ); self.surface_mut( focus ).request_redraw(); } else { let is_text = hit_idx.and_then( |idx| find_handlers( &self.surface( focus ).frame.widget_rects, idx ) .map( |h| h.is_text_input() ) ).unwrap_or( false ); if is_text { let idx = hit_idx.unwrap(); if self.surface( focus ).focused_idx != Some( idx ) { self.set_focus( focus, hit_idx, qh ); } self.show_context_menu( focus, idx, pos ); } else { self.hide_context_menu( focus ); self.app.on_pointer_secondary( pos.x, pos.y ); } } } /// Paste the primary selection into the text field under the pointer, or tell the app. pub( super ) fn on_pointer_middle_press( &mut self, qh: &QueueHandle, event: &PointerEvent ) { let PointerEventKind::Press { serial, .. } = event.kind else { return }; self.last_pointer_serial = serial; self.last_input_serial = serial; let focus = self.focus_for_surface( &event.surface ).unwrap_or( SurfaceFocus::Main ); let pos = self.surface( focus ).to_physical( event.position.0, event.position.1 ); self.pointer_pos = pos; let rects = &self.surface( focus ).frame.widget_rects; let field = find_widget_at( rects, pos ).filter( |&idx| find_handlers( rects, idx ).is_some_and( |h| h.is_text_input() ) ); let Some( idx ) = field else { return self.app.on_pointer_middle( pos.x, pos.y ) }; self.press_on_focused = self.surface( focus ).focused_idx == Some( idx ); if !self.press_on_focused { self.set_focus( focus, Some( idx ), qh ); } self.handle_text_pointer_down( focus, idx, pos ); let text = self.primary.text.clone(); if !text.is_empty() { self.handle_text_insert( focus, &text ); } } pub( super ) fn on_pointer_left_press( &mut self, _conn: &Connection, qh: &QueueHandle, _pointer: &WlPointer, event: &PointerEvent, ) { let focus = self.focus_for_surface( &event.surface ) .unwrap_or( SurfaceFocus::Main ); let serial = if let PointerEventKind::Press { serial, .. } = event.kind { serial } else { return; }; self.last_pointer_serial = serial; self.last_input_serial = serial; self.cancel_tooltip(); let pos = self.surface( focus ).to_physical( event.position.0, event.position.1 ); self.pointer_pos = pos; self.app.on_pointer_move( pos.x, pos.y ); if matches!( focus, SurfaceFocus::Main ) && !self.overlays.is_empty() { self.dismiss_main_outside_popups( pos ); } // Built-in context menu intercepts the press // before the regular gesture machine. Either an // item activates (Copy / Cut / Paste) or the // click is outside the menu and dismisses it — // in both cases we consume the event. if self.surface( focus ).context_menu.is_some() { if self.handle_context_menu_press( focus, pos ) { return; } } if self.osk_press( focus, pos ) { return; } // Client-side title bar interaction — pointer-only // (touch never hits a titlebar; layer-shell surfaces // have titlebar_height == 0). let sf = self.surface( focus ).scale_factor.max( 1 ) as f32; let tb_h = self.surface( focus ).titlebar_height * sf; if tb_h > 0.0 && pos.y < tb_h { let close_rect = self.surface( focus ).titlebar_close_rect; if close_rect.contains( pos ) { if self.app.on_close_requested() { self.exit_requested = true; } return; } } // Resize-edge interception. Wins over the titlebar // drag-move (top-left / top-right corners overlap // the titlebar) and over the gesture machine. if let Some( edge ) = self.resize_edge_under_pointer( focus ) { if let crate::event_loop::SurfaceKind::Window( ref window ) = self.main.surface { let seats: Vec<_> = self.seat_state.seats().collect(); if let Some( seat ) = seats.into_iter().next() { window.resize( &seat, serial, edge ); } } return; } if tb_h > 0.0 && pos.y < tb_h { // Drag to move the window. if matches!( focus, SurfaceFocus::Main ) { if let crate::event_loop::SurfaceKind::Window( ref window ) = self.main.surface { let seats: Vec<_> = self.seat_state.seats().collect(); if let Some( seat ) = seats.into_iter().next() { window.move_( &seat, serial ); } } } return; } // Past every chrome interception — surface the // press to the app so embeddings (e.g. an // embedded WPE WebView) see real button-down // events that were not consumed by the window // frame. self.app.on_pointer_button( pos.x, pos.y, true ); let outcome = { let ss = self.surface_mut( focus ); let result = ss.gesture.on_press( pos, &ss.frame.widget_rects, &ss.frame.scroll_rects ); // Mark this gesture as mouse-driven so the // gesture machine's 6 px stray-cancel skips // the drag-start / long-press slots — mouse // motion is intentional and should never // drop the candidate before the pointer-side // promotion at 24 px gets to fire. ss.gesture.mouse_press = true; ss.needs_redraw = true; result }; self.press_on_focused = outcome.hit_idx.is_some() && self.surface( focus ).focused_idx == outcome.hit_idx; self.set_focus( focus, outcome.hit_idx, qh ); self.osk_after_press( focus, outcome.hit_idx ); if let Some( msg ) = outcome.initial_slider_msg { self.pending_msgs.push( msg ); } // Press-and-hold repeat: when the hit is a button // that opted into `.repeating( true )`, fire the // `on_press` message immediately and arm the // repeat timer. The release handler in // `gesture.rs` knows to suppress the regular tap- // on-release fire for repeating buttons so a // quick click still counts as exactly one press. // The timer re-reads `on_press` from the live // widget tree on every tick — see // `start_button_repeat` for why. if let Some( idx ) = outcome.hit_idx { let immediate = { let handlers = find_handlers( &self.surface( focus ).frame.widget_rects, idx ); if matches!( handlers, Some( WidgetHandlers::Button { repeating: true, .. } ) ) { handlers.and_then( |h| h.press_msg() ) } else { None } }; if let Some( msg ) = immediate { self.pending_msgs.push( msg ); self.start_button_repeat( focus, idx ); } } // Click-to-position the text cursor when the press // landed on a TextEdit. `set_focus` above moves the // cursor to the end of the value; this overrides // it with the byte offset under the pointer and // collapses the selection there so a subsequent // drag widens the selection from the click point. // // Double-click on a TextEdit selects the word // under the cursor instead of just positioning. if let Some( idx ) = outcome.hit_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 { // Eye icon hit on a password field // short-circuits the text-edit // dispatch — fire the toggle msg and // skip cursor placement. if self.handle_password_toggle_press( focus, idx, pos ) { let _ = self.note_press_for_double_click( pos ); } else { let is_double = self.note_press_for_double_click( pos ); if is_double { self.handle_text_select_word( focus, idx, pos ); } else { self.handle_text_pointer_down( focus, idx, pos ); } } } else { let _ = self.note_press_for_double_click( pos ); } } else { let _ = self.note_press_for_double_click( pos ); } // Slider press → drag may have started; refresh // the cursor shape so it switches to `Grabbing`. self.dispatch_cursor_shape( focus ); } }