// SPDX-License-Identifier: LGPL-2.1-only // Copyright (C) 2026 Liberux Labs, S. L. use crate::app::App; use crate::event_loop::{ AppData, SurfaceFocus }; use crate::platform::Platform; use crate::tree::{ find_handlers, find_widget_at }; use crate::widget::WidgetHandlers; use super::super::event::PointerInput; impl AppData { pub( super ) fn on_pointer_right_click( &mut self, input: &PointerInput ) { let focus = input.focus; // 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( input.position.0, input.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 ); } 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, input: &PointerInput ) { let focus = input.focus; let pos = self.surface( focus ).to_physical( input.position.0, input.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 ) ); } self.handle_text_pointer_down( focus, idx, pos ); self.paste_primary( crate::event_loop::clipboard::PrimaryPaste::Field( focus, idx ) ); } pub( super ) fn on_pointer_left_press( &mut self, input: &PointerInput ) { let focus = input.focus; self.cancel_tooltip(); let pos = self.surface( focus ).to_physical( input.position.0, input.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() && 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; 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 ) { self.platform.begin_window_resize( &self.main.platform, edge ); return; } if tb_h > 0.0 && pos.y < tb_h { if matches!( focus, SurfaceFocus::Main ) { self.platform.begin_window_move( &self.main.platform ); } 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 ); 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 ); } }