ltk/input/pointer/
press.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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>

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<A: App> AppData<A>
{
	pub( super ) fn on_pointer_right_click(
		&mut self,
		_conn:    &Connection,
		qh:       &QueueHandle<Self>,
		_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 ).widget_rects, pos );
		let lp_msg = hit_idx.and_then( |idx|
			find_handlers( &self.surface( focus ).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 ).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 );
			}
		}
	}

	pub( super ) fn on_pointer_left_press(
		&mut self,
		_conn:    &Connection,
		qh:       &QueueHandle<Self>,
		_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;
			}
		}

		// 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.widget_rects, &ss.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.set_focus( focus, outcome.hit_idx, qh );
		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 ).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 ).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 );
	}
}