ltk/input/pointer/
motion.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
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>

use smithay_client_toolkit::seat::pointer::PointerEvent;
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 super::helpers::hover_affects_paint;

impl<A: App> AppData<A>
{
	pub( super ) fn on_pointer_motion(
		&mut self,
		_conn:    &Connection,
		_qh:      &QueueHandle<Self>,
		_pointer: &WlPointer,
		event:    &PointerEvent,
	)
	{
		let focus = self.focus_for_surface( &event.surface )
			.unwrap_or( SurfaceFocus::Main );
		let pp = self.surface( focus ).to_physical( event.position.0, event.position.1 );
		self.pointer_pos = pp;
		self.app.on_pointer_move( pp.x, pp.y );

		// Hover tracking — pointer-only (touch has no hover).
		// Runs before the gesture motion so the cache-dirty
		// below picks up any hover-dependent redraw request.
		let new_hover = find_widget_at( &self.surface( focus ).widget_rects, pp );
		let old_hover = self.surface( focus ).hovered_idx;
		if new_hover != old_hover
		{
			let redraw = hover_affects_paint( &self.surface( focus ).widget_rects, old_hover )
				|| hover_affects_paint( &self.surface( focus ).widget_rects, new_hover );
			{
				let ss = self.surface_mut( focus );
				ss.hovered_idx  = new_hover;
				if redraw { ss.needs_redraw = true; }
			}
			match new_hover
			{
				Some( idx ) => self.arm_tooltip( focus, idx ),
				None        => self.cancel_tooltip(),
			}
		}

		// Mouse drag-promotion: a left-button press whose hit
		// widget carries `on_drag_start` should arm a drag as
		// soon as the cursor moves past the threshold, without
		// waiting for the touch hold timer. Touch keeps its
		// hold-then-drag path because scroll / swipe gestures
		// need the in-between motion budget; mouse has a
		// dedicated right-click for the menu so left-button
		// can be drag-only.
		//
		// Threshold of 24 px (logical) sits comfortably above
		// the gesture machine's 6 px long-press cancel
		// tolerance and above crustace's 16 px drag-commit
		// threshold, so by the time we promote the next
		// motion sample is already past the app's commit
		// distance and drag mode latches without flashing
		// any half-state.
		//
		// Promotion is synchronous (`self.app.update(...)`
		// directly) so the app's drag state is armed BEFORE
		// the `on_drag_move` call below runs — otherwise the
		// seed coords land on a `dragging_item = None`
		// shell and get lost. We pay the cost of bypassing
		// `invalidate_after` for this one msg, but the next
		// frame will repaint everything anyway because the
		// drag is in flight.
		let promote = {
			let ss = self.surface( focus );
			match ( ss.gesture.long_press_origin, ss.gesture.drag_start_msg.is_some() )
			{
				( Some( o ), true ) => ( pp.x - o.x ).hypot( pp.y - o.y ) > 24.0,
				_                   => false,
			}
		};
		if promote
		{
			let ( ds_msg, origin ) = {
				let ss = self.surface_mut( focus );
				let m = ss.gesture.drag_start_msg.take().expect( "promote checked is_some" );
				let o = ss.gesture.long_press_origin.expect( "promote checked Some(origin)" );
				ss.gesture.long_press_start    = None;
				ss.gesture.long_press_origin   = None;
				ss.gesture.long_press_msg      = None;
				ss.gesture.long_press_text_idx = None;
				ss.gesture.long_press_fired    = true;
				ss.request_redraw();
				( m, o )
			};
			self.app.update( ds_msg );
			let ( ox, oy ) = self.surface_offset_for( focus );
			self.app.on_drag_move( origin.x + ox, origin.y + oy );
			self.dirty_caches();
			self.stop_button_repeat();
		}

		// `global_drag` must be sampled AFTER the promotion
		// above — promotion flips `long_press_fired` and we
		// want the gesture machine to take the drag branch
		// for the same motion event that triggered the
		// promotion.
		let global_drag = self.has_active_long_press_drag();
		let swipe       = self.swipe_config( focus );
		let outcome =
		{
			let ss = self.surface_mut( focus );
			ss.gesture.on_move( pp, &ss.widget_rects, &mut ss.scroll_offsets, &swipe, global_drag )
		};
		self.apply_move_outcome( focus, outcome );

		// Drag-to-select inside a TextEdit. Runs after the
		// gesture machine so the gesture's "did we leave
		// the press's hit rect?" reasoning still applies
		// to the press itself; for text fields the answer
		// is "fine, keep selecting" because we only widen
		// the selection while the pointer is still inside
		// the same widget rect.
		let pressed_text = self.surface( focus ).gesture.pressed_idx
			.and_then( |idx|
			{
				let is_text = find_handlers( &self.surface( focus ).widget_rects, idx )
					.map( |h| h.is_text_input() ).unwrap_or( false );
				if is_text { Some( idx ) } else { None }
			} );
		if let Some( idx ) = pressed_text
		{
			self.handle_text_pointer_drag( focus, idx, pp );
		}
		// Cursor shape: hover may have changed → push the
		// new shape to the compositor (no-op when
		// unchanged).
		self.dispatch_cursor_shape( focus );
	}
}