ltk/event_loop/
clipboard.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>

use std::time::Duration;

use super::app_data::AppData;
use super::surface::SurfaceFocus;
use crate::app::App;
use crate::platform::Platform;
use crate::time::Instant;
use crate::tree::find_handlers;

/// How long a middle-button paste waits for another application's primary selection before it takes what there is.
const PRIMARY_WAIT: Duration = Duration::from_millis( 500 );
/// How often the runtime looks whether it has arrived meanwhile, since the thread reading it does not wake the runtime.
const PRIMARY_POLL: Duration = Duration::from_millis( 10 );

/// Where a middle-button paste goes.
pub( crate ) enum PrimaryPaste
{
	/// The text field `idx` of a surface, which the press focused.
	Field( SurfaceFocus, usize ),
	/// The app, which asked for the primary selection with [`App::take_paste_request`].
	App,
}

impl<A: App, P: Platform> AppData<A, P>
{
	/// Paste the primary selection where `target` says: at once, or, while another application's is still being read, once it has been, so a press on a window that has just been given the selection does not paste the one before.
	pub( crate ) fn paste_primary( &mut self, target: PrimaryPaste )
	{
		if self.platform.primary_selection_pending()
		{
			self.pending_primary_paste = Some( ( target, Instant::now() ) );
			return;
		}
		self.deliver_primary( target );
	}

	/// Carry out the paste waiting for the primary selection once it has been read, or once it has waited long enough.
	pub( crate ) fn check_primary_paste( &mut self )
	{
		let Some( ( _, since ) ) = &self.pending_primary_paste else { return };
		if self.platform.primary_selection_pending() && since.elapsed() < PRIMARY_WAIT { return; }
		if let Some( ( target, _ ) ) = self.pending_primary_paste.take() { self.deliver_primary( target ); }
	}

	pub( crate ) fn next_primary_paste_wakeup( &self ) -> Option<Duration>
	{
		self.pending_primary_paste.as_ref().map( |_| PRIMARY_POLL )
	}

	fn deliver_primary( &mut self, target: PrimaryPaste )
	{
		let text = self.primary_selection.clone();
		if text.is_empty() { return; }
		match target
		{
			PrimaryPaste::Field( focus, idx ) =>
			{
				if self.try_surface( focus ).is_some_and( |ss| ss.focused_idx == Some( idx ) ) { self.handle_text_insert( focus, &text ); }
			}
			PrimaryPaste::App => self.app.on_paste( &text ),
		}
	}

	/// Make `text` the clipboard, for other applications as well when the system allows.
	pub( crate ) fn publish_clipboard( &mut self, text: String )
	{
		self.platform.publish_clipboard( &text );
		self.clipboard = text;
	}

	/// Make `text` the selection the middle button pastes, for other applications as well when the system allows.
	pub( crate ) fn publish_primary_selection( &mut self, text: String )
	{
		self.platform.publish_primary_selection( &text );
		self.primary_selection = text;
	}

	/// Publish a new selection in the focused text field once the press that makes it ends; a password field's never leaves it.
	pub( crate ) fn sync_text_field( &mut self )
	{
		let focus = self.keyboard_focus;
		if self.try_surface( focus ).is_none_or( |ss| ss.gesture.pressed_idx.is_some() ) { return; }
		let secure   = self.surface( focus ).focused_idx
			.and_then( |idx| self.text_input_geometry( focus, idx ) )
			.is_some_and( |( _, _, _, secure, _, _ )| secure );
		let selected = if secure { None } else { self.focused_selection_text( focus ) };
		if selected == self.primary_field { return; }
		self.primary_field = selected.clone();
		if let Some( text ) = selected { self.publish_primary_selection( text ); }
	}

	/// Copy the selected text to the clipboard. No-op when there is no selection.
	pub( crate ) fn handle_copy( &mut self, focus: SurfaceFocus )
	{
		if let Some( text ) = self.focused_selection_text( focus )
		{
			self.publish_clipboard( text );
		}
	}

	/// Copy + delete: the selected text lands in the clipboard, then
	/// the range is removed from the value.
	pub( crate ) fn handle_cut( &mut self, focus: SurfaceFocus )
	{
		let idx = match self.surface( focus ).focused_idx { Some( i ) => i, None => return };
		if let Some( text ) = self.focused_selection_text( focus )
		{
			self.publish_clipboard( text );
		}
		if let Some( new_value ) = self.delete_selection( focus )
		{
			let msg = find_handlers( &self.surface( focus ).frame.widget_rects, idx )
				.and_then( |h| h.text_change_msg( &new_value ) );
			if let Some( m ) = msg { self.pending_msgs.push( m ); }
		}
	}

	/// Insert the clipboard contents at the cursor (replacing the
	/// selection if there is one). No-op when the clipboard is empty.
	pub( crate ) fn handle_paste( &mut self, focus: SurfaceFocus )
	{
		if self.clipboard.is_empty() { return; }
		// Carbon-copy of the clipboard to break the borrow on `self`
		// before `handle_text_insert` runs.
		let text = self.clipboard.clone();
		self.handle_text_insert( focus, &text );
	}
}