ltk/event_loop/
step.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
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>

//! The parts of a run-loop iteration that are the same on every backend. After waking, a backend handles what its system delivered, then calls [`AppData::before_draw`], [`AppData::draw_due`] and [`AppData::after_draw`], and sleeps at most [`AppData::next_wakeup`].

use std::time::Duration;

use crate::app::{ App, InvalidationScope, SelectionKind, TextCommand };
use crate::platform::Platform;
use crate::tree::find_handlers;
use crate::widget::WidgetHandlers;

use super::{ AppData, SurfaceFocus };
use super::frame::draw_frame;
use super::invalidation::apply_invalidation;

/// Drawn frames a focus request waits for its target to appear: one for the rebuilt view, one per scroll viewport moved to reveal the widget.
const FOCUS_RETRY_FRAMES: u8 = 4;

impl<A: App, P: Platform> AppData<A, P>
{
	/// The longest the backend may sleep with no event: until a still press turns long, a tooltip shows or the built-in keyboard acts. Frames are paced by the system, so nothing else needs a deadline.
	pub( crate ) fn next_wakeup( &self ) -> Option<Duration>
	{
		[ self.next_long_press_wakeup(), self.next_tooltip_wakeup(), self.next_osk_wakeup(), self.next_primary_paste_wakeup() ]
			.into_iter()
			.flatten()
			.min()
	}

	/// Everything between handling the system's events and drawing: deadlines that passed, the app's external messages, the update, and what the update asks of the window.
	pub( crate ) fn before_draw( &mut self )
	{
		self.check_long_press_deadlines();
		self.check_tooltip_deadline();
		self.check_osk_deadlines();
		self.check_primary_paste();

		let ext: Vec<_> = self.app.poll_external();
		self.pending_msgs.extend( ext );

		self.process_messages();
		if self.app.needs_repaint()
		{
			self.main.request_redraw();
			for ( _, ss ) in self.secondaries.iter_mut()
			{
				ss.request_redraw();
			}
		}
		if self.app.requested_exit() { self.exit_requested = true; }
		self.sync_fullscreen();
		self.sync_external_text_input();

		// After `update()`, so the app's drag state set up by the paired long-press message is there to receive the origin.
		let drag_inits: Vec<_> = self.pending_drag_inits.drain( .. ).collect();
		if !drag_inits.is_empty()
		{
			for origin in drag_inits
			{
				self.app.on_drag_move( origin.x, origin.y );
			}
			self.dirty_caches();
			self.main.request_redraw();
		}
		// The app state is now the source of truth, so the next keystroke must read it rather than a stale buffer (a password cleared on a failed login).
		self.main.pending_text_values.clear();
		for ss in self.overlays.values_mut()
		{
			ss.pending_text_values.clear();
		}
	}

	/// Everything after drawing: the accessibility tree and its actions, then the app's requests to blur, copy, paste and move the focus. `drawn` is what [`Self::draw_due`] returned.
	pub( crate ) fn after_draw( &mut self, drawn: bool )
	{
		if drawn { self.publish_a11y_tree(); }
		self.apply_a11y_actions();

		if self.app.take_blur_request()
		{
			let focus = self.keyboard_focus;
			self.set_focus( focus, None );
			self.focus_retry = None;
		}

		if let Some( text ) = self.app.take_clipboard_request()
		{
			self.publish_clipboard( text );
		}
		self.sync_text_field();
		if let Some( text ) = self.app.take_primary_request()
		{
			self.publish_primary_selection( text );
		}
		match self.app.take_paste_request()
		{
			Some( SelectionKind::Clipboard ) =>
			{
				let text = self.clipboard.clone();
				if !text.is_empty() { self.app.on_paste( &text ); }
			}
			Some( SelectionKind::Primary ) => self.paste_primary( super::clipboard::PrimaryPaste::App ),
			None => {}
		}

		self.apply_focus_request( drawn );
		self.apply_text_command();
		self.report_text_cursors();
	}

	/// Tell each text field with `on_cursor` where its cursor is, once it has moved or the value's length has changed.
	fn report_text_cursors( &mut self )
	{
		let surfaces = std::iter::once( SurfaceFocus::Main )
			.chain( self.overlays.keys().map( |id| SurfaceFocus::Overlay( *id ) ) )
			.collect::<Vec<_>>();
		let mut reports = Vec::new();
		for focus in surfaces
		{
			let frame = &self.surface( focus ).frame;
			for w in &frame.widget_rects
			{
				let WidgetHandlers::TextEdit { value, on_cursor: Some( _ ), .. } = &w.handlers else { continue };
				let Some( &at ) = frame.cursor_state.get( &w.flat_idx ) else { continue };
				let seen = ( at.min( value.len() ), value.len() );
				if self.reported_cursors.get( &( focus, w.flat_idx ) ) == Some( &seen ) { continue; }
				if let Some( msg ) = w.handlers.text_cursor_msg( seen.0 )
				{
					reports.push( ( ( focus, w.flat_idx ), seen, msg ) );
				}
			}
		}
		if reports.is_empty() { return; }
		for ( key, seen, msg ) in reports
		{
			self.reported_cursors.insert( key, seen );
			self.pending_msgs.push( msg );
		}
		self.main.request_redraw();
	}

	/// The surface and flat index of the widget the app named `id`, on the main surface or any overlay.
	fn find_widget( &self, id: crate::types::WidgetId ) -> Option<( SurfaceFocus, usize )>
	{
		self.main.frame.widget_rects.iter()
			.find( |w| w.id == Some( id ) )
			.map( |w| ( SurfaceFocus::Main, w.flat_idx ) )
			.or_else( || self.overlays.iter().find_map( |( ov_id, surf )|
			{
				surf.frame.widget_rects.iter()
					.find( |w| w.id == Some( id ) )
					.map( |w| ( SurfaceFocus::Overlay( *ov_id ), w.flat_idx ) )
			} ) )
	}

	/// What the app asked a text field to do, with the field focused first.
	fn apply_text_command( &mut self )
	{
		let Some( ( id, command ) ) = self.app.take_text_command() else { return };
		let Some( ( focus, idx ) ) = self.find_widget( id ) else { return };
		if !matches!( command, TextCommand::Reveal { .. } ) && self.surface( focus ).focused_idx != Some( idx )
		{
			self.set_focus( focus, Some( idx ) );
		}
		match command
		{
			TextCommand::Copy      => self.handle_copy( focus ),
			TextCommand::Cut       => self.handle_cut( focus ),
			TextCommand::Paste     => self.handle_paste( focus ),
			TextCommand::SelectAll => self.handle_select_all( focus ),
			TextCommand::Select { start, end } | TextCommand::Reveal { start, end } => self.select_text_range( focus, idx, start, end ),
		}
		if let Some( ss ) = self.try_surface_mut( focus ) { ss.request_redraw(); }
	}

	/// Select the bytes `start..end` of the text field at `idx`, focused or not, the ends taken back to the value and to its characters' starts.
	fn select_text_range( &mut self, focus: SurfaceFocus, idx: usize, start: usize, end: usize )
	{
		let ss    = self.surface( focus );
		let value = ss.pending_text_values.get( &idx ).cloned()
			.or_else( || find_handlers( &ss.frame.widget_rects, idx )?.current_value().map( str::to_string ) );
		let Some( value ) = value else { return };
		let boundary = |at: usize|
		{
			let mut at = at.min( value.len() );
			while !value.is_char_boundary( at ) { at -= 1; }
			at
		};
		let ( start, end ) = ( boundary( start ), boundary( end ) );
		let frame = &mut self.surface_mut( focus ).frame;
		frame.cursor_state.insert( idx, end );
		frame.selection_anchor.insert( idx, start );
	}

	/// What follows the system's word that the last frame of `focus` is on screen: an animation draws its next frame, or, when the app animates only by moving subsurfaces `focus` hosts, leaves the cadence to them. Under the software animation cap a skipped frame keeps the cadence with a bare frame request.
	pub( crate ) fn frame_presented( &mut self, focus: SurfaceFocus, hosts_subsurfaces: bool )
	{
		let animating = self.app.is_animating();
		match focus
		{
			SurfaceFocus::Main =>
			{
				self.main.frame_pending = false;
				if !animating
				{
					self.perf.animation_stopped();
					return;
				}
				if hosts_subsurfaces && self.app.subsurface_motion_only()
				{
					self.main.frame_tick_due = true;
					return;
				}
				let cap = self.app.cap_software_animation();
				if self.perf.animated_frame( crate::render::is_software_render(), cap )
				{
					self.view_dirty = true;
					self.main.request_redraw();
				} else if self.platform.request_frame( &self.main.platform, SurfaceFocus::Main ) {
					self.main.frame_pending = true;
				}
			}
			SurfaceFocus::Overlay( id ) =>
			{
				let motion_only = hosts_subsurfaces && self.app.subsurface_motion_only();
				let Some( ss ) = self.overlays.get_mut( &id ) else { return };
				ss.frame_pending = false;
				if !animating { return; }
				if motion_only
				{
					ss.frame_tick_due = true;
				} else {
					ss.request_redraw();
					self.overlays_dirty = true;
				}
			}
		}
	}

	/// Focus the widget the app asked for, on the main surface or any overlay. One not laid out yet, or clipped by a scroll viewport, is waited for over a few drawn frames.
	fn apply_focus_request( &mut self, drawn: bool )
	{
		let retry = self.focus_retry.take();
		let Some( ( id, frames_left ) ) = self.app.take_focus_request()
			.map( |id| ( id, FOCUS_RETRY_FRAMES ) )
			.or( retry )
		else { return };

		if let Some( ( focus, flat_idx ) ) = self.find_widget( id )
		{
			self.set_focus( focus, Some( flat_idx ) );
			if let Some( ss ) = self.try_surface_mut( focus ) { ss.request_redraw(); }
			return;
		}
		let frames_left = if drawn { frames_left.saturating_sub( 1 ) } else { frames_left };
		if frames_left > 0
		{
			self.reveal_clipped_widget( id );
			self.focus_retry = Some( ( id, frames_left ) );
			self.view_dirty  = true;
			self.main.request_redraw();
		}
	}

	/// Hand the queued messages to [`App::update`], folding their invalidation scopes into one, and re-sync the cursor the update may have changed.
	pub( crate ) fn process_messages( &mut self )
	{
		let msgs: Vec<_> = self.pending_msgs.drain( .. ).collect();
		if msgs.is_empty() { return; }
		let mut scope = InvalidationScope::Only( Vec::new() );
		for msg in msgs
		{
			scope = scope.union( self.app.invalidate_after( &msg ) );
			self.app.update( msg );
		}
		apply_invalidation( self, scope );
		// `update()` may have flipped the busy / loading flag
		// the app reads from inside `cursor_override`. Re-sync
		// the pointer cursor so the change propagates without
		// waiting for the next motion event.
		let pf = self.pointer_focus;
		self.dispatch_cursor_shape( pf );
	}

	/// Draw every surface that is configured, dirty and not waiting on a frame callback, rebuilding the cached view and overlays first when they are stale. Returns whether any surface was due.
	pub( crate ) fn draw_due( &mut self ) -> bool
	{
		let any_drawable = ( self.main.configured && self.main.needs_redraw && !self.main.frame_pending )
			|| self.overlays.values().any( |ss| ss.configured && ss.needs_redraw && !ss.frame_pending )
			|| self.secondaries.iter().any( |( _, ss )| ss.configured && ss.needs_redraw && !ss.frame_pending );
		if !any_drawable { return false; }

		// Rebuild while motion is in progress and on the first frame
		// after it ends, so the settle frame paints at full quality
		// instead of freezing one step short of the target.
		// `wants_low_quality_paint` is the source of truth so
		// finger-tracked drags get the same treatment. Slider /
		// scroll drags are owned by the runtime gesture machine,
		// not by `App::update`, so OR with the gesture state to
		// pick up that motion signal too.
		let runtime_slider_motion =
			self.main.gesture.dragging_slider.is_some()
			|| self.overlays.values().any( |ss| ss.gesture.dragging_slider.is_some() );
		if runtime_slider_motion
		{
			self.view_dirty     = true;
			self.overlays_dirty = true;
		}
		if self.view_dirty
		{
			self.cached_view = Some( self.app.view() );
			self.view_dirty  = false;
		}
		if self.overlays_dirty
		{
			let mut specs = self.app.overlays();
			if let Some( ts ) = self.tooltip_overlay() { specs.push( ts ); }
			self.cached_overlays = Some( specs );
			self.overlays_dirty  = false;
		}
		if draw_frame( self )
		{
			self.app.on_first_frame_committed();
		}
		true
	}
}