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

use super::app_data::AppData;
use super::surface::{ SurfaceFocus, SurfaceState };
use crate::app::App;
use crate::types::Point;

impl<A: App> AppData<A>
{
	/// Fire long-press messages for any surface whose deadline has
	/// elapsed. Idempotent — if a press is in-flight but not yet due this
	/// does nothing. On fire, the message is pushed to `pending_msgs`,
	/// `long_press_fired` is set, and the per-surface long-press slot is
	/// cleared so the same press can't fire twice.
	pub( crate ) fn check_long_press_deadlines( &mut self )
	{
		let dur = self.app.long_press_duration();
		let now = std::time::Instant::now();

		// Returns `(lp_msg_opt, ds_msg_opt, origin, text_idx_opt)`:
		// - `lp_msg_opt = Some` → user-set on_long_press fires; push msg
		//   (opens the context menu in the app).
		// - `ds_msg_opt = Some` → user-set on_drag_start fires; push
		//   msg + seed `on_drag_move(origin)` so the drag arms with
		//   the right anchor point. Also flips `long_press_fired` so
		//   subsequent motion / release route through drag / drop.
		// - `text_idx_opt = Some` → press was on a TextEdit with
		//   neither user msg; the runtime opens the built-in Copy /
		//   Cut / Paste menu instead.
		// Any combination can be present: an icon with both menu and
		// drag fires both messages in order (menu first, then drag).
		let fire = |ss: &mut SurfaceState<A::Message>|
			-> Option<( Option<A::Message>, Option<A::Message>, Point, Option<usize> )>
		{
			let start = ss.gesture.long_press_start?;
			if now.duration_since( start ) < dur { return None; }
			ss.gesture.long_press_start = None;
			let origin   = ss.gesture.long_press_origin.unwrap_or_default();
			let lp_msg   = ss.gesture.long_press_msg.take();
			let ds_msg   = ss.gesture.drag_start_msg.take();
			let text_idx = ss.gesture.long_press_text_idx.take();
			if lp_msg.is_none() && ds_msg.is_none() && text_idx.is_none() { return None; }
			// Only flip "fired" when the gesture actually transitions
			// into drag mode. A menu-only widget (no on_drag_start)
			// stays in tap mode, so a release after the menu fires
			// still goes through `on_release` cleanly. The built-in
			// text menu likewise does not switch to drag, so a
			// subsequent motion can still extend a selection.
			if ds_msg.is_some() { ss.gesture.long_press_fired = true; }
			ss.request_redraw();
			Some( ( lp_msg, ds_msg, origin, text_idx ) )
		};

		// Capture per-surface fires, then post-process so the borrow
		// of `self.main` / `self.overlays` is released before we
		// call menu helpers (which take &mut self again).
		let main_fire = fire( &mut self.main ).map( |x| ( SurfaceFocus::Main, x ) );
		let overlay_fires: Vec<_> = self.overlays.iter_mut()
			.filter_map( |( id, ss )| fire( ss ).map( |x| ( SurfaceFocus::Overlay( *id ), x ) ) )
			.collect();

		let mut consumed_anything = false;
		for ( focus, ( lp_msg, ds_msg, origin, text_idx ) ) in main_fire.into_iter().chain( overlay_fires )
		{
			// Push menu first so the app sets up `edit_menu` before
			// the drag-arm message lands and starts consuming
			// `on_drag_move` callbacks.
			if let Some( m ) = lp_msg
			{
				self.pending_msgs.push( m );
				consumed_anything = true;
			}
			if let Some( m ) = ds_msg
			{
				self.pending_msgs.push( m );
				let ( ox, oy ) = self.surface_offset_for( focus );
				let global = Point { x: origin.x + ox, y: origin.y + oy };
				self.pending_drag_inits.push( global );
				// Drag promotion cancels any held-button repeat — the
				// gesture has switched semantics and the timer has
				// nothing to fire against any more.
				self.stop_button_repeat();
				consumed_anything = true;
			}
			if let Some( idx ) = text_idx
			{
				// Built-in Copy / Cut / Paste menu near the press.
				self.show_context_menu( focus, idx, origin );
			}
		}
		if consumed_anything { self.dirty_caches(); }
	}

	pub( crate ) fn has_active_long_press_drag( &self ) -> bool
	{
		self.main.gesture.long_press_fired
			|| self.overlays.values().any( |ss| ss.gesture.long_press_fired )
	}

	pub( crate ) fn clear_long_press_drag( &mut self )
	{
		let clear = |ss: &mut SurfaceState<A::Message>|
		{
			ss.gesture.long_press_start    = None;
			ss.gesture.long_press_origin   = None;
			ss.gesture.long_press_msg      = None;
			ss.gesture.drag_start_msg      = None;
			ss.gesture.long_press_text_idx = None;
			ss.gesture.long_press_fired    = false;
			ss.gesture.mouse_press         = false;
		};
		clear( &mut self.main );
		for ss in self.overlays.values_mut()
		{
			clear( ss );
		}
	}
}