Files
ltk/src/input/pointer/press.rs
T
admin 8f689ec479
CI / build + test (push) Canceled after 0s
CI / cargo audit (push) Canceled after 0s
An ltk app pastes what other applications copied, the middle button pastes the selection just made, a browser desktop's applications share the clipboard and the primary selection, and the office icons are drawn like the terminal's
Pasting from other applications. Neither Ctrl+V nor the middle button pasted into an ltk app anything copied in another application; only what was copied within the same process pasted. The Wayland backend bound its `wl_data_device` and its primary-selection device in `SeatHandler::new_seat`, which smithay-client-toolkit calls only for seats announced after start-up: `SeatState::new` binds the seats already in the registry without calling it, and on every compositor the seat is there when the app connects. The app therefore never had either device, the compositor never offered it another client's selection, and what it copied did not reach other applications through the compositor either. A Wayland trace of prompt shows both managers bound and neither `get_data_device` nor `get_device` sent. Both devices are now bound in `new_capability`, which comes for every seat, the first time it does; `new_seat` does nothing.
The middle button and a selection just made. A compositor offers the primary selection to the window it gives the keyboard, and gives it on the press, so a middle click on a window that did not have the keyboard arrives together with the offer. ltk reads an offer on a thread of its own and pasted at once what it held, so after selecting in another application an ltk app pasted the selection before that. `Platform::primary_selection_pending` says whether another application's selection is still being read: the Wayland backend counts the reader threads it starts and the selections that arrive (`PrimarySelection::pending`), and the other backends answer `false`. A middle-button paste, into the text field under the pointer (`PrimaryPaste::Field`) or for an app that asked with `SelectionKind::Primary` (`PrimaryPaste::App`), goes through `AppData::paste_primary`, which while a read is pending keeps it in `pending_primary_paste`; `check_primary_paste`, run before every draw, makes it once the selection has arrived or after 500 ms with what there is, and `next_primary_paste_wakeup` wakes the loop every 10 ms meanwhile, since the reading thread does not. A field paste is made only if the field still has the focus.
The clipboard and the primary selection in a browser desktop. On a page with forge/web, what an app copies goes to forge/web as a `set_selection` request as well as to the browser's clipboard, and the selection the middle button pastes, which did nothing in a browser, as `set_primary_selection` (`Connection::set_selection`). forge/web's `selection` and `primary_selection` events, which every client hears when one sets them and each hears on connecting, become `PageEvent::Selection` and so `AppData::clipboard` and `AppData::primary_selection`, as another client's selection does on Wayland. Copying, and selecting and pasting with the middle button, thus work between the page's applications whether or not the browser lets a page write its clipboard. A `paste` from the browser still brings what other applications of the computer copied; a browser has no primary selection a page can reach.
The office icons. The default theme's word processor, spreadsheet and presentation icons came from Figma with a `foreignObject` backdrop blur that resvg ignores, and showed as flat white discs with pale glyphs. They are drawn again as the terminal's are: a dark gradient disc in each one's colour (blue, green, orange), a bright glyph and the terminal's filter.
The doc comment of `ltk::files` links `write()` as a function, since `write` alone was ambiguous with the macro for rustdoc.
`CHANGELOG.md` describes the changes.
2026-10-23 19:20:10 +02:00

227 lines
8.0 KiB
Rust

// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
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<A: App, P: Platform> AppData<A, P>
{
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 );
}
}