1use std::{
6 cell::{Cell, RefCell},
7 collections::{HashMap, HashSet, VecDeque},
8 rc::Rc,
9 sync::Arc,
10 time::{Duration, Instant},
11};
12
13#[cfg(feature = "robot")]
14use cranpose_app_shell::PointerSource;
15use cranpose_app_shell::{
16 AppShell, FramePacingMode, FrameUpdateResult, RootId, SurfaceMut, default_root_key,
17};
18use cranpose_platform_desktop_winit::DesktopWinitPlatform;
19use cranpose_render_common::Renderer;
20use cranpose_render_wgpu::{WgpuRenderer, WgpuTextSystem};
21use winit::{
22 application::ApplicationHandler,
23 dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Position},
24 event::{ButtonSource, ElementState, MouseButton, WindowEvent},
25 event_loop::{ActiveEventLoop, ControlFlow, EventLoop, EventLoopProxy},
26 icon::{Icon, RgbaIcon},
27 window::{ResizeDirection, Window, WindowAttributes, WindowId as WinitWindowId, WindowLevel},
28};
29
30#[cfg(feature = "robot")]
31use crate::robot::{
32 Robot, RobotChannel, RobotCommand, RobotIdlePending, RobotResponse, RobotScreenshot,
33 RobotTimelineAction, char_to_key_code, extract_semantics, find_button_in_app, find_text_in_app,
34 panic_payload_message, robot_key_code_and_text, robot_wait_for_idle_animation_loop_only,
35};
36use crate::{
37 app_launcher::{AppSettings, LaunchError},
38 desktop_input::{app_modifiers, dispatch_keyboard_input},
39 native_window::{
40 self, NativeWindowEvents, NativeWindowKey, NativeWindowOptions, NativeWindowPositionOrigin,
41 NativeWindowRequest, WindowFocus, WindowResizeDirection, WindowState,
42 },
43 wgpu_surface::{
44 SurfaceFrame, current_surface_texture, present_initial_placeholder_frame,
45 present_initial_placeholder_frame_cleared_to, surface_present_required,
46 },
47 winit_pointer::{
48 is_primary_pointer_button, pointer_source_from_button, pointer_source_from_winit,
49 },
50};
51
52const NATIVE_WINDOW_DRAG_POLL_INTERVAL: Duration = Duration::from_millis(16);
53const NATIVE_WINDOW_GLOBAL_POINTER_POLLED: bool = cfg!(all(
54 target_os = "linux",
55 not(target_arch = "wasm32"),
56 feature = "desktop-x11"
57));
58const NATIVE_WINDOW_POSITION_POLL_INTERVAL: Duration = Duration::from_millis(16);
59const NATIVE_WINDOW_POSITION_SETTLE_TIMEOUT: Duration = Duration::from_millis(36);
60const NATIVE_WINDOW_POSITION_SETTLE_POLL: Duration = Duration::from_millis(1);
61const NATIVE_WINDOW_PLACEMENT_MARGIN: f32 = 32.0;
62#[cfg(feature = "robot")]
63const ROBOT_IDLE_TIMEOUT: Duration = Duration::from_secs(10);
64#[cfg(feature = "robot")]
65const ROBOT_IDLE_MIN_ITERATIONS: u32 = 100;
66#[cfg(feature = "robot")]
67const ROBOT_IDLE_PRESENT_TIMEOUT: Duration = Duration::from_secs(30);
68#[cfg(feature = "robot")]
69const ROBOT_IDLE_STARVATION_CEILING: Duration = Duration::from_secs(60);
70#[cfg(feature = "robot")]
71const ROBOT_PUMP_FRAME_INTERVAL: Duration = Duration::from_nanos(16_666_667);
72#[cfg(feature = "robot")]
73const ROBOT_PRESENT_WAIT_TIMEOUT: Duration = Duration::from_secs(5);
74const DEFAULT_DESKTOP_FRAME_TELEMETRY_THRESHOLD_MS: f64 = 4.0;
75
76#[cfg(feature = "robot")]
77use std::sync::mpsc;
78
79macro_rules! trace_when {
80 ($enabled:expr, $sink:ident, $($arg:tt)*) => {
81 if $enabled {
82 $sink(format_args!($($arg)*));
83 }
84 };
85}
86
87macro_rules! trace_native_window {
88 ($($arg:tt)*) => {
89 trace_when!(native_window_trace_enabled(), print_native_window_trace, $($arg)*)
90 };
91}
92
93macro_rules! trace_native_window_timing {
94 ($($arg:tt)*) => {
95 trace_when!(native_window_timing_enabled(), print_native_window_timing, $($arg)*)
96 };
97}
98
99fn update_app_with_native_window_registry(
100 app: &mut AppShell<WgpuRenderer>,
101 registry: &Rc<native_window::NativeWindowRegistry>,
102) -> FrameUpdateResult {
103 native_window::with_native_window_registry(registry, || app.update())
104}
105
106fn defer_window_redraw(
107 event_loop: &dyn ActiveEventLoop,
108 frames_enabled: bool,
109 frame_cap_deadline: Option<Instant>,
110) -> bool {
111 if !frames_enabled {
112 return true;
113 }
114 if let Some(deadline) = frame_cap_deadline
115 && deadline > Instant::now()
116 {
117 event_loop.set_control_flow(ControlFlow::WaitUntil(deadline));
118 return true;
119 }
120 false
121}
122
123fn desktop_frame_telemetry_threshold_ms() -> Option<f64> {
124 static THRESHOLD_MS: std::sync::OnceLock<Option<f64>> = std::sync::OnceLock::new();
125 *THRESHOLD_MS.get_or_init(|| {
126 let explicit = std::env::var("CRANPOSE_DESKTOP_FRAME_TELEMETRY_MS")
127 .ok()
128 .and_then(|value| value.parse::<f64>().ok())
129 .filter(|value| value.is_finite() && *value >= 0.0);
130 explicit.or_else(|| {
131 std::env::var_os("CRANPOSE_DESKTOP_FRAME_TELEMETRY")
132 .is_some()
133 .then_some(DEFAULT_DESKTOP_FRAME_TELEMETRY_THRESHOLD_MS)
134 })
135 })
136}
137
138fn log_desktop_frame_telemetry(
139 frame_start: Instant,
140 after_update: Instant,
141 after_acquire: Instant,
142 after_render: Instant,
143 after_present: Instant,
144 label: &str,
145) {
146 let Some(threshold_ms) = desktop_frame_telemetry_threshold_ms() else {
147 return;
148 };
149
150 let total_ms = after_present.duration_since(frame_start).as_secs_f64() * 1000.0;
151 if total_ms < threshold_ms {
152 return;
153 }
154
155 let update_ms = after_update.duration_since(frame_start).as_secs_f64() * 1000.0;
156 let acquire_ms = after_acquire.duration_since(after_update).as_secs_f64() * 1000.0;
157 let render_ms = after_render.duration_since(after_acquire).as_secs_f64() * 1000.0;
158 let present_ms = after_present.duration_since(after_render).as_secs_f64() * 1000.0;
159 log::warn!(
160 "[desktop-frame-telemetry:{label}] total_ms={total_ms:.2} update_ms={update_ms:.2} acquire_ms={acquire_ms:.2} render_ms={render_ms:.2} present_ms={present_ms:.2}",
161 );
162}
163
164#[cfg(feature = "robot")]
165fn robot_tree_response(app: &mut AppShell<WgpuRenderer>, command: &RobotCommand) -> RobotResponse {
166 match command {
167 RobotCommand::GetInspectorState => {
168 RobotResponse::InspectorState(Box::new(app.inspector_state().clone()))
169 }
170 RobotCommand::GetSpokenTree => {
171 RobotResponse::SpokenTree(crate::accessibility::spoken_tree(app))
172 }
173 RobotCommand::AuditAccessibility => audit_response(app),
174 _ => RobotResponse::Semantics(extract_semantics(app)),
175 }
176}
177
178#[cfg(feature = "robot")]
179fn audit_response(app: &mut AppShell<WgpuRenderer>) -> RobotResponse {
180 match cranpose_app_shell::placed_semantics::placed_semantics_from_shell(app) {
181 Some(placed) => RobotResponse::AccessibilityIssues(
182 cranpose_app_shell::accessibility_audit::audit_accessibility(&placed)
183 .iter()
184 .map(ToString::to_string)
185 .collect(),
186 ),
187 None => RobotResponse::Error(
188 "the app has no laid out semantics tree yet; wait for a frame first".to_string(),
189 ),
190 }
191}
192
193#[cfg(feature = "robot")]
194fn pump_robot_frame(
195 app: &mut AppShell<WgpuRenderer>,
196 registry: &Rc<native_window::NativeWindowRegistry>,
197) -> FrameUpdateResult {
198 let mut result = FrameUpdateResult::default();
199 for _ in 0..3 {
200 if !robot_query_should_drain_frame(app) {
201 break;
202 }
203 let frame_result = update_app_with_native_window_registry(app, registry);
204 result.visual_changed |= frame_result.visual_changed;
205 result.structure_changed |= frame_result.structure_changed;
206 }
207 result
208}
209
210#[cfg(feature = "robot")]
211fn robot_query_should_drain_frame(app: &AppShell<WgpuRenderer>) -> bool {
212 app.needs_redraw()
213}
214
215#[cfg(feature = "robot")]
216fn robot_query_visual_dirty(update_result: FrameUpdateResult, needs_redraw: bool) -> bool {
217 update_result.visual_changed || needs_redraw
218}
219
220#[cfg(feature = "robot")]
221struct RobotController {
222 rx: mpsc::Receiver<RobotCommand>,
223 tx: mpsc::Sender<RobotResponse>,
224 pending_command: Option<RobotCommand>,
225 waiting_for_idle: bool,
226 idle_started_at: Option<Instant>,
227 idle_iterations: u32,
228 idle_structure_clean_frames: u32,
229 idle_present_blocked_since: Option<Instant>,
230 waiting_for_present_generation: Option<u64>,
231 waiting_for_pump_present_generation: Option<u64>,
232 pump_present_started_at: Option<Instant>,
233 scroll_sequence: Option<RobotScrollSequence>,
234 synthetic_primary_down: bool,
235}
236
237#[cfg(feature = "robot")]
238#[derive(Clone, Copy, Debug)]
239struct RobotScrollSequence {
240 delta_x: f32,
241 delta_y: f32,
242 remaining: u32,
243}
244
245#[cfg(feature = "robot")]
246#[derive(Clone, Copy, Debug, Eq, PartialEq)]
247enum IdleWaitTimeout {
248 AppNotConverging,
249 SurfaceNotPresenting,
250 HostStarved,
251}
252
253#[cfg(feature = "robot")]
254impl RobotController {
255 fn new(wake_event_loop: impl Fn() + Send + Sync + 'static) -> (Self, Robot) {
256 let (channel, robot) = RobotChannel::new(wake_event_loop);
257 let RobotChannel { rx, tx } = channel;
258
259 let controller = RobotController {
260 rx,
261 tx,
262 pending_command: None,
263 waiting_for_idle: false,
264 idle_started_at: None,
265 idle_iterations: 0,
266 idle_structure_clean_frames: 0,
267 idle_present_blocked_since: None,
268 waiting_for_present_generation: None,
269 waiting_for_pump_present_generation: None,
270 pump_present_started_at: None,
271 scroll_sequence: None,
272 synthetic_primary_down: false,
273 };
274
275 (controller, robot)
276 }
277
278 fn begin_synthetic_primary_gesture(&mut self) {
279 self.synthetic_primary_down = true;
280 }
281
282 fn end_synthetic_primary_gesture(&mut self) {
283 self.synthetic_primary_down = false;
284 }
285
286 fn synthetic_primary_down(&self) -> bool {
287 self.synthetic_primary_down
288 }
289
290 fn start_idle_wait(&mut self) {
291 self.start_idle_wait_at(Instant::now());
292 }
293
294 fn start_idle_wait_at(&mut self, started_at: Instant) {
295 self.waiting_for_idle = true;
296 self.idle_started_at = Some(started_at);
297 self.idle_iterations = 0;
298 self.idle_structure_clean_frames = 0;
299 self.idle_present_blocked_since = None;
300 }
301
302 fn observe_idle_present_block(&mut self, present_is_sole_blocker: bool, now: Instant) {
303 if !present_is_sole_blocker {
304 self.idle_present_blocked_since = None;
305 } else if self.idle_present_blocked_since.is_none() {
306 self.idle_present_blocked_since = Some(now);
307 }
308 }
309
310 fn idle_wait_timeout(&self, now: Instant) -> Option<IdleWaitTimeout> {
311 let started_at = self.idle_started_at?;
312 if let Some(blocked_since) = self.idle_present_blocked_since {
313 let blocked_for = now.saturating_duration_since(blocked_since);
314 return (blocked_for >= ROBOT_IDLE_PRESENT_TIMEOUT)
315 .then_some(IdleWaitTimeout::SurfaceNotPresenting);
316 }
317 let elapsed = now.saturating_duration_since(started_at);
318 if elapsed >= ROBOT_IDLE_TIMEOUT && self.idle_iterations >= ROBOT_IDLE_MIN_ITERATIONS {
319 return Some(IdleWaitTimeout::AppNotConverging);
320 }
321 if elapsed >= ROBOT_IDLE_STARVATION_CEILING {
322 return Some(IdleWaitTimeout::HostStarved);
323 }
324 None
325 }
326
327 fn begin_pump_present_wait(&mut self, target_generation: u64) {
328 self.begin_pump_present_wait_at(target_generation, Instant::now());
329 }
330
331 fn begin_pump_present_wait_at(&mut self, target_generation: u64, started_at: Instant) {
332 self.waiting_for_pump_present_generation = Some(target_generation);
333 self.pump_present_started_at = Some(started_at);
334 }
335
336 fn finish_pump_present_wait(&mut self) {
337 self.waiting_for_pump_present_generation = None;
338 self.pump_present_started_at = None;
339 }
340
341 fn pump_present_wait_timed_out(&self, now: Instant) -> bool {
342 self.pump_present_started_at.is_some_and(|started_at| {
343 now.saturating_duration_since(started_at) >= ROBOT_PRESENT_WAIT_TIMEOUT
344 })
345 }
346
347 fn stage_pending_command(&mut self) -> bool {
348 if self.pending_command.is_none() {
349 self.pending_command = self.rx.try_recv().ok();
350 }
351 self.pending_command.is_some()
352 }
353
354 fn next_command(&mut self) -> Option<RobotCommand> {
355 self.pending_command
356 .take()
357 .or_else(|| self.rx.try_recv().ok())
358 }
359
360 fn finish_idle_wait(&mut self) {
361 self.waiting_for_idle = false;
362 self.idle_started_at = None;
363 self.waiting_for_present_generation = None;
364 self.idle_structure_clean_frames = 0;
365 self.idle_present_blocked_since = None;
366 }
367
368 fn awaiting_progress(&mut self) -> bool {
369 self.waiting_for_idle
370 || self.waiting_for_present_generation.is_some()
371 || self.waiting_for_pump_present_generation.is_some()
372 || self.scroll_sequence.is_some()
373 || self.synthetic_primary_down
374 || self.stage_pending_command()
375 }
376
377 fn record_idle_update_result(&mut self, result: FrameUpdateResult) {
378 if !self.waiting_for_idle {
379 return;
380 }
381 if result.structure_changed {
382 self.idle_structure_clean_frames = 0;
383 } else if result.visual_changed {
384 self.idle_structure_clean_frames = self.idle_structure_clean_frames.saturating_add(1);
385 }
386 }
387}
388
389struct DesktopGpuContext {
390 gpu: crate::GpuContext,
391 adapter_backend: wgpu::Backend,
392 text_system: WgpuTextSystem,
393}
394
395struct NativeWindowSurface {
396 key: NativeWindowKey,
397 revision: u64,
398 options: NativeWindowOptions,
399 events: NativeWindowEvents,
400 state: Option<WindowState>,
401 window: Arc<dyn Window>,
402 surface: wgpu::Surface<'static>,
403 surface_config: wgpu::SurfaceConfiguration,
404 surface_caps: wgpu::SurfaceCapabilities,
405 surface_dirty: bool,
406 occluded: bool,
407 minimized: bool,
408 root: native_window::NativeWindowRootHandle,
409 platform: DesktopWinitPlatform,
410 last_cursor_position: Option<(f32, f32)>,
411 last_cursor_physical_position: Option<PhysicalPosition<f64>>,
412 last_frame_start_time: Option<Instant>,
413 last_redraw_empty: bool,
414 vsync_interval: Duration,
415 pending_outer_positions: PendingNativeWindowPositions,
416 active_drag: Option<NativeWindowPollingDragSession>,
417 held_press: Option<PhysicalPosition<f64>>,
418}
419
420struct NativeWindowShell {
421 request: NativeWindowRequest,
422 window: Arc<dyn Window>,
423 create_started: Instant,
424}
425
426#[derive(Default)]
427struct NativeWindowEventSettlement {
428 sync_after_event: bool,
429 drag_move: Option<(NativeWindowKey, cranpose_ui::Point)>,
430 finish_drag: bool,
431}
432
433impl NativeWindowEventSettlement {
434 fn also(mut self, other: Self) -> Self {
435 self.sync_after_event |= other.sync_after_event;
436 self.drag_move = self.drag_move.or(other.drag_move);
437 self.finish_drag |= other.finish_drag;
438 self
439 }
440}
441
442#[derive(Clone, Copy, Debug, PartialEq)]
443struct DesktopRect {
444 x: f32,
445 y: f32,
446 width: f32,
447 height: f32,
448}
449
450impl DesktopRect {
451 fn right(self) -> f32 {
452 self.x + self.width
453 }
454
455 fn bottom(self) -> f32 {
456 self.y + self.height
457 }
458
459 fn center(self) -> cranpose_ui::Point {
460 cranpose_ui::Point::new(self.x + self.width / 2.0, self.y + self.height / 2.0)
461 }
462
463 fn contains_rect_with_margin(self, rect: Self, margin: f32) -> bool {
464 rect.x >= self.x + margin
465 && rect.right() <= self.right() - margin
466 && rect.y >= self.y + margin
467 && rect.bottom() <= self.bottom() - margin
468 }
469
470 fn distance_to_point(self, point: cranpose_ui::Point) -> f32 {
471 let dx = if point.x < self.x {
472 self.x - point.x
473 } else if point.x > self.right() {
474 point.x - self.right()
475 } else {
476 0.0
477 };
478 let dy = if point.y < self.y {
479 self.y - point.y
480 } else if point.y > self.bottom() {
481 point.y - self.bottom()
482 } else {
483 0.0
484 };
485 dx * dx + dy * dy
486 }
487}
488
489#[derive(Clone, Copy, Debug)]
490struct NativeWindowPollingDragSession {
491 start_pointer_screen: PhysicalPosition<f64>,
492 start_window_outer: PhysicalPosition<i32>,
493 last_target_outer: PhysicalPosition<i32>,
494 next_poll_at: Instant,
495}
496
497struct NativeWindowPositionRequest {
498 window: Arc<dyn Window>,
499 logical: LogicalPosition<f64>,
500 physical: PhysicalPosition<i32>,
501}
502
503#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
504enum NativeWindowPositionApplyMode {
505 #[default]
506 WaitForSettle,
507 FlushOnly,
508}
509
510#[derive(Clone, Copy, Debug, Eq, PartialEq)]
511enum NativeWindowPositionObservation {
512 Current,
513 Superseded,
514 External,
515}
516
517impl NativeWindowPollingDragSession {
518 fn new(
519 start_pointer_screen: PhysicalPosition<f64>,
520 start_window_outer: PhysicalPosition<i32>,
521 now: Instant,
522 ) -> Self {
523 Self {
524 start_pointer_screen,
525 start_window_outer,
526 last_target_outer: start_window_outer,
527 next_poll_at: now + NATIVE_WINDOW_DRAG_POLL_INTERVAL,
528 }
529 }
530
531 fn target_for_pointer(&self, pointer: PhysicalPosition<f64>) -> PhysicalPosition<i32> {
532 PhysicalPosition::new(
533 (self.start_window_outer.x as f64 + pointer.x - self.start_pointer_screen.x).round()
534 as i32,
535 (self.start_window_outer.y as f64 + pointer.y - self.start_pointer_screen.y).round()
536 as i32,
537 )
538 }
539}
540
541#[derive(Default)]
542struct PendingNativeWindowPositions {
543 positions: VecDeque<(f32, f32)>,
544}
545
546impl PendingNativeWindowPositions {
547 fn push(&mut self, position: (f32, f32)) {
548 if self
549 .positions
550 .back()
551 .is_some_and(|pending| native_window_positions_close(*pending, position))
552 {
553 return;
554 }
555 self.positions.push_back(position);
556 while self.positions.len() > 16 {
557 self.positions.pop_front();
558 }
559 }
560
561 fn acknowledge(&mut self, position: (f32, f32)) -> bool {
562 let Some(index) = self
563 .positions
564 .iter()
565 .position(|pending| native_window_positions_close(*pending, position))
566 else {
567 return false;
568 };
569 for _ in 0..=index {
570 self.positions.pop_front();
571 }
572 true
573 }
574
575 fn acknowledge_or_matches_known(
576 &mut self,
577 position: (f32, f32),
578 known_position: Option<(f32, f32)>,
579 ) -> NativeWindowPositionObservation {
580 if self.acknowledge(position) {
581 return if self.has_pending() {
582 NativeWindowPositionObservation::Superseded
583 } else {
584 NativeWindowPositionObservation::Current
585 };
586 }
587 if known_position.is_some_and(|known| native_window_positions_close(known, position)) {
588 self.clear();
589 return NativeWindowPositionObservation::Current;
590 }
591 NativeWindowPositionObservation::External
592 }
593
594 fn clear(&mut self) {
595 self.positions.clear();
596 }
597
598 fn has_pending(&self) -> bool {
599 !self.positions.is_empty()
600 }
601}
602
603impl NativeWindowSurface {
604 fn presentable(&self) -> bool {
605 self.options.visible && !self.occluded && !self.minimized
606 }
607
608 fn frame_interval(&self, mode: FramePacingMode) -> Option<Duration> {
609 frame_cap_interval(mode, self.vsync_interval, self.last_redraw_empty)
610 }
611
612 fn root_id(&self) -> RootId {
613 RootId::Window(self.key.raw())
614 }
615}
616
617fn native_surface<'a>(
618 app: &'a mut AppShell<WgpuRenderer>,
619 native: &NativeWindowSurface,
620) -> Option<SurfaceMut<'a, WgpuRenderer>> {
621 app.surface(native.root_id())
622}
623
624struct App {
625 #[cfg(feature = "webview")]
626 webviews: crate::webview_host::WebViews<wry::WebView>,
627 #[cfg(feature = "webview")]
628 webview_wake: Rc<dyn Fn()>,
629 settings: AppSettings,
630 platform_env: Rc<crate::platform_env::PlatformEnvironment>,
631 content: Option<Box<dyn FnMut()>>,
632 window: Option<Arc<dyn Window>>,
633 surface: Option<wgpu::Surface<'static>>,
634 surface_config: Option<wgpu::SurfaceConfiguration>,
635 surface_caps: Option<wgpu::SurfaceCapabilities>,
636 app: Option<AppShell<WgpuRenderer>>,
637 accessibility: Option<crate::desktop_accessibility::DesktopAccessibilityBridge>,
638 platform: Option<DesktopWinitPlatform>,
639 gpu_context: Option<DesktopGpuContext>,
640 native_windows: HashMap<WinitWindowId, NativeWindowSurface>,
641 native_window_registry: Rc<native_window::NativeWindowRegistry>,
642 native_window_ids: HashMap<NativeWindowKey, WinitWindowId>,
643 native_window_positions: HashMap<NativeWindowKey, (f32, f32)>,
644 closed_native_windows: HashSet<NativeWindowKey>,
645 next_native_window_position_poll_at: Instant,
646 native_window_platform_probe: NativeWindowPlatformProbe,
647 native_global_primary_down: bool,
648 primary_held_press: Option<PhysicalPosition<f64>>,
649 handed_press: Option<HandedPress>,
650 primary_wrap_size: Option<(u32, u32)>,
651 cursors: crate::desktop_cursor::DesktopCursors,
652 current_modifiers: winit::keyboard::ModifiersState,
653 last_cursor_position: Option<(f32, f32)>,
654 primary_shown: Arc<std::sync::atomic::AtomicBool>,
655 primary_presence: Option<crate::desktop_lifecycle::WindowPresence>,
656 #[cfg(feature = "robot")]
657 robot_controller: Option<RobotController>,
658 #[cfg(feature = "robot")]
659 robot_app_hook: Option<Box<crate::RobotAppHook>>,
660 recorder: Option<crate::recorder::InputRecorder>,
661 launch_error: Rc<RefCell<Option<LaunchError>>>,
662 event_proxy: EventLoopProxy,
663 applied_frame_pacing_mode: FramePacingMode,
664 last_frame_start_time: Option<Instant>,
665 last_redraw_empty: bool,
666 primary_redraw_pending: bool,
667 primary_surface_dirty: bool,
668 primary_initial_present_pending: bool,
669 vsync_interval: Duration,
670 exiting: bool,
671 file_drops: crate::desktop_incoming::FileDrops,
672 #[cfg(feature = "robot")]
673 presented_frame_generation: u64,
674 #[cfg(feature = "robot")]
675 unpresentable_frames_since_present: u32,
676 #[cfg(feature = "robot")]
677 robot_visible_surface_dirty: bool,
678}
679
680impl App {
681 fn new(
682 mut settings: AppSettings,
683 content: impl FnMut() + 'static,
684 launch_error: Rc<RefCell<Option<LaunchError>>>,
685 event_proxy: EventLoopProxy,
686 ) -> Self {
687 let recorder = settings
688 .record_to
689 .take()
690 .map(crate::recorder::InputRecorder::new);
691 #[cfg(feature = "robot")]
692 let robot_app_hook = settings.robot_app_hook.take();
693 let applied_frame_pacing_mode = settings.frame_pacing_mode;
694 let custom_cursor_size = settings.custom_cursor_size;
695
696 let platform_env = crate::platform_env::PlatformEnvironment::new();
697 let env_for_content = Rc::clone(&platform_env);
698 let mut content = content;
699 let content = move || env_for_content.compose_root(&mut content);
700
701 Self {
702 #[cfg(feature = "webview")]
703 webviews: Default::default(),
704 #[cfg(feature = "webview")]
705 webview_wake: crate::wry_webview::wake_callback(&event_proxy),
706 settings,
707 platform_env,
708 content: Some(Box::new(content)),
709 window: None,
710 surface: None,
711 surface_config: None,
712 surface_caps: None,
713 app: None,
714 accessibility: None,
715 platform: None,
716 gpu_context: None,
717 native_windows: HashMap::new(),
718 native_window_registry: Rc::new(native_window::NativeWindowRegistry::default()),
719 native_window_ids: HashMap::new(),
720 native_window_positions: HashMap::new(),
721 closed_native_windows: HashSet::new(),
722 next_native_window_position_poll_at: Instant::now()
723 + NATIVE_WINDOW_POSITION_POLL_INTERVAL,
724 #[allow(clippy::default_constructed_unit_structs)]
725 native_window_platform_probe: NativeWindowPlatformProbe::default(),
726 native_global_primary_down: false,
727 primary_held_press: None,
728 handed_press: None,
729 primary_wrap_size: None,
730 cursors: crate::desktop_cursor::DesktopCursors::new(custom_cursor_size),
731 current_modifiers: winit::keyboard::ModifiersState::empty(),
732 last_cursor_position: None,
733 primary_shown: Arc::new(std::sync::atomic::AtomicBool::new(false)),
734 primary_presence: None,
735 #[cfg(feature = "robot")]
736 robot_controller: None,
737 #[cfg(feature = "robot")]
738 robot_app_hook,
739 recorder,
740 launch_error,
741 event_proxy,
742 applied_frame_pacing_mode,
743 last_frame_start_time: None,
744 last_redraw_empty: false,
745 primary_redraw_pending: false,
746 primary_surface_dirty: false,
747 primary_initial_present_pending: false,
748 vsync_interval: default_vsync_interval(),
749 exiting: false,
750 file_drops: crate::desktop_incoming::FileDrops::default(),
751 #[cfg(feature = "robot")]
752 presented_frame_generation: 0,
753 #[cfg(feature = "robot")]
754 unpresentable_frames_since_present: 0,
755 #[cfg(feature = "robot")]
756 robot_visible_surface_dirty: false,
757 }
758 }
759
760 fn abort_launch(&self, event_loop: &dyn ActiveEventLoop, error: LaunchError) {
761 let mut slot = self.launch_error.borrow_mut();
762 if slot.is_none() {
763 *slot = Some(error);
764 }
765 event_loop.exit();
766 }
767
768 fn frame_pacing_mode(&self) -> FramePacingMode {
769 self.app
770 .as_ref()
771 .map_or(self.settings.frame_pacing_mode, |app| {
772 app.frame_pacing_mode()
773 })
774 }
775
776 fn frame_interval(&self) -> Option<Duration> {
777 frame_cap_interval(
778 self.frame_pacing_mode(),
779 self.vsync_interval,
780 self.last_redraw_empty,
781 )
782 }
783
784 fn sync_frame_pacing(&mut self) {
785 let mode = self.frame_pacing_mode();
786 if mode == self.applied_frame_pacing_mode {
787 return;
788 }
789 self.applied_frame_pacing_mode = mode;
790
791 if let Some(app) = &mut self.app {
792 app.set_frame_pacing_mode(mode);
793 }
794 let Some(device) = self
795 .gpu_context
796 .as_ref()
797 .map(|context| Arc::clone(&context.gpu.device))
798 else {
799 return;
800 };
801 if let (Some(surface), Some(surface_config)) = (&self.surface, &mut self.surface_config) {
802 apply_frame_pacing_mode(
803 &device,
804 surface,
805 surface_config,
806 self.surface_caps.as_ref(),
807 mode,
808 self.vsync_interval,
809 );
810 }
811 self.last_frame_start_time = None;
812 if let Some(window) = self.window.clone() {
813 request_redraw_once(&window, &mut self.primary_redraw_pending);
814 }
815
816 for native in self.native_windows.values_mut() {
817 apply_frame_pacing_mode(
818 &device,
819 &native.surface,
820 &mut native.surface_config,
821 Some(&native.surface_caps),
822 mode,
823 native.vsync_interval,
824 );
825 native.last_frame_start_time = None;
826 native.window.request_redraw();
827 }
828 }
829
830 fn set_native_transparency(
831 app: &mut AppShell<WgpuRenderer>,
832 native: &mut NativeWindowSurface,
833 transparent: bool,
834 ) {
835 native.window.set_transparent(transparent);
836 if let Some(mut surface) = native_surface(app, native) {
837 surface.renderer().set_transparent_background(transparent);
838 }
839 }
840
841 fn refresh_native_window_requests(&mut self) {
842 let registry = Rc::clone(&self.native_window_registry);
843 let frames_enabled = self.frames_enabled();
844 if let Some(app) = &mut self.app {
845 if frames_enabled {
846 update_app_with_native_window_registry(app, ®istry);
847 } else {
848 native_window::with_native_window_registry(®istry, || {
849 app.update_without_frame()
850 });
851 }
852 }
853 }
854
855 fn refresh_and_sync_native_windows(&mut self, event_loop: &dyn ActiveEventLoop) {
856 self.refresh_native_window_requests();
857 self.sync_native_windows(event_loop);
858 }
859
860 fn handle_primary_frame_requested(&mut self, event_loop: &dyn ActiveEventLoop) {
861 if !self.primary_frames_enabled() {
862 return;
863 }
864 let registry = Rc::clone(&self.native_window_registry);
865 let frame_interval = self.frame_interval();
866 let waiting_for_frame_cap = self
867 .last_frame_start_time
868 .and_then(|started_at| frame_interval.map(|interval| started_at + interval))
869 .is_some_and(|deadline| deadline > Instant::now());
870 let primary_visible = self.primary_visible();
871 let direct_declaration_update = {
872 let Some(app) = &mut self.app else {
873 return;
874 };
875 let needs_redraw = app.frame_schedule().needs_frame;
876 if !needs_redraw {
877 return;
878 }
879 let direct_declaration_update = primary_declaration_host_needs_direct_update(
880 primary_visible,
881 self.settings.headless,
882 needs_redraw,
883 waiting_for_frame_cap,
884 );
885 if direct_declaration_update {
886 trace_native_window!(
887 "primary declaration host proxy update visible={} headless={}",
888 primary_visible,
889 self.settings.headless
890 );
891 let update_result = update_declaration_host_frame(
892 app,
893 ®istry,
894 &mut self.last_frame_start_time,
895 frame_interval,
896 );
897 #[cfg(feature = "robot")]
898 if let Some(controller) = &mut self.robot_controller {
899 controller.record_idle_update_result(update_result);
900 }
901 #[cfg(not(feature = "robot"))]
902 let _ = update_result;
903 }
904 direct_declaration_update
905 };
906
907 if direct_declaration_update {
908 self.sync_native_windows(event_loop);
909 } else if waiting_for_frame_cap {
910 } else if let Some(window) = &self.window {
911 request_redraw_once(window, &mut self.primary_redraw_pending);
912 }
913 }
914
915 fn poll_primary_pointer_gesture(&mut self) -> bool {
916 let active = self
917 .app
918 .as_ref()
919 .is_some_and(AppShell::has_active_pointer_gesture);
920 #[cfg(feature = "robot")]
921 let synthetic_primary_down = self
922 .robot_controller
923 .as_ref()
924 .is_some_and(RobotController::synthetic_primary_down);
925 #[cfg(not(feature = "robot"))]
926 let synthetic_primary_down = false;
927
928 let action = primary_pointer_gesture_poll_action(
929 active,
930 synthetic_primary_down,
931 native_window_global_pointer_state(&self.native_window_platform_probe),
932 );
933
934 let (Some(window), Some(platform), Some(app)) =
935 (&self.window, &self.platform, self.app.as_mut())
936 else {
937 return false;
938 };
939
940 let pointer = match action {
941 PrimaryPointerGesturePollAction::Inactive => return false,
942 PrimaryPointerGesturePollAction::ReleaseAt(position) => {
943 let event_time = app.realtime_pointer_event_time(None);
944 let cursor_dirty = native_window_local_pointer_physical(
945 &self.native_window_platform_probe,
946 window,
947 position,
948 )
949 .is_some_and(|local| {
950 let logical = platform.pointer_position(local);
951 self.last_cursor_position = Some((logical.x, logical.y));
952 app.set_cursor_at_event_time(logical.x, logical.y, event_time)
953 });
954 let released = app.pointer_released_at_event_time(event_time);
955 let handled = cursor_dirty || released;
956 if handled {
957 self.last_frame_start_time = None;
958 request_redraw_once(window, &mut self.primary_redraw_pending);
959 }
960 return handled;
961 }
962 PrimaryPointerGesturePollAction::Pressed(pointer) => pointer,
963 };
964
965 if self.last_cursor_position.is_some() {
966 return false;
967 }
968
969 let Some(local) = native_window_local_pointer_physical(
970 &self.native_window_platform_probe,
971 window,
972 pointer.position,
973 ) else {
974 return false;
975 };
976 let logical = platform.pointer_position(local);
977 self.last_cursor_position = Some((logical.x, logical.y));
978 if app.set_cursor(logical.x, logical.y) {
979 self.last_frame_start_time = None;
980 request_redraw_once(window, &mut self.primary_redraw_pending);
981 return true;
982 }
983
984 false
985 }
986
987 fn refresh_primary_cursor_from_platform_pointer(
988 platform_probe: &NativeWindowPlatformProbe,
989 window: &Arc<dyn Window>,
990 platform: &DesktopWinitPlatform,
991 app: &mut AppShell<WgpuRenderer>,
992 last_cursor_position: &mut Option<(f32, f32)>,
993 ) -> bool {
994 let Some(pointer) = native_window_global_pointer_state(platform_probe) else {
995 return false;
996 };
997
998 let Some(local) =
999 native_window_local_pointer_physical(platform_probe, window, pointer.position)
1000 else {
1001 return false;
1002 };
1003
1004 let logical = platform.pointer_position(local);
1005 *last_cursor_position = Some((logical.x, logical.y));
1006 app.set_cursor(logical.x, logical.y)
1007 }
1008
1009 fn refresh_native_cursor_from_platform_pointer(
1010 platform_probe: &NativeWindowPlatformProbe,
1011 app: &mut AppShell<WgpuRenderer>,
1012 native: &mut NativeWindowSurface,
1013 ) -> bool {
1014 let Some(pointer) = native_window_global_pointer_state(platform_probe) else {
1015 return false;
1016 };
1017
1018 let Some(local) =
1019 native_window_local_pointer_physical(platform_probe, &native.window, pointer.position)
1020 else {
1021 return false;
1022 };
1023
1024 let logical = native.platform.pointer_position(local);
1025 native.last_cursor_position = Some((logical.x, logical.y));
1026 native.last_cursor_physical_position = Some(local);
1027 let Some(mut surface) = native_surface(app, native) else {
1028 return false;
1029 };
1030 surface.set_screen_origin(native_window_surface_origin(platform_probe, &native.window));
1031 surface.set_cursor(logical.x, logical.y)
1032 }
1033
1034 fn track_pointer_for_cursors(&mut self, window_id: WinitWindowId, event: &WindowEvent) {
1037 match event {
1038 WindowEvent::PointerLeft { .. } => self.pointer_left_window(window_id),
1039 WindowEvent::PointerMoved { .. } | WindowEvent::PointerEntered { .. } => {
1040 self.cursors.pointer_moved(window_id);
1041 }
1042 _ => {}
1043 }
1044 }
1045
1046 fn pointer_left_window(&mut self, window_id: WinitWindowId) {
1049 let window = if self.primary_window_id() == Some(window_id) {
1050 self.window.clone()
1051 } else {
1052 self.native_windows
1053 .get(&window_id)
1054 .map(|native| native.window.clone())
1055 };
1056 if let Some(window) = window {
1057 self.cursors.pointer_left(&window);
1058 }
1059 }
1060
1061 fn sync_pointer_icons(&mut self, event_loop: &dyn ActiveEventLoop) {
1062 let Some(app) = self.app.as_mut() else {
1063 return;
1064 };
1065 if let Some(window) = self.window.as_ref()
1066 && let Some(icon) = app.take_pointer_icon_change()
1067 {
1068 self.cursors.apply(event_loop, window, &icon);
1069 }
1070 for native in self.native_windows.values() {
1071 if let Some(icon) =
1072 native_surface(app, native).and_then(|surface| surface.take_pointer_icon_change())
1073 {
1074 self.cursors.apply(event_loop, &native.window, &icon);
1075 }
1076 }
1077 self.cursors.keep_held();
1078 }
1079
1080 fn let_the_first_window_take_focus(&self, native: &NativeWindowSurface) {
1081 if a_new_window_comes_up_key(
1082 self.settings.headless,
1083 native.options.visible,
1084 self.any_window_has_focus(),
1085 native.options.focus,
1086 ) {
1087 native.window.focus_window();
1088 }
1089 }
1090
1091 fn any_window_has_focus(&self) -> bool {
1092 let primary_focused = self.primary_visible()
1093 && self
1094 .window
1095 .as_ref()
1096 .is_some_and(|window| window.has_focus());
1097 primary_focused
1098 || self
1099 .native_windows
1100 .values()
1101 .any(|open| open.window.has_focus())
1102 }
1103
1104 fn primary_window_id(&self) -> Option<WinitWindowId> {
1105 self.window.as_ref().map(|window| window.id())
1106 }
1107
1108 fn refresh_native_window(
1109 platform_probe: &NativeWindowPlatformProbe,
1110 registry: &Rc<native_window::NativeWindowRegistry>,
1111 headless: bool,
1112 app: &mut AppShell<WgpuRenderer>,
1113 native: &mut NativeWindowSurface,
1114 request: &NativeWindowRequest,
1115 ) {
1116 native.events = request.events.clone();
1117 native.state = request.state;
1118 native.root = Rc::clone(&request.root);
1119 let revision_changed = native.revision != request.revision;
1120 let options_changed = native.options != request.options;
1121 let position_only_options_change = options_changed
1122 && native_window_options_change_is_position_only(&native.options, &request.options);
1123 let resized = Self::apply_native_window_options(
1124 platform_probe,
1125 app,
1126 native,
1127 &request.options,
1128 headless,
1129 );
1130 if revision_changed {
1131 native.revision = request.revision;
1132 if position_only_options_change {
1133 trace_native_window!(
1134 "sync update position key={:?} title={:?}",
1135 native.key,
1136 native.options.title
1137 );
1138 } else {
1139 trace_native_window!(
1140 "sync update content key={:?} title={:?}",
1141 native.key,
1142 native.options.title
1143 );
1144 native.window.request_redraw();
1145 }
1146 } else if options_changed && request.options.visible {
1147 trace_native_window!(
1148 "sync update options key={:?} title={:?} visible={}",
1149 native.key,
1150 native.options.title,
1151 request.options.visible
1152 );
1153 native.window.request_redraw();
1154 }
1155 if resized {
1156 Self::present_before_the_desktop_composites(app, native, registry, "resize");
1157 }
1158 }
1159
1160 fn present_before_the_desktop_composites(
1161 app: &mut AppShell<WgpuRenderer>,
1162 native: &mut NativeWindowSurface,
1163 registry: &Rc<native_window::NativeWindowRegistry>,
1164 why: &str,
1165 ) {
1166 let started = Instant::now();
1167 let presented = Self::redraw_native_window(app, native, registry);
1168 trace_native_window_timing!(
1169 "{} {} presented={presented} in {}ms",
1170 native.options.title,
1171 why,
1172 started.elapsed().as_millis()
1173 );
1174 }
1175
1176 fn let_go_of_windows_no_longer_declared(
1177 &mut self,
1178 app: &mut AppShell<WgpuRenderer>,
1179 active_keys: &HashSet<NativeWindowKey>,
1180 ) {
1181 let stale_window_ids: Vec<WinitWindowId> = self
1182 .native_windows
1183 .iter()
1184 .filter_map(|(window_id, native)| {
1185 (!active_keys.contains(&native.key)).then_some(*window_id)
1186 })
1187 .collect();
1188 for window_id in stale_window_ids {
1189 if let Some(native) = self.native_windows.get(&window_id) {
1190 trace_native_window!(
1191 "sync stale key={:?} title={:?} visible={} shown={:?}",
1192 native.key,
1193 native.options.title,
1194 native.options.visible,
1195 native.window.is_visible()
1196 );
1197 if let Some((x, y)) =
1198 current_native_window_position(&self.native_window_platform_probe, native)
1199 {
1200 self.native_window_positions.insert(native.key, (x, y));
1201 notify_native_window_moved(&native.events, x, y);
1202 }
1203 }
1204 if let Some(native) = self.native_windows.get_mut(&window_id) {
1205 note_native_window_presented(native.state, false);
1206 native.state = None;
1207 if native.options.visible {
1208 native.options.visible = false;
1209 native.window.set_visible(false);
1210 if let Some(mut surface) = native_surface(app, native) {
1211 cancel_surface_input(&mut surface);
1212 }
1213 }
1214 }
1215 }
1216 }
1217
1218 fn sync_native_windows(&mut self, event_loop: &dyn ActiveEventLoop) {
1219 if self.gpu_context.is_none() {
1220 return;
1221 }
1222 let Some(mut app) = self.app.take() else {
1223 return;
1224 };
1225 self.sync_native_windows_with(&mut app, event_loop);
1226 self.app = Some(app);
1227 }
1228
1229 fn sync_native_windows_with(
1230 &mut self,
1231 app: &mut AppShell<WgpuRenderer>,
1232 event_loop: &dyn ActiveEventLoop,
1233 ) {
1234 let has_requests = native_window::has_native_window_requests(&self.native_window_registry);
1235 if self.native_windows.is_empty() && self.closed_native_windows.is_empty() && !has_requests
1236 {
1237 return;
1238 }
1239 if !has_requests
1240 && self.closed_native_windows.is_empty()
1241 && self
1242 .native_windows
1243 .values()
1244 .all(|native| !native.options.visible)
1245 {
1246 return;
1247 }
1248
1249 let sync_started = Instant::now();
1250 let requests = native_window::native_window_requests(&self.native_window_registry);
1251 let active_keys: HashSet<NativeWindowKey> =
1252 requests.iter().map(|request| request.key).collect();
1253 trace_native_window_timing!(
1254 "sync start requests={} existing={}",
1255 requests.len(),
1256 self.native_windows.len()
1257 );
1258
1259 self.closed_native_windows
1260 .retain(|key| active_keys.contains(key));
1261
1262 self.let_go_of_windows_no_longer_declared(app, &active_keys);
1263
1264 let mut shown_again = Vec::new();
1265 let mut native_windows_to_create =
1266 self.refresh_native_windows_or_collect_new(app, requests, &mut shown_again);
1267 for window_id in shown_again {
1268 self.hand_held_press_to_new_window(app, window_id);
1269 }
1270
1271 self.place_initial_native_windows_on_visible_monitors(
1272 event_loop,
1273 &mut native_windows_to_create,
1274 );
1275
1276 let mut native_window_shells = Vec::with_capacity(native_windows_to_create.len());
1277 let anything_focused = self.any_window_has_focus();
1278 for request in native_windows_to_create {
1279 trace_native_window!(
1280 "sync create key={:?} title={:?} visible={}",
1281 request.key,
1282 request.options.title,
1283 request.options.visible
1284 );
1285 match Self::create_native_window_shell(
1286 event_loop,
1287 request,
1288 self.settings.headless,
1289 anything_focused,
1290 self.settings.window_icon.as_ref(),
1291 ) {
1292 Ok(shell) => native_window_shells.push(shell),
1293 Err(error) => {
1294 self.abort_launch(event_loop, error);
1295 return;
1296 }
1297 }
1298 }
1299
1300 for shell in native_window_shells {
1301 match self.create_native_window(app, shell) {
1302 Ok(native) => {
1303 let window_id = native.window.id();
1304 self.let_the_first_window_take_focus(&native);
1305 self.remember_native_window_position(&native);
1306 self.native_window_ids.insert(native.key, window_id);
1307 self.native_windows.insert(window_id, native);
1308 self.hand_held_press_to_new_window(app, window_id);
1309 }
1310 Err(error) => {
1311 self.abort_launch(event_loop, error);
1312 return;
1313 }
1314 }
1315 }
1316 trace_native_window_timing!("sync done in {}ms", sync_started.elapsed().as_millis());
1317 if self.no_window_left_to_show() {
1318 event_loop.exit();
1319 }
1320 }
1321
1322 fn refresh_native_windows_or_collect_new(
1323 &mut self,
1324 app: &mut AppShell<WgpuRenderer>,
1325 requests: Vec<NativeWindowRequest>,
1326 shown_again: &mut Vec<WinitWindowId>,
1327 ) -> Vec<NativeWindowRequest> {
1328 let mut native_windows_to_create = Vec::new();
1329 for request in requests {
1330 if self.closed_native_windows.contains(&request.key) {
1331 trace_native_window!(
1332 "sync skip closed key={:?} title={:?}",
1333 request.key,
1334 request.options.title
1335 );
1336 continue;
1337 }
1338
1339 let request = self.native_window_request_for_host(&request);
1340 if let Some(window_id) = self.native_window_ids.get(&request.key).copied() {
1341 if let Some(native) = self.native_windows.get_mut(&window_id) {
1342 let was_hidden = !native.options.visible;
1343 Self::refresh_native_window(
1344 &self.native_window_platform_probe,
1345 &self.native_window_registry,
1346 self.settings.headless,
1347 app,
1348 native,
1349 &request,
1350 );
1351 if window_shown_again_takes_a_held_press(was_hidden, native.options.visible) {
1352 trace_native_window!(
1353 "sync shown again key={:?} title={:?}",
1354 native.key,
1355 native.options.title
1356 );
1357 shown_again.push(window_id);
1358 }
1359 continue;
1360 }
1361 self.native_window_ids.remove(&request.key);
1362 }
1363
1364 native_windows_to_create.push(request);
1365 }
1366 native_windows_to_create
1367 }
1368
1369 fn frames_enabled(&self) -> bool {
1370 self.primary_frames_enabled()
1371 || self
1372 .native_windows
1373 .values()
1374 .any(NativeWindowSurface::presentable)
1375 }
1376
1377 fn update_primary_before_wait(
1378 &mut self,
1379 primary_frames_enabled: bool,
1380 needs_update: bool,
1381 needs_redraw: bool,
1382 waiting_for_frame_cap: bool,
1383 frame_interval: Option<Duration>,
1384 ) -> bool {
1385 let primary_visible = self.primary_visible();
1386 let direct_declaration_update = primary_declaration_host_needs_direct_update(
1387 primary_visible,
1388 self.settings.headless,
1389 needs_redraw,
1390 waiting_for_frame_cap,
1391 );
1392 let Some(app) = &mut self.app else {
1393 return false;
1394 };
1395 let Some(window) = &self.window else {
1396 return false;
1397 };
1398 let registry = &self.native_window_registry;
1399 let updated_without_frame = !primary_frames_enabled && app.needs_update_without_frame();
1400 if updated_without_frame {
1401 native_window::with_native_window_registry(registry, || app.update_without_frame());
1402 } else if primary_frames_enabled && needs_update && !needs_redraw && !waiting_for_frame_cap
1403 {
1404 record_pacing_event(|diag| &mut diag.updates);
1405 update_app_with_native_window_registry(app, registry);
1406 if app.frame_owed() || app.needs_redraw() {
1407 request_redraw_once(window, &mut self.primary_redraw_pending);
1408 }
1409 } else if needs_redraw && !waiting_for_frame_cap {
1410 if direct_declaration_update {
1411 trace_native_window!(
1412 "primary declaration host direct update visible={} headless={}",
1413 primary_visible,
1414 self.settings.headless
1415 );
1416 record_pacing_event(|diag| &mut diag.direct_updates);
1417 update_declaration_host_frame(
1418 app,
1419 registry,
1420 &mut self.last_frame_start_time,
1421 frame_interval,
1422 );
1423 } else {
1424 request_redraw_once(window, &mut self.primary_redraw_pending);
1425 }
1426 }
1427 direct_declaration_update || updated_without_frame
1428 }
1429
1430 fn primary_frames_enabled(&self) -> bool {
1431 self.settings.headless
1432 || if self.primary_visible() {
1433 self.primary_presence
1434 .as_ref()
1435 .is_none_or(crate::desktop_lifecycle::WindowPresence::presentable)
1436 } else {
1437 self.native_windows.is_empty()
1438 }
1439 }
1440
1441 fn primary_visible(&self) -> bool {
1442 self.primary_shown
1443 .load(std::sync::atomic::Ordering::Relaxed)
1444 }
1445
1446 fn serve_app_requests(&mut self) {
1450 if let Some((width, height)) = crate::desktop_host_surface::take_requested_size()
1451 && let Some(window) = self.window.as_ref()
1452 {
1453 let _ = window.request_surface_size(
1454 winit::dpi::LogicalSize::new(width as f64, height as f64).into(),
1455 );
1456 }
1457 let registry = Rc::clone(&self.native_window_registry);
1458 if let Some(app) = &mut self.app {
1459 native_window::with_native_window_registry(®istry, || app.run_pending_tasks());
1460 }
1461 }
1462
1463 fn sync_primary_visibility(&mut self) {
1464 let (Some(window), Some(app)) = (self.window.clone(), self.app.as_mut()) else {
1465 return;
1466 };
1467 let was_shown = self
1468 .primary_shown
1469 .load(std::sync::atomic::Ordering::Relaxed);
1470 show_primary_when_it_has_content(
1471 &self.primary_shown,
1472 app,
1473 window.as_ref(),
1474 self.settings.headless,
1475 );
1476 if !was_shown
1477 && self
1478 .primary_shown
1479 .load(std::sync::atomic::Ordering::Relaxed)
1480 {
1481 request_redraw_once(&window, &mut self.primary_redraw_pending);
1482 }
1483 }
1484
1485 fn sync_primary_size(&mut self) {
1486 if self.primary_visible() {
1487 return;
1488 }
1489 let (Some(window), Some(app)) = (self.window.clone(), self.app.as_mut()) else {
1490 return;
1491 };
1492 if let Some(size) = wrap_primary_window_to_content(
1493 window.as_ref(),
1494 app,
1495 self.settings.primary_wraps_content,
1496 self.primary_wrap_size,
1497 ) {
1498 self.primary_wrap_size = Some(size);
1499 }
1500 }
1501
1502 fn no_window_left_to_show(&self) -> bool {
1503 if self.primary_visible() || self.settings.headless {
1504 return false;
1505 }
1506 if self
1507 .native_windows
1508 .values()
1509 .any(|native| native.options.visible)
1510 {
1511 return false;
1512 }
1513
1514 let requests = native_window::native_window_requests(&self.native_window_registry);
1515 requests.is_empty()
1516 || requests
1517 .iter()
1518 .all(|request| self.closed_native_windows.contains(&request.key))
1519 }
1520
1521 fn place_initial_native_windows_on_visible_monitors(
1522 &self,
1523 event_loop: &dyn ActiveEventLoop,
1524 requests: &mut [NativeWindowRequest],
1525 ) {
1526 let monitors = logical_monitor_rects(event_loop);
1527 if monitors.is_empty() {
1528 return;
1529 }
1530
1531 let mut groups: HashMap<NativeWindowKey, Vec<usize>> = HashMap::new();
1532 for (index, request) in requests.iter().enumerate() {
1533 if !request.options.visible
1534 || !Self::native_window_options_have_screen_position(&request.options)
1535 {
1536 continue;
1537 }
1538 groups.entry(request.key).or_default().push(index);
1539 }
1540
1541 for indices in groups.values() {
1542 let Some(bounds) = native_window_request_bounds(requests, indices) else {
1543 continue;
1544 };
1545 if monitors.iter().any(|monitor| {
1546 monitor.contains_rect_with_margin(bounds, NATIVE_WINDOW_PLACEMENT_MARGIN)
1547 }) {
1548 continue;
1549 }
1550
1551 let Some(monitor) = nearest_monitor_to_rect(&monitors, bounds) else {
1552 continue;
1553 };
1554 let delta =
1555 clamp_rect_to_monitor_delta(bounds, monitor, NATIVE_WINDOW_PLACEMENT_MARGIN);
1556 if delta.x.abs() <= f32::EPSILON && delta.y.abs() <= f32::EPSILON {
1557 continue;
1558 }
1559
1560 for index in indices {
1561 let options = &mut requests[*index].options;
1562 if let (Some(x), Some(y)) = (options.x, options.y) {
1563 options.x = Some(x + delta.x);
1564 options.y = Some(y + delta.y);
1565 }
1566 }
1567 }
1568 }
1569
1570 fn native_window_request_for_host(&self, request: &NativeWindowRequest) -> NativeWindowRequest {
1571 let mut request = request.clone();
1572 Self::apply_native_window_state_to_options(&mut request.options, request.state);
1573 if Self::native_window_options_have_screen_position(&request.options) {
1574 request.options = self.resolve_native_window_options(&request.options);
1575 } else if let Some((x, y)) = self.native_window_positions.get(&request.key).copied() {
1576 request.options.x = Some(x);
1577 request.options.y = Some(y);
1578 request.options.position_origin = NativeWindowPositionOrigin::Screen;
1579 } else {
1580 request.options = self.resolve_native_window_options(&request.options);
1581 }
1582 request
1583 }
1584
1585 fn apply_native_window_state_to_options(
1586 options: &mut NativeWindowOptions,
1587 state: Option<WindowState>,
1588 ) {
1589 let Some(state) = state else {
1590 return;
1591 };
1592 let size = state.size_non_reactive();
1593 options.width = size.width;
1594 options.height = size.height;
1595 if let Some(position) = state.position_non_reactive() {
1596 options.x = Some(position.x);
1597 options.y = Some(position.y);
1598 options.position_origin = NativeWindowPositionOrigin::Screen;
1599 }
1600 }
1601
1602 fn resolve_native_window_options(&self, options: &NativeWindowOptions) -> NativeWindowOptions {
1603 let mut options = options.clone();
1604 if options.position_origin == NativeWindowPositionOrigin::HostWindow {
1605 if let (Some(x), Some(y), Some((host_x, host_y))) =
1606 (options.x, options.y, self.host_window_position())
1607 {
1608 options.x = Some(host_x + x);
1609 options.y = Some(host_y + y);
1610 }
1611 options.position_origin = NativeWindowPositionOrigin::Screen;
1612 }
1613 options
1614 }
1615
1616 fn native_window_options_have_screen_position(options: &NativeWindowOptions) -> bool {
1617 options.position_origin == NativeWindowPositionOrigin::Screen
1618 && options.x.is_some()
1619 && options.y.is_some()
1620 }
1621
1622 fn host_window_position(&self) -> Option<(f32, f32)> {
1623 let window = self.window.as_ref()?;
1624 logical_outer_position(window)
1625 }
1626
1627 fn remember_native_window_position(&mut self, native: &NativeWindowSurface) {
1628 tell_the_application_the_window_frame(native.state, &native.window);
1629 if let Some((x, y)) = Self::initial_native_window_position(
1630 &native.options,
1631 current_native_window_position(&self.native_window_platform_probe, native),
1632 ) {
1633 self.native_window_positions.insert(native.key, (x, y));
1634 if let Some(state) = native.state
1635 && state.position_non_reactive().is_none()
1636 {
1637 state.set_position(Some(cranpose_ui::Point::new(x, y)));
1638 }
1639 notify_native_window_moved(&native.events, x, y);
1640 }
1641 }
1642
1643 fn initial_native_window_position(
1644 options: &NativeWindowOptions,
1645 current_position: Option<(f32, f32)>,
1646 ) -> Option<(f32, f32)> {
1647 if options.position_origin == NativeWindowPositionOrigin::Screen
1648 && let Some(position) = options.x.zip(options.y)
1649 {
1650 return Some(position);
1651 }
1652
1653 current_position
1654 }
1655
1656 fn create_native_window_shell(
1657 event_loop: &dyn ActiveEventLoop,
1658 request: NativeWindowRequest,
1659 headless: bool,
1660 anything_focused: bool,
1661 window_icon: Option<&cranpose_ui::ImageBitmap>,
1662 ) -> Result<NativeWindowShell, LaunchError> {
1663 let create_started = Instant::now();
1664 let options = &request.options;
1665 let attributes = native_window_attributes(
1666 options,
1667 headless,
1668 a_new_window_comes_up_key(headless, options.visible, anything_focused, options.focus),
1669 window_icon,
1670 );
1671
1672 let window: Arc<dyn Window> = event_loop
1673 .create_window(attributes)
1674 .map_err(LaunchError::WindowCreate)?
1675 .into();
1676 trace_native_window_timing!(
1677 "{} create_window {}ms",
1678 options.title,
1679 create_started.elapsed().as_millis()
1680 );
1681 Ok(NativeWindowShell {
1682 request,
1683 window,
1684 create_started,
1685 })
1686 }
1687
1688 fn create_native_window(
1689 &self,
1690 app: &mut AppShell<WgpuRenderer>,
1691 shell: NativeWindowShell,
1692 ) -> Result<NativeWindowSurface, LaunchError> {
1693 let NativeWindowShell {
1694 request,
1695 window,
1696 create_started,
1697 } = shell;
1698 let Some(context) = self.gpu_context.as_ref() else {
1699 return Err(LaunchError::GpuContextUnavailable);
1700 };
1701
1702 let options = &request.options;
1703 let surface = context
1704 .gpu
1705 .instance
1706 .create_surface(window.clone())
1707 .map_err(LaunchError::SurfaceCreate)?;
1708 trace_native_window_timing!(
1709 "{} create_surface {}ms",
1710 options.title,
1711 create_started.elapsed().as_millis()
1712 );
1713 let surface_caps = surface.get_capabilities(&context.gpu.adapter);
1714 let surface_format = select_surface_format(&surface_caps)?;
1715 let present_mode = desktop_present_mode(&surface_caps, self.frame_pacing_mode());
1716 log_desktop_present_mode(self.frame_pacing_mode(), present_mode, &surface_caps);
1717 let size = window.surface_size();
1718 let surface_config = surface_config_for_window(
1719 &surface_caps,
1720 surface_format,
1721 size.width.max(1),
1722 size.height.max(1),
1723 present_mode,
1724 options.transparent,
1725 desired_frame_latency(self.frame_pacing_mode(), monitor_refresh_interval(&window)),
1726 )?;
1727 surface.configure(&context.gpu.device, &surface_config);
1728 let placeholder_presented = present_initial_placeholder_frame_cleared_to(
1729 &surface,
1730 &context.gpu.device,
1731 &context.gpu.queue,
1732 surface_format,
1733 "native window initial present",
1734 cranpose_render_wgpu::frame_clear_color(options.transparent),
1735 );
1736 trace_native_window_timing!(
1737 "{} configure {}ms placeholder_presented={placeholder_presented} alpha={:?}",
1738 options.title,
1739 create_started.elapsed().as_millis(),
1740 surface_config.alpha_mode
1741 );
1742
1743 let scale_factor = window.scale_factor();
1744 let mut renderer = wgpu_renderer_for_surface(
1745 context.text_system.clone(),
1746 Arc::clone(&context.gpu.device),
1747 Arc::clone(&context.gpu.queue),
1748 crate::surface_format::display_surface_view_format(surface_format),
1749 context.adapter_backend,
1750 context.gpu.adapter.get_downlevel_capabilities().flags,
1751 scale_factor,
1752 );
1753 renderer.set_transparent_background(options.transparent);
1754 trace_native_window_timing!(
1755 "{} renderer {}ms",
1756 options.title,
1757 create_started.elapsed().as_millis()
1758 );
1759 let viewport = (
1760 surface_config.width as f32 / scale_factor as f32,
1761 surface_config.height as f32 / scale_factor as f32,
1762 );
1763 let registry = Rc::clone(&self.native_window_registry);
1764 let root_id = request.key.raw();
1765 request
1766 .root
1767 .set_size(cranpose_ui::Size::new(viewport.0, viewport.1));
1768 app.add_window_surface(
1769 root_id,
1770 renderer,
1771 (surface_config.width, surface_config.height),
1772 viewport,
1773 );
1774 if let Some(mut surface) = app.surface(RootId::Window(root_id)) {
1775 DesktopTextInput::install(&mut surface, &window);
1776 }
1777 trace_native_window_timing!(
1778 "{} surface {}ms",
1779 options.title,
1780 create_started.elapsed().as_millis()
1781 );
1782
1783 let mut platform = DesktopWinitPlatform::default();
1784 platform.set_scale_factor(scale_factor);
1785 window.request_redraw();
1786 trace_native_window_timing!(
1787 "{} create done {}ms",
1788 options.title,
1789 create_started.elapsed().as_millis()
1790 );
1791
1792 let mut native = NativeWindowSurface {
1793 key: request.key,
1794 revision: request.revision,
1795 options: request.options.clone(),
1796 events: request.events.clone(),
1797 state: request.state,
1798 window,
1799 surface,
1800 surface_config,
1801 surface_caps,
1802 surface_dirty: true,
1803 occluded: false,
1804 minimized: false,
1805 root: Rc::clone(&request.root),
1806 platform,
1807 last_cursor_position: None,
1808 last_cursor_physical_position: None,
1809 last_frame_start_time: None,
1810 last_redraw_empty: false,
1811 vsync_interval: default_vsync_interval(),
1812 pending_outer_positions: PendingNativeWindowPositions::default(),
1813 active_drag: None,
1814 held_press: None,
1815 };
1816 Self::present_before_the_desktop_composites(app, &mut native, ®istry, "first frame");
1817 Ok(native)
1818 }
1819
1820 fn apply_native_window_options(
1821 platform_probe: &NativeWindowPlatformProbe,
1822 app: &mut AppShell<WgpuRenderer>,
1823 native: &mut NativeWindowSurface,
1824 options: &NativeWindowOptions,
1825 headless: bool,
1826 ) -> bool {
1827 let mut resized = false;
1828 Self::apply_native_window_appearance(app, native, options);
1829 resized |=
1830 Self::apply_native_window_geometry(platform_probe, app, native, options, headless);
1831 native.options = options.clone();
1832 resized
1833 }
1834
1835 fn apply_native_window_appearance(
1836 app: &mut AppShell<WgpuRenderer>,
1837 native: &mut NativeWindowSurface,
1838 options: &NativeWindowOptions,
1839 ) {
1840 if native.options.title != options.title {
1841 native.window.set_title(&options.title);
1842 }
1843 if native.options.decorations != options.decorations {
1844 native.window.set_decorations(options.decorations);
1845 }
1846 if native.options.resizable != options.resizable {
1847 native.window.set_resizable(options.resizable);
1848 }
1849 if native.options.transparent != options.transparent {
1850 Self::set_native_transparency(app, native, options.transparent);
1851 }
1852 if native.options.shadow != options.shadow {
1853 set_native_window_shadow(&native.window, options.shadow);
1854 }
1855 if native.options.always_on_top != options.always_on_top {
1856 native
1857 .window
1858 .set_window_level(native_window_level(options.always_on_top));
1859 }
1860 }
1861
1862 fn apply_native_window_geometry(
1863 platform_probe: &NativeWindowPlatformProbe,
1864 app: &mut AppShell<WgpuRenderer>,
1865 native: &mut NativeWindowSurface,
1866 options: &NativeWindowOptions,
1867 headless: bool,
1868 ) -> bool {
1869 let mut resized = false;
1870 if native.options.min_width != options.min_width
1871 || native.options.min_height != options.min_height
1872 {
1873 native
1874 .window
1875 .set_min_surface_size(match (options.min_width, options.min_height) {
1876 (Some(width), Some(height)) => {
1877 Some(LogicalSize::new(width.max(1.0) as f64, height.max(1.0) as f64).into())
1878 }
1879 _ => None,
1880 });
1881 }
1882 if native.options.max_width != options.max_width
1883 || native.options.max_height != options.max_height
1884 {
1885 native
1886 .window
1887 .set_max_surface_size(match (options.max_width, options.max_height) {
1888 (Some(width), Some(height)) => {
1889 Some(LogicalSize::new(width.max(1.0) as f64, height.max(1.0) as f64).into())
1890 }
1891 _ => None,
1892 });
1893 }
1894 if native.options.visible != options.visible {
1895 native.window.set_visible(!headless && options.visible);
1896 note_native_window_presented(native.state, false);
1897 }
1898 if (native.options.x != options.x || native.options.y != options.y)
1899 && let (Some(x), Some(y)) = (options.x, options.y)
1900 {
1901 native.pending_outer_positions.push((x, y));
1902 let logical = LogicalPosition::new(x as f64, y as f64);
1903 let physical = logical.to_physical::<i32>(native.window.scale_factor());
1904 if !native_window_set_outer_position_physical(platform_probe, &native.window, physical)
1905 {
1906 native.window.set_outer_position(Position::Logical(logical));
1907 }
1908 }
1909 if (native.options.width != options.width || native.options.height != options.height)
1910 && let Some(size) = native.window.request_surface_size(
1911 LogicalSize::new(
1912 options.width.max(1.0) as f64,
1913 options.height.max(1.0) as f64,
1914 )
1915 .into(),
1916 )
1917 {
1918 Self::resize_native_surface(app, native, size.width, size.height);
1919 resized = true;
1920 }
1921 resized
1922 }
1923
1924 fn resize_native_surface(
1925 app: &mut AppShell<WgpuRenderer>,
1926 native: &mut NativeWindowSurface,
1927 width: u32,
1928 height: u32,
1929 ) {
1930 if width == 0 || height == 0 {
1931 return;
1932 }
1933 let viewport = surface_logical_viewport_size(width, height, native.window.scale_factor());
1934 let Some(device) = app.renderer().try_device() else {
1935 log::error!("native surface resize skipped: GPU renderer is not initialized");
1936 return;
1937 };
1938 native.surface_config.width = width;
1939 native.surface_config.height = height;
1940 native.surface.configure(device, &native.surface_config);
1941 native
1942 .root
1943 .set_size(cranpose_ui::Size::new(viewport.0, viewport.1));
1944 if let Some(mut surface) = native_surface(app, native) {
1945 surface.set_buffer_size(width, height);
1946 surface.set_viewport(viewport.0, viewport.1);
1947 }
1948 native.surface_dirty = surface_reconfigure_requires_redraw(width, height);
1949 }
1950
1951 fn apply_native_window_resize(
1952 app: &mut AppShell<WgpuRenderer>,
1953 native: &mut NativeWindowSurface,
1954 width: u32,
1955 height: u32,
1956 ) {
1957 let previous_state_size = native
1958 .state
1959 .map(super::native_window::WindowState::size_non_reactive);
1960 update_native_options_size(&mut native.options, &native.window, width, height);
1961 tell_the_application_the_window_frame(native.state, &native.window);
1962 notify_native_window_resized(&native.events, &native.window, width, height);
1963 sync_native_window_state_size(
1964 native.state,
1965 previous_state_size,
1966 &native.window,
1967 width,
1968 height,
1969 );
1970 Self::resize_native_surface(app, native, width, height);
1971 }
1972
1973 fn sync_native_window_position_from_os(
1974 platform_probe: &NativeWindowPlatformProbe,
1975 native: &mut NativeWindowSurface,
1976 native_window_positions: &mut HashMap<NativeWindowKey, (f32, f32)>,
1977 ) -> bool {
1978 let Some(position) = current_native_window_position(platform_probe, native) else {
1979 return false;
1980 };
1981 if native_window_positions
1982 .get(&native.key)
1983 .is_some_and(|known| native_window_positions_close(*known, position))
1984 && native
1985 .state
1986 .and_then(super::native_window::WindowState::position_non_reactive)
1987 .is_some_and(|known| native_window_positions_close((known.x, known.y), position))
1988 {
1989 return false;
1990 }
1991
1992 let previous_state_position = native
1993 .state
1994 .and_then(super::native_window::WindowState::position_non_reactive);
1995 native_window_positions.insert(native.key, position);
1996 update_native_options_position(&mut native.options, position.0, position.1);
1997 native.pending_outer_positions.clear();
1998 tell_the_application_the_window_moved(&native.events, native.state, position.0, position.1);
1999 sync_native_window_state_position(
2000 native.state,
2001 previous_state_position,
2002 position.0,
2003 position.1,
2004 );
2005 true
2006 }
2007
2008 fn prepare_native_window_position_request(
2009 platform_probe: &NativeWindowPlatformProbe,
2010 native: &mut NativeWindowSurface,
2011 position: cranpose_ui::Point,
2012 ) -> Option<NativeWindowPositionRequest> {
2013 let logical_position = (position.x, position.y);
2014 if current_native_window_position(platform_probe, native).is_some_and(|current| {
2015 (current.0 - logical_position.0).abs() <= f32::EPSILON
2016 && (current.1 - logical_position.1).abs() <= f32::EPSILON
2017 }) {
2018 update_native_options_position(&mut native.options, position.x, position.y);
2019 return None;
2020 }
2021
2022 native.pending_outer_positions.push(logical_position);
2023 let logical = LogicalPosition::new(position.x as f64, position.y as f64);
2024 let physical = logical.to_physical::<i32>(native.window.scale_factor());
2025 update_native_options_position(&mut native.options, position.x, position.y);
2026 let previous_state_position = native
2027 .state
2028 .and_then(super::native_window::WindowState::position_non_reactive);
2029 tell_the_application_the_window_moved(&native.events, native.state, position.x, position.y);
2030 sync_native_window_state_position(
2031 native.state,
2032 previous_state_position,
2033 position.x,
2034 position.y,
2035 );
2036 Some(NativeWindowPositionRequest {
2037 window: Arc::clone(&native.window),
2038 logical,
2039 physical,
2040 })
2041 }
2042
2043 fn apply_native_window_position_requests(
2044 platform_probe: &NativeWindowPlatformProbe,
2045 requests: Vec<NativeWindowPositionRequest>,
2046 mode: NativeWindowPositionApplyMode,
2047 ) {
2048 if requests.is_empty() {
2049 return;
2050 }
2051 if native_window_set_outer_positions_physical(platform_probe, &requests) {
2052 if mode == NativeWindowPositionApplyMode::WaitForSettle {
2053 wait_native_window_positions_physical(platform_probe, &requests);
2054 }
2055 return;
2056 }
2057 for request in &requests {
2058 request
2059 .window
2060 .set_outer_position(Position::Logical(request.logical));
2061 }
2062 if mode == NativeWindowPositionApplyMode::WaitForSettle {
2063 wait_native_window_positions_physical(platform_probe, &requests);
2064 }
2065 }
2066
2067 fn handle_native_primary_pressed(
2068 &mut self,
2069 app: &mut AppShell<WgpuRenderer>,
2070 native: &mut NativeWindowSurface,
2071 ) -> bool {
2072 let (handled, drag_requested) =
2073 Self::dispatch_native_primary_pressed(&self.native_window_platform_probe, app, native);
2074 if handled {
2075 apply_pointer_button_frame_request(
2076 &native.window,
2077 &mut native.last_frame_start_time,
2078 pointer_button_frame_request(handled),
2079 );
2080 if drag_requested {
2081 self.begin_native_window_drag(native);
2082 }
2083 }
2084 handled
2085 }
2086
2087 fn dispatch_native_primary_pressed(
2088 platform_probe: &NativeWindowPlatformProbe,
2089 app: &mut AppShell<WgpuRenderer>,
2090 native: &mut NativeWindowSurface,
2091 ) -> (bool, bool) {
2092 let Some(mut surface) = native_surface(app, native) else {
2093 return (false, false);
2094 };
2095 let drag_requested = Rc::new(Cell::new(false));
2096 let drag_requested_for_handler = Rc::clone(&drag_requested);
2097 let drag_handler: Rc<dyn Fn() -> bool> = Rc::new(move || {
2098 drag_requested_for_handler.set(true);
2099 true
2100 });
2101 let resize_window = native.window.clone();
2102 let resize_handler: Rc<dyn Fn(WindowResizeDirection)> = Rc::new(move |direction| {
2103 if let Err(error) = resize_window.drag_resize_window(native_resize_direction(direction))
2104 {
2105 log::debug!("native window resize request failed: {error}");
2106 }
2107 });
2108 surface.set_screen_origin(native_window_surface_origin(platform_probe, &native.window));
2109 let handled =
2110 native_window::with_native_window_drag_handler(drag_handler, resize_handler, || {
2111 surface.pointer_pressed()
2112 });
2113 (handled, drag_requested.get())
2114 }
2115
2116 fn begin_native_window_drag(&mut self, native: &mut NativeWindowSurface) {
2117 self.begin_native_window_drag_with_anchor(native, None);
2118 }
2119
2120 fn begin_native_window_drag_with_anchor(
2121 &mut self,
2122 native: &mut NativeWindowSurface,
2123 start_pointer_screen: Option<PhysicalPosition<f64>>,
2124 ) {
2125 trace_native_window!("drag requested key={:?}", native.key);
2126 let platform_probe = &self.native_window_platform_probe;
2127 Self::sync_native_window_position_from_os(
2128 platform_probe,
2129 native,
2130 &mut self.native_window_positions,
2131 );
2132 for other_native in self.native_windows.values_mut() {
2133 Self::sync_native_window_position_from_os(
2134 platform_probe,
2135 other_native,
2136 &mut self.native_window_positions,
2137 );
2138 }
2139 let anchor =
2140 start_pointer_screen.or_else(|| native_window_press_anchor(platform_probe, native));
2141 if !Self::start_native_window_drag(platform_probe, native, anchor) {
2142 trace_native_window!("drag cancel key={:?} reason=start-failed", native.key);
2143 }
2144 }
2145
2146 fn move_dragged_window(&mut self, key: NativeWindowKey, target: cranpose_ui::Point) -> bool {
2147 let Some(window_id) = self.native_window_ids.get(&key).copied() else {
2148 return false;
2149 };
2150 let Some(native) = self.native_windows.get_mut(&window_id) else {
2151 return false;
2152 };
2153 let Some(request) = Self::prepare_native_window_position_request(
2154 &self.native_window_platform_probe,
2155 native,
2156 target,
2157 ) else {
2158 return false;
2159 };
2160 self.native_window_positions
2161 .insert(key, (target.x, target.y));
2162 Self::apply_native_window_position_requests(
2163 &self.native_window_platform_probe,
2164 vec![request],
2165 NativeWindowPositionApplyMode::FlushOnly,
2166 );
2167 true
2168 }
2169
2170 fn start_native_window_drag(
2171 platform_probe: &NativeWindowPlatformProbe,
2172 native: &mut NativeWindowSurface,
2173 start_pointer_screen: Option<PhysicalPosition<f64>>,
2174 ) -> bool {
2175 let now = Instant::now();
2176 if let Some(session) = Self::native_window_polling_drag_session(
2177 platform_probe,
2178 native,
2179 now,
2180 start_pointer_screen,
2181 ) {
2182 let pointer = session.start_pointer_screen;
2183 let window_outer = session.start_window_outer;
2184 native.active_drag = Some(session);
2185 trace_native_window!(
2186 "drag start polling key={:?} pointer=({:.1},{:.1}) outer=({},{})",
2187 native.key,
2188 pointer.x,
2189 pointer.y,
2190 window_outer.x,
2191 window_outer.y
2192 );
2193 return true;
2194 }
2195
2196 false
2197 }
2198
2199 fn native_window_polling_drag_session(
2200 platform_probe: &NativeWindowPlatformProbe,
2201 native: &NativeWindowSurface,
2202 now: Instant,
2203 start_pointer_screen: Option<PhysicalPosition<f64>>,
2204 ) -> Option<NativeWindowPollingDragSession> {
2205 let pointer = native_window_polling_drag_pointer(
2206 native_window_global_pointer_state(platform_probe),
2207 start_pointer_screen,
2208 )?;
2209 let window_outer = current_native_window_physical_position(platform_probe, &native.window)?;
2210 Some(NativeWindowPollingDragSession::new(
2211 pointer,
2212 window_outer,
2213 now,
2214 ))
2215 }
2216
2217 fn poll_native_window_global_primary_press(&mut self) -> bool {
2218 let Some(mut app) = self.app.take() else {
2219 return false;
2220 };
2221 let handled = self.poll_native_window_global_primary_press_with(&mut app);
2222 self.app = Some(app);
2223 handled
2224 }
2225
2226 fn poll_native_window_global_primary_press_with(
2227 &mut self,
2228 app: &mut AppShell<WgpuRenderer>,
2229 ) -> bool {
2230 let platform_probe = &self.native_window_platform_probe;
2231 let Some(pointer) = native_window_global_pointer_state(platform_probe) else {
2232 self.native_global_primary_down = false;
2233 return false;
2234 };
2235 if !pointer.primary_down {
2236 if self.native_global_primary_down {
2237 self.native_global_primary_down = false;
2238 return self.release_native_global_primary_press(app);
2239 }
2240 self.native_global_primary_down = false;
2241 return false;
2242 }
2243 if self.native_global_primary_down {
2244 return false;
2245 }
2246
2247 let Some(window_id) = self.native_windows.iter().find_map(|(window_id, native)| {
2248 (native.options.visible
2249 && native.active_drag.is_none()
2250 && native_window_surface_contains_pointer(platform_probe, native, pointer.position))
2251 .then_some(*window_id)
2252 }) else {
2253 self.native_global_primary_down = false;
2254 return false;
2255 };
2256
2257 self.recover_primary_press_into(app, window_id, pointer)
2258 }
2259
2260 fn hand_held_press_to_new_window(
2261 &mut self,
2262 app: &mut AppShell<WgpuRenderer>,
2263 window_id: WinitWindowId,
2264 ) {
2265 let holder = app.root_holding_the_press();
2266 let platform_probe = &self.native_window_platform_probe;
2267 let Some(native) = self.native_windows.get(&window_id) else {
2268 return;
2269 };
2270 let belongs = press_belongs_here(holder, native.root_id());
2271 let dragging_elsewhere = self
2272 .native_windows
2273 .values()
2274 .any(|other| other.active_drag.is_some());
2275 let held = self.held_press_pointer_state();
2276 let Some(handover) = press_to_hand_over(
2277 native_window_global_pointer_state(platform_probe),
2278 held,
2279 native.options.visible && !dragging_elsewhere,
2280 belongs,
2281 |position| native_window_surface_contains_pointer(platform_probe, native, position),
2282 ) else {
2283 return;
2284 };
2285 trace_native_window!(
2286 "held press handed to key={:?} pointer=({:.1},{:.1}) relayed_by={:?}",
2287 native.key,
2288 handover.pointer.position.x,
2289 handover.pointer.position.y,
2290 handover.relayed_by
2291 );
2292 self.cancel_held_press_elsewhere(app, window_id);
2293 let recovered = self.recover_primary_press_into(app, window_id, handover.pointer);
2294 self.handed_press = handover
2295 .relayed_by
2296 .filter(|_| recovered)
2297 .map(|holder| HandedPress {
2298 holder,
2299 taker: window_id,
2300 });
2301 }
2302
2303 fn held_press_pointer_state(&self) -> Option<(WinitWindowId, NativeWindowPointerState)> {
2304 let platform_probe = &self.native_window_platform_probe;
2305 let primary = self
2306 .window
2307 .as_ref()
2308 .map(|window| (window, self.primary_held_press));
2309 let peers = self
2310 .native_windows
2311 .values()
2312 .map(|native| (&native.window, native.held_press));
2313 primary.into_iter().chain(peers).find_map(|(window, held)| {
2314 let pointer = held_press_on_screen(platform_probe, window, held?)?;
2315 Some((window.id(), pointer))
2316 })
2317 }
2318
2319 fn relay_primary_held_press(&mut self, event_loop: &dyn ActiveEventLoop, event: &WindowEvent) {
2320 let Some(step) = held_press_step(event) else {
2321 return;
2322 };
2323 self.primary_held_press = held_press_after_step(self.primary_held_press, step);
2324 if self.handed_press.is_none() {
2325 return;
2326 }
2327 let Some(window) = self.window.clone() else {
2328 return;
2329 };
2330 let Some(mut app) = self.app.take() else {
2331 return;
2332 };
2333 let settlement = self.relay_held_press_step(&mut app, window.id(), &window, step);
2334 self.app = Some(app);
2335 self.settle_native_window_event(event_loop, settlement);
2336 }
2337
2338 fn relay_native_held_press(
2339 &mut self,
2340 app: &mut AppShell<WgpuRenderer>,
2341 native: &mut NativeWindowSurface,
2342 event: &WindowEvent,
2343 ) -> NativeWindowEventSettlement {
2344 let Some(step) = held_press_step(event) else {
2345 return NativeWindowEventSettlement::default();
2346 };
2347 native.held_press = held_press_after_step(native.held_press, step);
2348 self.relay_held_press_step(app, native.window.id(), &native.window, step)
2349 }
2350
2351 fn relay_held_press_step(
2352 &mut self,
2353 app: &mut AppShell<WgpuRenderer>,
2354 holder: WinitWindowId,
2355 holder_window: &Arc<dyn Window>,
2356 step: HeldPressStep,
2357 ) -> NativeWindowEventSettlement {
2358 let Some(taker) = self
2359 .handed_press
2360 .and_then(|handed| handed.taker_for(holder))
2361 else {
2362 return NativeWindowEventSettlement::default();
2363 };
2364 if let HeldPressStep::Pressed(_) = step {
2365 self.handed_press = None;
2366 return NativeWindowEventSettlement::default();
2367 }
2368 let platform_probe = &self.native_window_platform_probe;
2369 let Some(native) = self.native_windows.get_mut(&taker) else {
2370 self.handed_press = None;
2371 return NativeWindowEventSettlement::default();
2372 };
2373 let Some(screen) =
2374 native_window_screen_pointer_physical(platform_probe, holder_window, step.position())
2375 else {
2376 return NativeWindowEventSettlement::default();
2377 };
2378 Self::set_native_cursor_from_screen(platform_probe, app, native, screen);
2379 match step {
2380 HeldPressStep::Pressed(_) | HeldPressStep::Moved(_) => NativeWindowEventSettlement {
2381 drag_move: Self::update_native_window_polling_drag_target(native, screen),
2382 ..NativeWindowEventSettlement::default()
2383 },
2384 HeldPressStep::Released(_) => {
2385 self.handed_press = None;
2386 self.native_global_primary_down = false;
2387 #[expect(
2388 clippy::redundant_closure_for_method_calls,
2389 reason = "the method path is not general over the surface lifetime"
2390 )]
2391 let settlement = Self::finish_native_press(
2392 platform_probe,
2393 app,
2394 native,
2395 Some(screen),
2396 "relayed-release",
2397 |surface| surface.pointer_released(),
2398 );
2399 trace_native_window!("held press released, key={:?} raised", native.key);
2400 native.window.focus_window();
2401 settlement
2402 }
2403 }
2404 }
2405
2406 fn set_native_cursor_from_screen(
2407 platform_probe: &NativeWindowPlatformProbe,
2408 app: &mut AppShell<WgpuRenderer>,
2409 native: &mut NativeWindowSurface,
2410 screen: PhysicalPosition<f64>,
2411 ) {
2412 let Some(local) =
2413 native_window_local_pointer_physical(platform_probe, &native.window, screen)
2414 else {
2415 return;
2416 };
2417 let logical = native.platform.pointer_position(local);
2418 native.last_cursor_position = Some((logical.x, logical.y));
2419 native.last_cursor_physical_position = Some(local);
2420 if let Some(mut surface) = native_surface(app, native) {
2421 surface.set_screen_origin(native_window_surface_origin(platform_probe, &native.window));
2422 surface.set_cursor(logical.x, logical.y);
2423 }
2424 }
2425
2426 fn finish_native_press(
2427 platform_probe: &NativeWindowPlatformProbe,
2428 app: &mut AppShell<WgpuRenderer>,
2429 native: &mut NativeWindowSurface,
2430 pointer: Option<PhysicalPosition<f64>>,
2431 reason: &str,
2432 release: impl FnOnce(&mut SurfaceMut<'_, WgpuRenderer>) -> bool,
2433 ) -> NativeWindowEventSettlement {
2434 let drag_move = pointer
2435 .and_then(|pointer| Self::update_native_window_polling_drag_target(native, pointer));
2436 let finish_drag = native.active_drag.take().is_some();
2437 if finish_drag {
2438 trace_native_window!("drag finish key={:?} reason={reason}", native.key);
2439 }
2440 let handled = native_surface(app, native).is_some_and(|mut surface| {
2441 surface.set_screen_origin(native_window_surface_origin(platform_probe, &native.window));
2442 release(&mut surface)
2443 });
2444 app.sync_selection_to_primary();
2445 if handled {
2446 apply_pointer_button_frame_request(
2447 &native.window,
2448 &mut native.last_frame_start_time,
2449 pointer_button_frame_request(handled),
2450 );
2451 }
2452 NativeWindowEventSettlement {
2453 sync_after_event: handled,
2454 drag_move,
2455 finish_drag,
2456 }
2457 }
2458
2459 fn cancel_held_press_elsewhere(
2460 &mut self,
2461 app: &mut AppShell<WgpuRenderer>,
2462 keep: WinitWindowId,
2463 ) {
2464 if app.has_active_pointer_gesture() {
2465 cancel_app_input(app);
2466 }
2467 for native in self.native_windows.values_mut() {
2468 if native.window.id() == keep {
2469 continue;
2470 }
2471 if let Some(mut surface) = native_surface(app, native)
2472 && surface.has_active_pointer_gesture()
2473 {
2474 cancel_surface_input(&mut surface);
2475 }
2476 }
2477 self.native_global_primary_down = false;
2478 }
2479
2480 fn recover_primary_press_into(
2481 &mut self,
2482 app: &mut AppShell<WgpuRenderer>,
2483 window_id: WinitWindowId,
2484 pointer: NativeWindowPointerState,
2485 ) -> bool {
2486 let platform_probe = &self.native_window_platform_probe;
2487 let Some(mut native) = self.native_windows.remove(&window_id) else {
2488 self.native_global_primary_down = false;
2489 return false;
2490 };
2491 Self::set_native_cursor_from_screen(platform_probe, app, &mut native, pointer.position);
2492
2493 let (press_handled, drag_requested) =
2494 Self::dispatch_native_primary_pressed(platform_probe, app, &mut native);
2495 if press_handled {
2496 self.native_global_primary_down = true;
2497 trace_native_window!(
2498 "global primary recovered key={:?} drag_requested={}",
2499 native.key,
2500 drag_requested
2501 );
2502 apply_pointer_button_frame_request(
2503 &native.window,
2504 &mut native.last_frame_start_time,
2505 pointer_button_frame_request(press_handled),
2506 );
2507 if drag_requested {
2508 self.begin_native_window_drag_with_anchor(&mut native, Some(pointer.position));
2509 } else {
2510 app.sync_selection_to_primary();
2511 }
2512 } else {
2513 self.native_global_primary_down = false;
2514 }
2515
2516 self.native_windows.insert(window_id, native);
2517 press_handled
2518 }
2519
2520 fn release_native_global_primary_press(&mut self, app: &mut AppShell<WgpuRenderer>) -> bool {
2521 let platform_probe = &self.native_window_platform_probe;
2522 let mut handled_any = false;
2523 for native in self.native_windows.values_mut() {
2524 if native.active_drag.is_some() {
2525 continue;
2526 }
2527 let Some(mut surface) = native_surface(app, native) else {
2528 continue;
2529 };
2530 surface.set_screen_origin(native_window_surface_origin(platform_probe, &native.window));
2531 let handled = surface.pointer_released();
2532 app.sync_selection_to_primary();
2533 if handled {
2534 apply_pointer_button_frame_request(
2535 &native.window,
2536 &mut native.last_frame_start_time,
2537 pointer_button_frame_request(handled),
2538 );
2539 native.window.request_redraw();
2540 handled_any = true;
2541 }
2542 }
2543 handled_any
2544 }
2545
2546 fn poll_active_native_window_drags(&mut self, now: Instant) -> bool {
2547 let has_due_drag = self.native_windows.values().any(|native| {
2548 native
2549 .active_drag
2550 .and_then(|active_drag| {
2551 native_window_drag_poll_deadline(
2552 active_drag.next_poll_at,
2553 NATIVE_WINDOW_GLOBAL_POINTER_POLLED,
2554 )
2555 })
2556 .is_some_and(|deadline| deadline <= now)
2557 });
2558 if !has_due_drag {
2559 return false;
2560 }
2561 let Some(mut app) = self.app.take() else {
2562 return false;
2563 };
2564 let needs_registry_sync = self.poll_active_native_window_drags_with(&mut app, now);
2565 self.app = Some(app);
2566 needs_registry_sync
2567 }
2568
2569 fn poll_active_native_window_drags_with(
2570 &mut self,
2571 app: &mut AppShell<WgpuRenderer>,
2572 now: Instant,
2573 ) -> bool {
2574 let platform_probe = &self.native_window_platform_probe;
2575 let pointer = native_window_global_pointer_state(platform_probe);
2576 let mut updates = Vec::new();
2577 let mut needs_registry_sync = false;
2578 for native in self.native_windows.values_mut() {
2579 let Some(active_drag) = native.active_drag.as_mut() else {
2580 continue;
2581 };
2582 if active_drag.next_poll_at > now {
2583 continue;
2584 }
2585 active_drag.next_poll_at = now + NATIVE_WINDOW_DRAG_POLL_INTERVAL;
2586
2587 let Some(pointer) = pointer else {
2588 trace_native_window!(
2589 "drag poll skipped key={:?} reason=no-global-pointer",
2590 native.key
2591 );
2592 continue;
2593 };
2594 if !pointer.primary_down {
2595 native.active_drag = None;
2596 needs_registry_sync = true;
2597 trace_native_window!("drag finish key={:?} reason=global-release", native.key);
2598 if native_surface(app, native).is_some_and(|mut surface| surface.pointer_released())
2599 {
2600 native.window.request_redraw();
2601 needs_registry_sync = true;
2602 }
2603 app.sync_selection_to_primary();
2604 continue;
2605 }
2606 if let Some(update) =
2607 Self::update_native_window_polling_drag_target(native, pointer.position)
2608 {
2609 updates.push(update);
2610 }
2611 }
2612
2613 for (key, position) in updates {
2614 self.move_dragged_window(key, position);
2615 }
2616 needs_registry_sync
2617 }
2618
2619 fn poll_external_native_window_moves(&mut self, now: Instant) -> bool {
2620 if self.native_windows.is_empty() || self.next_native_window_position_poll_at > now {
2621 return false;
2622 }
2623 self.next_native_window_position_poll_at = now + NATIVE_WINDOW_POSITION_POLL_INTERVAL;
2624
2625 let mut external_moves = Vec::new();
2626 let native_window_positions = &mut self.native_window_positions;
2627 let platform_probe = &self.native_window_platform_probe;
2628 for (window_id, native) in &mut self.native_windows {
2629 if !native.options.visible || native.active_drag.is_some() {
2630 continue;
2631 }
2632 let Some(position) = current_native_window_position(platform_probe, native) else {
2633 continue;
2634 };
2635 let known_position = native_window_positions.get(&native.key).copied();
2636 match native
2637 .pending_outer_positions
2638 .acknowledge_or_matches_known(position, known_position)
2639 {
2640 NativeWindowPositionObservation::Current => {
2641 native_window_positions.insert(native.key, position);
2642 update_native_options_position(&mut native.options, position.0, position.1);
2643 continue;
2644 }
2645 NativeWindowPositionObservation::Superseded => {
2646 continue;
2647 }
2648 NativeWindowPositionObservation::External => {}
2649 }
2650 if native.pending_outer_positions.has_pending()
2651 || native_window_positions
2652 .get(&native.key)
2653 .is_some_and(|known| native_window_positions_close(*known, position))
2654 {
2655 continue;
2656 }
2657
2658 let previous_state_position = native
2659 .state
2660 .and_then(super::native_window::WindowState::position_non_reactive);
2661 external_moves.push((*window_id, native.key, position, previous_state_position));
2662 }
2663
2664 let mut moved = false;
2665 for (window_id, key, position, previous_state_position) in external_moves {
2666 moved |= self.reconcile_external_native_window_move(
2667 window_id,
2668 key,
2669 position,
2670 previous_state_position,
2671 );
2672 }
2673 moved
2674 }
2675
2676 fn reconcile_external_native_window_move(
2677 &mut self,
2678 window_id: WinitWindowId,
2679 key: NativeWindowKey,
2680 position: (f32, f32),
2681 previous_state_position: Option<cranpose_ui::Point>,
2682 ) -> bool {
2683 let Some(native) = self.native_windows.get_mut(&window_id) else {
2684 return false;
2685 };
2686 if native.active_drag.is_some() || native.pending_outer_positions.has_pending() {
2687 return false;
2688 }
2689
2690 trace_native_window!(
2691 "poll external move key={:?} pos=({:.1},{:.1})",
2692 key,
2693 position.0,
2694 position.1
2695 );
2696 self.native_window_positions.insert(key, position);
2697 update_native_options_position(&mut native.options, position.0, position.1);
2698 let position = cranpose_ui::Point::new(position.0, position.1);
2699 tell_the_application_the_window_moved(&native.events, native.state, position.x, position.y);
2700 sync_native_window_state_position(
2701 native.state,
2702 previous_state_position,
2703 position.x,
2704 position.y,
2705 );
2706 true
2707 }
2708
2709 fn update_native_window_polling_drag_target(
2710 native: &mut NativeWindowSurface,
2711 pointer: PhysicalPosition<f64>,
2712 ) -> Option<(NativeWindowKey, cranpose_ui::Point)> {
2713 let active_drag = native.active_drag.as_mut()?;
2714 let target = active_drag.target_for_pointer(pointer);
2715 if target == active_drag.last_target_outer {
2716 return None;
2717 }
2718 active_drag.last_target_outer = target;
2719 let logical = target.to_logical::<f64>(native.window.scale_factor());
2720 trace_native_window!(
2721 "drag target key={:?} logical=({:.1},{:.1}) physical=({},{})",
2722 native.key,
2723 logical.x,
2724 logical.y,
2725 target.x,
2726 target.y
2727 );
2728 Some((
2729 native.key,
2730 cranpose_ui::Point::new(logical.x as f32, logical.y as f32),
2731 ))
2732 }
2733
2734 fn dispatch_native_window_event(
2735 &mut self,
2736 event_loop: &dyn ActiveEventLoop,
2737 window_id: WinitWindowId,
2738 event: WindowEvent,
2739 ) {
2740 let Some(mut native) = self.native_windows.remove(&window_id) else {
2741 return;
2742 };
2743 let Some(mut app) = self.app.take() else {
2744 self.native_windows.insert(window_id, native);
2745 return;
2746 };
2747 let relayed = self.relay_native_held_press(&mut app, &mut native, &event);
2748 let (keep_window, settlement) =
2749 self.native_window_event(&mut app, event_loop, &mut native, event);
2750 self.app = Some(app);
2751 if keep_window {
2752 self.native_windows.insert(window_id, native);
2753 self.settle_native_window_event(event_loop, settlement.also(relayed));
2754 }
2755 }
2756
2757 fn native_window_event(
2758 &mut self,
2759 app: &mut AppShell<WgpuRenderer>,
2760 event_loop: &dyn ActiveEventLoop,
2761 native: &mut NativeWindowSurface,
2762 event: WindowEvent,
2763 ) -> (bool, NativeWindowEventSettlement) {
2764 let mut keep_window = true;
2765 let mut sync_after_event = false;
2766 let mut drag_move_after_insert = None::<(NativeWindowKey, cranpose_ui::Point)>;
2767 let mut finish_drag_after_insert = false;
2768 if let Some(label) = native_window_lifecycle_event(&event) {
2769 trace_native_window!("event {label} key={:?}", native.key);
2770 }
2771 let Some(event) = deliver_native_surface_event(app, native, self.current_modifiers, event)
2772 else {
2773 return (true, NativeWindowEventSettlement::default());
2774 };
2775 match event {
2776 WindowEvent::CloseRequested => {
2777 trace_native_window!("event close-request key={:?}", native.key);
2778 notify_native_window_close_requested(&native.events);
2779 self.remember_native_window_position(native);
2780 self.native_window_ids.remove(&native.key);
2781 self.closed_native_windows.insert(native.key);
2782 app.remove_window_surface(native.key.raw());
2783 keep_window = false;
2784 }
2785 WindowEvent::SurfaceResized(new_size) => {
2786 Self::apply_native_window_resize(app, native, new_size.width, new_size.height);
2787 Self::present_before_the_desktop_composites(
2788 app,
2789 native,
2790 &self.native_window_registry,
2791 "resize",
2792 );
2793 sync_after_event = true;
2794 }
2795 WindowEvent::ScaleFactorChanged {
2796 scale_factor,
2797 mut surface_size_writer,
2798 } => {
2799 native.platform.set_scale_factor(scale_factor);
2800 if let Some(mut surface) = native_surface(app, native) {
2801 surface.renderer().set_root_scale(scale_factor as f32);
2802 }
2803
2804 let new_size = native.window.surface_size();
2805 let _ = surface_size_writer.request_surface_size(new_size);
2806 Self::apply_native_window_resize(app, native, new_size.width, new_size.height);
2807 sync_after_event = true;
2808 }
2809 WindowEvent::Focused(true) => {
2810 native_window_focused(app, native);
2811 }
2812 WindowEvent::Moved(position) => {
2813 native.vsync_interval = monitor_refresh_interval(&native.window);
2814 let known_position = self.native_window_positions.get(&native.key).copied();
2815 let previous_state_position = native
2816 .state
2817 .and_then(super::native_window::WindowState::position_non_reactive);
2818 let platform_probe = &self.native_window_platform_probe;
2819 let position = current_native_window_position(platform_probe, native)
2820 .unwrap_or_else(|| {
2821 let logical = position.to_logical::<f64>(native.window.scale_factor());
2822 (logical.x as f32, logical.y as f32)
2823 });
2824 let position_observation = native
2825 .pending_outer_positions
2826 .acknowledge_or_matches_known(position, known_position);
2827 trace_native_window!(
2828 "event moved key={:?} pos=({:.1},{:.1}) observation={:?} active_drag={}",
2829 native.key,
2830 position.0,
2831 position.1,
2832 position_observation,
2833 native.active_drag.is_some()
2834 );
2835 match position_observation {
2836 NativeWindowPositionObservation::Current => {
2837 self.native_window_positions.insert(native.key, position);
2838 update_native_options_position(&mut native.options, position.0, position.1);
2839 }
2840 NativeWindowPositionObservation::Superseded => {}
2841 NativeWindowPositionObservation::External => {
2842 if native.active_drag.is_some() {
2843 trace_native_window!(
2844 "event moved ignored during polling drag key={:?}",
2845 native.key
2846 );
2847 } else {
2848 self.native_window_positions.insert(native.key, position);
2849 update_native_options_position(
2850 &mut native.options,
2851 position.0,
2852 position.1,
2853 );
2854 let position = cranpose_ui::Point::new(position.0, position.1);
2855 native.pending_outer_positions.clear();
2856 tell_the_application_the_window_moved(
2857 &native.events,
2858 native.state,
2859 position.x,
2860 position.y,
2861 );
2862 sync_native_window_state_position(
2863 native.state,
2864 previous_state_position,
2865 position.x,
2866 position.y,
2867 );
2868 sync_after_event = true;
2869 }
2870 }
2871 }
2872 }
2873 WindowEvent::PointerMoved {
2874 position, source, ..
2875 } => {
2876 let logical = native.platform.pointer_position(position);
2877 native.last_cursor_position = Some((logical.x, logical.y));
2878 native.last_cursor_physical_position = Some(position);
2879 let platform_probe = &self.native_window_platform_probe;
2880 let event_pointer =
2881 native_window_screen_pointer_physical(platform_probe, &native.window, position);
2882 let global_pointer = native_window_global_pointer_state(platform_probe);
2883 let pointer_position = global_pointer.map(|state| state.position).or(event_pointer);
2884 let handled = native_surface(app, native).is_some_and(|mut surface| {
2885 surface.set_pointer_source(pointer_source_from_winit(&source));
2886 surface.set_screen_origin(native_window_surface_origin(
2887 platform_probe,
2888 &native.window,
2889 ));
2890 surface.set_cursor(logical.x, logical.y)
2891 });
2892 if let Some(pointer) = pointer_position
2893 && let Some((key, position)) =
2894 Self::update_native_window_polling_drag_target(native, pointer)
2895 {
2896 drag_move_after_insert = Some((key, position));
2897 sync_after_event = true;
2898 }
2899 if native.active_drag.is_none()
2900 && !self.native_global_primary_down
2901 && global_pointer.is_some_and(|pointer| {
2902 pointer.primary_down
2903 && native_window_surface_contains_pointer(
2904 platform_probe,
2905 native,
2906 pointer.position,
2907 )
2908 })
2909 {
2910 let (press_handled, drag_requested) =
2911 Self::dispatch_native_primary_pressed(platform_probe, app, native);
2912 if press_handled {
2913 self.native_global_primary_down = true;
2914 trace_native_window!(
2915 "event pointer-move recovered primary-press key={:?} drag_requested={}",
2916 native.key,
2917 drag_requested
2918 );
2919 apply_pointer_button_frame_request(
2920 &native.window,
2921 &mut native.last_frame_start_time,
2922 pointer_button_frame_request(press_handled),
2923 );
2924 if drag_requested {
2925 self.begin_native_window_drag_with_anchor(
2926 native,
2927 recovered_native_window_drag_start_pointer(
2928 event_pointer,
2929 global_pointer,
2930 ),
2931 );
2932 } else {
2933 app.sync_selection_to_primary();
2934 }
2935 sync_after_event = true;
2936 }
2937 }
2938 if handled {
2939 native.window.request_redraw();
2940 sync_after_event = true;
2941 }
2942 }
2943 WindowEvent::ModifiersChanged(modifiers) => {
2944 self.current_modifiers = modifiers.state();
2945 app.set_modifiers(app_modifiers(self.current_modifiers));
2946 }
2947 WindowEvent::MouseWheel { delta, .. } => {
2948 if native.last_cursor_position.is_none() {
2949 Self::refresh_native_cursor_from_platform_pointer(
2950 &self.native_window_platform_probe,
2951 app,
2952 native,
2953 );
2954 }
2955 if let Some(mut surface) = native_surface(app, native) {
2956 dispatch_mouse_wheel(
2957 &mut surface,
2958 &native.platform,
2959 self.current_modifiers,
2960 native.last_cursor_position,
2961 delta,
2962 );
2963 }
2964 }
2965 WindowEvent::PointerButton {
2966 state,
2967 position,
2968 button,
2969 ..
2970 } if is_primary_pointer_button(&button) => {
2971 let source = pointer_source_from_button(&button);
2972 let logical = native.platform.pointer_position(position);
2973 native.last_cursor_position = Some((logical.x, logical.y));
2974 native.last_cursor_physical_position = Some(position);
2975 trace_native_window!(
2976 "event pointer-button key={:?} state={:?} cursor={:?}",
2977 native.key,
2978 state,
2979 native.last_cursor_position
2980 );
2981 if let Some(mut surface) = native_surface(app, native) {
2982 surface.set_pointer_source(source);
2983 let platform_probe = &self.native_window_platform_probe;
2984 surface.set_screen_origin(native_window_surface_origin(
2985 platform_probe,
2986 &native.window,
2987 ));
2988 surface.set_cursor(logical.x, logical.y);
2989 }
2990 match state {
2991 ElementState::Pressed => {
2992 if self.native_global_primary_down || native.active_drag.is_some() {
2993 trace_native_window!(
2994 "event pointer-button duplicate-primary-down key={:?}",
2995 native.key
2996 );
2997 self.native_global_primary_down = true;
2998 } else {
2999 self.native_global_primary_down = true;
3000 if self.handle_native_primary_pressed(app, native) {
3001 sync_after_event = true;
3002 }
3003 }
3004 }
3005 ElementState::Released => {
3006 self.native_global_primary_down = false;
3007 let fallback_pointer =
3008 native.last_cursor_physical_position.and_then(|position| {
3009 native_window_screen_pointer_physical(
3010 &self.native_window_platform_probe,
3011 &native.window,
3012 position,
3013 )
3014 });
3015 let pointer =
3016 native_window_global_pointer_state(&self.native_window_platform_probe)
3017 .map(|state| state.position)
3018 .or(fallback_pointer);
3019 let released = Self::finish_native_press(
3020 &self.native_window_platform_probe,
3021 app,
3022 native,
3023 pointer,
3024 "local-release",
3025 |surface| {
3026 if source.is_touch_like() {
3027 surface.pointer_released_at_position(logical.x, logical.y)
3028 } else {
3029 surface.pointer_released()
3030 }
3031 },
3032 );
3033 drag_move_after_insert = released.drag_move;
3034 finish_drag_after_insert = released.finish_drag;
3035 sync_after_event |= released.sync_after_event;
3036 }
3037 }
3038 }
3039 WindowEvent::PointerButton {
3040 state: ElementState::Pressed,
3041 button: ButtonSource::Mouse(MouseButton::Middle),
3042 ..
3043 } => {
3044 Self::refresh_native_cursor_from_platform_pointer(
3045 &self.native_window_platform_probe,
3046 app,
3047 native,
3048 );
3049 if let Some(mut surface) = native_surface(app, native) {
3050 dispatch_middle_click_paste(&mut surface, native.last_cursor_position);
3051 }
3052 }
3053 WindowEvent::RedrawRequested => {
3054 let frame_cap_deadline = native.last_frame_start_time.and_then(|started_at| {
3055 native
3056 .frame_interval(app.frame_pacing_mode())
3057 .map(|interval| started_at + interval)
3058 });
3059 if defer_window_redraw(event_loop, native.presentable(), frame_cap_deadline) {
3060 return (true, NativeWindowEventSettlement::default());
3061 }
3062 Self::redraw_native_window(app, native, &self.native_window_registry);
3063 }
3064 event => {
3065 if pointer_icon_is_owed_again(&event)
3066 && let Some(surface) = native_surface(app, native)
3067 {
3068 surface.refresh_pointer_icon();
3069 }
3070 present_native_frame_owed_while_hidden(native, &event);
3071 }
3072 }
3073
3074 let settlement = NativeWindowEventSettlement {
3075 sync_after_event,
3076 drag_move: drag_move_after_insert,
3077 finish_drag: finish_drag_after_insert,
3078 };
3079 (keep_window, settlement)
3080 }
3081
3082 fn settle_native_window_event(
3083 &mut self,
3084 event_loop: &dyn ActiveEventLoop,
3085 settlement: NativeWindowEventSettlement,
3086 ) {
3087 let mut sync_after_event = settlement.sync_after_event;
3088 if let Some((key, position)) = settlement.drag_move
3089 && self.move_dragged_window(key, position)
3090 {
3091 sync_after_event = true;
3092 }
3093 if sync_after_event {
3094 self.refresh_and_sync_native_windows(event_loop);
3095 }
3096 }
3097
3098 fn redraw_native_window(
3099 app: &mut AppShell<WgpuRenderer>,
3100 native: &mut NativeWindowSurface,
3101 registry: &Rc<native_window::NativeWindowRegistry>,
3102 ) -> bool {
3103 if native_window_redraw_held_while_hidden(native.presentable()) {
3104 return false;
3105 }
3106 let frame_started_at = Instant::now();
3107 update_app_with_native_window_registry(app, registry);
3108 let after_update = Instant::now();
3109 let Some(mut surface) = native_surface(app, native) else {
3110 return false;
3111 };
3112 let frame_owed = surface.take_frame_owed();
3113 let present_required =
3114 surface_present_required(native.surface_dirty, frame_owed, surface.needs_redraw());
3115 pace_after_empty_redraw(
3116 &mut native.last_frame_start_time,
3117 &mut native.last_redraw_empty,
3118 frame_started_at,
3119 present_required,
3120 );
3121 if !present_required {
3122 return false;
3123 }
3124 native.surface_dirty = true;
3125
3126 let acquire_started_at = Instant::now();
3127 let output = match current_surface_texture(&native.surface, "native window") {
3128 SurfaceFrame::Ready(output) => output,
3129 SurfaceFrame::Reconfigure => {
3130 trace_native_window!("redraw surface outdated key={:?}", native.key);
3131 let size = native.window.surface_size();
3132 Self::resize_native_surface(app, native, size.width, size.height);
3133 if surface_reconfigure_requires_redraw(size.width, size.height) {
3134 native.window.request_redraw();
3135 }
3136 return false;
3137 }
3138 SurfaceFrame::Skip => {
3139 trace_native_window!("redraw surface unavailable key={:?}", native.key);
3140 pace_after_empty_redraw(
3141 &mut native.last_frame_start_time,
3142 &mut native.last_redraw_empty,
3143 frame_started_at,
3144 false,
3145 );
3146 return false;
3147 }
3148 };
3149 let after_acquire = Instant::now();
3150
3151 let view = output.texture.create_view(&wgpu::TextureViewDescriptor {
3152 format: Some(crate::surface_format::display_surface_view_format(
3153 native.surface_config.format,
3154 )),
3155 ..Default::default()
3156 });
3157 let Some(mut surface) = native_surface(app, native) else {
3158 return false;
3159 };
3160 if let Err(error) = surface.renderer().render_surface_texture(
3161 &output.texture,
3162 &view,
3163 native.surface_config.width,
3164 native.surface_config.height,
3165 ) {
3166 log::error!("native window render failed: {error:?}");
3167 return false;
3168 }
3169 let after_render = Instant::now();
3170
3171 native.window.pre_present_notify();
3172 surface.renderer().present(output);
3173 note_native_window_presented(native.state, true);
3174 let after_present = Instant::now();
3175 trace_native_window_timing!(
3176 "{} presented {}x{} key={:?}",
3177 native.options.title,
3178 native.surface_config.width,
3179 native.surface_config.height,
3180 native.key
3181 );
3182 native.surface_dirty = false;
3183 app.record_presented_frame(frame_started_at, after_render);
3184 log_desktop_frame_telemetry(
3185 frame_started_at,
3186 after_update,
3187 after_acquire,
3188 after_render,
3189 after_present,
3190 "native",
3191 );
3192 native.last_frame_start_time = Some(presented_frame_anchor(
3193 frame_started_at,
3194 native.frame_interval(app.frame_pacing_mode()),
3195 after_acquire.duration_since(acquire_started_at),
3196 ));
3197 let needs_frame =
3198 native_surface(app, native).is_some_and(|surface| surface.frame_schedule().needs_frame);
3199 if should_chain_no_vsync_redraw(native.frame_interval(app.frame_pacing_mode()), needs_frame)
3200 {
3201 native.window.request_redraw();
3202 }
3203 true
3204 }
3205
3206 #[cfg(feature = "robot")]
3207 fn set_robot_controller(&mut self, controller: RobotController) {
3208 self.robot_controller = Some(controller);
3209 }
3210
3211 #[cfg(feature = "robot")]
3212 fn robot_drives(&self) -> bool {
3213 self.robot_controller.is_some()
3214 }
3215
3216 #[cfg(not(feature = "robot"))]
3217 fn robot_drives(&self) -> bool {
3218 false
3219 }
3220}
3221
3222#[cfg(feature = "robot")]
3223fn begin_robot_pump_present_wait(
3224 controller: &mut RobotController,
3225 window: &Arc<dyn Window>,
3226 last_frame_start_time: &mut Option<Instant>,
3227 primary_redraw_pending: &mut bool,
3228 target: u64,
3229) {
3230 controller.begin_pump_present_wait(target);
3231 *last_frame_start_time = None;
3232 request_redraw_once(window, primary_redraw_pending);
3233}
3234
3235#[cfg(feature = "robot")]
3236fn robot_finish_without_wait(
3237 app: &mut AppShell<WgpuRenderer>,
3238 registry: &Rc<native_window::NativeWindowRegistry>,
3239 controller: &RobotController,
3240 robot_visible_surface_dirty: &mut bool,
3241) {
3242 let update_result = update_app_with_native_window_registry(app, registry);
3243 *robot_visible_surface_dirty = app.needs_redraw() || update_result.visual_changed;
3244 let _ = controller.tx.send(RobotResponse::Ok);
3245}
3246
3247#[cfg(feature = "robot")]
3248#[expect(clippy::too_many_arguments)]
3249fn robot_present_or_update(
3250 controller: &mut RobotController,
3251 window: &Arc<dyn Window>,
3252 app: &mut AppShell<WgpuRenderer>,
3253 registry: &Rc<native_window::NativeWindowRegistry>,
3254 last_frame_start_time: &mut Option<Instant>,
3255 primary_redraw_pending: &mut bool,
3256 robot_visible_surface_dirty: &mut bool,
3257 target: Option<u64>,
3258) {
3259 match target {
3260 Some(target) => begin_robot_pump_present_wait(
3261 controller,
3262 window,
3263 last_frame_start_time,
3264 primary_redraw_pending,
3265 target,
3266 ),
3267 None => robot_finish_without_wait(app, registry, controller, robot_visible_surface_dirty),
3268 }
3269}
3270
3271#[cfg(feature = "robot")]
3272fn robot_present_or_ack(
3273 controller: &mut RobotController,
3274 window: &Arc<dyn Window>,
3275 last_frame_start_time: &mut Option<Instant>,
3276 primary_redraw_pending: &mut bool,
3277 target: Option<u64>,
3278) {
3279 match target {
3280 Some(target) => begin_robot_pump_present_wait(
3281 controller,
3282 window,
3283 last_frame_start_time,
3284 primary_redraw_pending,
3285 target,
3286 ),
3287 None => {
3288 let _ = controller.tx.send(RobotResponse::Ok);
3289 }
3290 }
3291}
3292
3293#[cfg(feature = "robot")]
3294fn robot_scroll_present_target(
3295 app: &mut AppShell<WgpuRenderer>,
3296 delta_x: f32,
3297 delta_y: f32,
3298 primary_window_visible: bool,
3299 headless: bool,
3300 presented_frame_generation: u64,
3301) -> Option<Option<u64>> {
3302 let consumed = app.pointer_scrolled(delta_x, delta_y);
3303 if !consumed {
3304 return None;
3305 }
3306 Some(robot_visible_pump_present_target(
3307 primary_window_visible,
3308 headless,
3309 1,
3310 presented_frame_generation,
3311 ))
3312}
3313
3314#[cfg(feature = "robot")]
3315fn robot_frame_diagnostics(
3316 command: &RobotCommand,
3317 app: &mut AppShell<WgpuRenderer>,
3318 config: Option<&wgpu::SurfaceConfiguration>,
3319 caps: Option<&wgpu::SurfaceCapabilities>,
3320 interval: Duration,
3321) -> RobotResponse {
3322 match command {
3323 RobotCommand::GetRenderStats => {
3324 RobotResponse::RenderStats(Box::new(app.renderer().last_frame_stats()))
3325 }
3326 RobotCommand::GetFpsStats => RobotResponse::FpsStats(app.fps_stats()),
3327 RobotCommand::GetPresentationInfo => match (config, caps) {
3328 (Some(config), Some(caps)) => {
3329 RobotResponse::PresentationInfo(crate::RobotPresentationInfo {
3330 requested_mode: std::env::var("CRANPOSE_PRESENT_MODE").ok(),
3331 present_mode: crate::present_mode::resolved_present_mode(
3332 config.present_mode,
3333 caps,
3334 ),
3335 supported_modes: caps.present_modes.clone(),
3336 frame_pacing_mode: app.frame_pacing_mode(),
3337 refresh_rate_hz: 1.0 / interval.as_secs_f64(),
3338 })
3339 }
3340 _ => RobotResponse::Error("Window presentation is not configured".into()),
3341 },
3342 _ => RobotResponse::Error("Command is not a frame diagnostics query".into()),
3343 }
3344}
3345
3346fn apply_frame_pacing_mode(
3347 device: &wgpu::Device,
3348 surface: &wgpu::Surface<'static>,
3349 surface_config: &mut wgpu::SurfaceConfiguration,
3350 surface_caps: Option<&wgpu::SurfaceCapabilities>,
3351 mode: FramePacingMode,
3352 vsync_interval: Duration,
3353) {
3354 let Some(caps) = surface_caps else {
3355 log::error!(
3356 "desktop surface pacing not applied for {}: surface capabilities are unknown",
3357 mode.label()
3358 );
3359 return;
3360 };
3361 let present_mode = desktop_present_mode(caps, mode);
3362 let frame_latency = desired_frame_latency(mode, vsync_interval);
3363 if surface_config.present_mode == present_mode
3364 && surface_config.desired_maximum_frame_latency == frame_latency
3365 {
3366 return;
3367 }
3368 surface_config.present_mode = present_mode;
3369 surface_config.desired_maximum_frame_latency = frame_latency;
3370 surface.configure(device, surface_config);
3371 log_desktop_present_mode(mode, present_mode, caps);
3372}
3373
3374fn log_desktop_present_mode(
3375 mode: FramePacingMode,
3376 present_mode: wgpu::PresentMode,
3377 caps: &wgpu::SurfaceCapabilities,
3378) {
3379 log::info!(
3380 "desktop present mode: pacing={} chose {present_mode:?}; surface offers {:?}",
3381 mode.label(),
3382 caps.present_modes,
3383 );
3384}
3385
3386fn desired_frame_latency(mode: FramePacingMode, vsync_interval: Duration) -> u32 {
3387 match mode {
3388 FramePacingMode::Vsync | FramePacingMode::NoVsync => 2,
3389 FramePacingMode::Hard60 | FramePacingMode::Hard120 => {
3390 match frame_interval_for_mode(mode, vsync_interval) {
3391 Some(interval) if interval >= vsync_interval => 1,
3392 _ => 2,
3393 }
3394 }
3395 }
3396}
3397
3398fn frame_interval_for_mode(mode: FramePacingMode, vsync_interval: Duration) -> Option<Duration> {
3399 match mode {
3400 FramePacingMode::Vsync => Some(vsync_interval),
3401 FramePacingMode::Hard60 => Some(Duration::from_nanos(16_666_667)),
3402 FramePacingMode::Hard120 => Some(Duration::from_nanos(8_333_333)),
3403 FramePacingMode::NoVsync => None,
3404 }
3405}
3406
3407fn update_declaration_host_frame(
3408 app: &mut AppShell<WgpuRenderer>,
3409 registry: &Rc<native_window::NativeWindowRegistry>,
3410 last_frame_start_time: &mut Option<Instant>,
3411 frame_interval: Option<Duration>,
3412) -> FrameUpdateResult {
3413 let frame_started_at = Instant::now();
3414 let update_result = update_app_with_native_window_registry(app, registry);
3415 let presented = app.take_frame_owed();
3416 if presented {
3417 app.record_presented_frame(frame_started_at, Instant::now());
3418 }
3419 *last_frame_start_time = declaration_host_frame_anchor(
3420 *last_frame_start_time,
3421 frame_started_at,
3422 frame_interval,
3423 presented,
3424 app.frame_schedule().needs_frame,
3425 );
3426 update_result
3427}
3428
3429fn declaration_host_frame_anchor(
3430 previous: Option<Instant>,
3431 frame_started_at: Instant,
3432 interval: Option<Duration>,
3433 presented: bool,
3434 still_needs_frame: bool,
3435) -> Option<Instant> {
3436 if presented || still_needs_frame {
3437 Some(next_frame_anchor(previous, frame_started_at, interval))
3438 } else {
3439 previous
3440 }
3441}
3442
3443fn native_surface_needs_frame(
3444 needs_frame: bool,
3445 frame_owed: bool,
3446 scene_dirty: bool,
3447 surface_dirty: bool,
3448) -> bool {
3449 needs_frame || frame_owed || scene_dirty || surface_dirty
3450}
3451
3452fn should_chain_no_vsync_redraw(frame_interval: Option<Duration>, needs_frame: bool) -> bool {
3453 frame_interval.is_none() && needs_frame
3454}
3455
3456fn free_running_frame(
3457 frame_interval: Option<Duration>,
3458 needs_redraw: bool,
3459 redraw_pending: bool,
3460) -> bool {
3461 frame_interval.is_none() && (needs_redraw || redraw_pending)
3462}
3463
3464struct LoopControlInputs {
3465 robot_needs_poll: bool,
3466 free_running: bool,
3467 primary_pointer_polled: bool,
3468 drag_poll_deadline: Option<Instant>,
3469 position_poll_deadline: Option<Instant>,
3470 frame_cap_deadline: Option<Instant>,
3471 has_active_animations: bool,
3472 next_event_time: Option<Instant>,
3473}
3474
3475fn event_loop_control_flow(now: Instant, inputs: LoopControlInputs) -> ControlFlow {
3476 let due = |deadline: Option<Instant>| deadline.is_some_and(|deadline| deadline <= now);
3477 if inputs.robot_needs_poll
3478 || inputs.free_running
3479 || inputs.primary_pointer_polled
3480 || due(inputs.drag_poll_deadline)
3481 || due(inputs.position_poll_deadline)
3482 {
3483 return ControlFlow::Poll;
3484 }
3485 let deadline = [
3486 inputs.frame_cap_deadline,
3487 inputs.position_poll_deadline,
3488 inputs.drag_poll_deadline,
3489 ]
3490 .into_iter()
3491 .flatten()
3492 .min();
3493 match deadline {
3494 Some(deadline) => ControlFlow::WaitUntil(deadline),
3495 None if inputs.has_active_animations => ControlFlow::Poll,
3496 None => inputs
3497 .next_event_time
3498 .map_or(ControlFlow::Wait, ControlFlow::WaitUntil),
3499 }
3500}
3501
3502fn native_window_drag_poll_deadline(
3503 next_poll_at: Instant,
3504 global_pointer_polled: bool,
3505) -> Option<Instant> {
3506 global_pointer_polled.then_some(next_poll_at)
3507}
3508
3509fn pace_after_empty_redraw(
3510 last_frame_start_time: &mut Option<Instant>,
3511 last_redraw_empty: &mut bool,
3512 attempt_started_at: Instant,
3513 presented_something: bool,
3514) {
3515 *last_redraw_empty = !presented_something;
3516 if !presented_something {
3517 *last_frame_start_time = Some(attempt_started_at);
3518 }
3519}
3520
3521fn frame_cap_interval(
3527 mode: FramePacingMode,
3528 vsync_interval: Duration,
3529 last_redraw_empty: bool,
3530) -> Option<Duration> {
3531 frame_interval_for_mode(mode, vsync_interval)
3532 .or_else(|| last_redraw_empty.then_some(vsync_interval))
3533}
3534
3535fn primary_wrap_request(
3536 content: Option<cranpose_ui::Size>,
3537 last: Option<(u32, u32)>,
3538) -> Option<(u32, u32)> {
3539 let content = content.filter(|size| size.width > 0.0 && size.height > 0.0)?;
3540 let wanted = (content.width.ceil() as u32, content.height.ceil() as u32);
3541 (last != Some(wanted)).then_some(wanted)
3542}
3543
3544fn wrap_primary_window_to_content(
3545 window: &dyn Window,
3546 app: &mut AppShell<WgpuRenderer>,
3547 wraps_content: bool,
3548 last: Option<(u32, u32)>,
3549) -> Option<(u32, u32)> {
3550 let content = wraps_content.then(|| app.primary_content_size()).flatten();
3551 let (width, height) = primary_wrap_request(content, last)?;
3552 trace_native_window!("primary window wraps {width}x{height}");
3553 let origin = window.outer_position().ok();
3554 let _ = window.request_surface_size(LogicalSize::new(width as f64, height as f64).into());
3555 if let Some(origin) = origin {
3556 window.set_outer_position(Position::Physical(origin));
3557 }
3558 Some((width, height))
3559}
3560
3561#[expect(clippy::too_many_arguments)]
3562fn wrap_primary_window_for_frame(
3563 app: &mut AppShell<WgpuRenderer>,
3564 surface: &wgpu::Surface<'static>,
3565 surface_config: &mut wgpu::SurfaceConfiguration,
3566 window: &Arc<dyn Window>,
3567 registry: &Rc<native_window::NativeWindowRegistry>,
3568 primary_viewport_override: Option<(f32, f32)>,
3569 wraps_content: bool,
3570 last: &mut Option<(u32, u32)>,
3571) {
3572 let content = wraps_content.then(|| app.primary_content_size()).flatten();
3573 let Some((width, height)) = primary_wrap_request(content, *last) else {
3574 return;
3575 };
3576 trace_native_window!("primary window wraps {width}x{height} with the frame it drew");
3577 let origin = window.outer_position().ok();
3578 let applied = window.request_surface_size(LogicalSize::new(width as f64, height as f64).into());
3579 if let Some(origin) = origin {
3580 window.set_outer_position(Position::Physical(origin));
3581 }
3582 *last = Some((width, height));
3583 if let Some(size) = applied {
3584 let viewport = viewport_for_surface_size(
3585 primary_viewport_override,
3586 size.width,
3587 size.height,
3588 window.scale_factor(),
3589 );
3590 configure_app_surface_size(
3591 app,
3592 surface,
3593 surface_config,
3594 size.width,
3595 size.height,
3596 viewport,
3597 );
3598 update_app_with_native_window_registry(app, registry);
3599 }
3600}
3601
3602fn note_native_window_presented(state: Option<WindowState>, presented: bool) {
3603 if let Some(state) = state {
3604 state.set_presented(presented);
3605 }
3606}
3607
3608#[derive(Default)]
3609pub(crate) struct PacingDiag {
3610 presents: u32,
3611 redraw_events: u32,
3612 updates: u32,
3613 direct_updates: u32,
3614 skipped_no_present: u32,
3615 control_flow_poll: u32,
3616 control_flow_wait_until: u32,
3617 control_flow_wait: u32,
3618 window_started: Option<Instant>,
3619}
3620
3621fn pacing_diag_enabled() -> bool {
3622 static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
3623 *ENABLED.get_or_init(|| std::env::var_os("CRANPOSE_PACING_DIAG").is_some())
3624}
3625
3626fn record_pacing_event(counter: fn(&mut PacingDiag) -> &mut u32) {
3627 if !pacing_diag_enabled() {
3628 return;
3629 }
3630 crate::thread_state::with(|state| {
3631 let Ok(mut diag) = state.pacing_diag.try_borrow_mut() else {
3632 return;
3633 };
3634 *counter(&mut diag) += 1;
3635 let now = Instant::now();
3636 let started = *diag.window_started.get_or_insert(now);
3637 if now.duration_since(started) < Duration::from_secs(1) {
3638 return;
3639 }
3640 log::warn!(
3641 "[pacing] present={} redraw_events={} update={} direct_update={} no_present={} control_flow(poll={} wait_until={} wait={})",
3642 diag.presents,
3643 diag.redraw_events,
3644 diag.updates,
3645 diag.direct_updates,
3646 diag.skipped_no_present,
3647 diag.control_flow_poll,
3648 diag.control_flow_wait_until,
3649 diag.control_flow_wait,
3650 );
3651 *diag = PacingDiag {
3652 window_started: Some(now),
3653 ..PacingDiag::default()
3654 };
3655 });
3656}
3657
3658fn next_frame_anchor(
3659 previous: Option<Instant>,
3660 frame_started_at: Instant,
3661 interval: Option<Duration>,
3662) -> Instant {
3663 let (Some(previous), Some(interval)) = (previous, interval) else {
3664 return frame_started_at;
3665 };
3666 if interval.is_zero() {
3667 return frame_started_at;
3668 }
3669 let anchor = previous + interval;
3670 if frame_started_at.saturating_duration_since(anchor) >= interval {
3671 frame_started_at
3672 } else {
3673 anchor
3674 }
3675}
3676
3677fn presented_frame_anchor(
3678 anchor: Instant,
3679 interval: Option<Duration>,
3680 acquire_duration: Duration,
3681) -> Instant {
3682 match interval {
3683 Some(interval) if acquire_duration >= interval / 2 => {
3684 anchor + acquire_duration.min(interval)
3685 }
3686 _ => anchor,
3687 }
3688}
3689
3690fn logical_monitor_rects(event_loop: &dyn ActiveEventLoop) -> Vec<DesktopRect> {
3691 event_loop
3692 .available_monitors()
3693 .filter_map(|monitor| logical_monitor_rect(&monitor))
3694 .collect()
3695}
3696
3697fn logical_monitor_rect(monitor: &winit::monitor::MonitorHandle) -> Option<DesktopRect> {
3698 let position = monitor.position()?;
3699 let scale_factor = monitor.scale_factor() as f32;
3700 if scale_factor <= 0.0 {
3701 return None;
3702 }
3703 let size = monitor.current_video_mode()?.size();
3704 Some(DesktopRect {
3705 x: position.x as f32 / scale_factor,
3706 y: position.y as f32 / scale_factor,
3707 width: size.width as f32 / scale_factor,
3708 height: size.height as f32 / scale_factor,
3709 })
3710}
3711
3712fn native_window_request_bounds(
3713 requests: &[NativeWindowRequest],
3714 indices: &[usize],
3715) -> Option<DesktopRect> {
3716 let mut bounds = None::<DesktopRect>;
3717 for index in indices {
3718 let options = &requests[*index].options;
3719 let (Some(x), Some(y)) = (options.x, options.y) else {
3720 continue;
3721 };
3722 let rect = DesktopRect {
3723 x,
3724 y,
3725 width: options.width.max(1.0),
3726 height: options.height.max(1.0),
3727 };
3728 bounds = Some(match bounds {
3729 Some(current) => union_desktop_rect(current, rect),
3730 None => rect,
3731 });
3732 }
3733 bounds
3734}
3735
3736fn union_desktop_rect(a: DesktopRect, b: DesktopRect) -> DesktopRect {
3737 let x = a.x.min(b.x);
3738 let y = a.y.min(b.y);
3739 let right = a.right().max(b.right());
3740 let bottom = a.bottom().max(b.bottom());
3741 DesktopRect {
3742 x,
3743 y,
3744 width: right - x,
3745 height: bottom - y,
3746 }
3747}
3748
3749fn nearest_monitor_to_rect(monitors: &[DesktopRect], rect: DesktopRect) -> Option<DesktopRect> {
3750 let center = rect.center();
3751 monitors
3752 .iter()
3753 .min_by(|a, b| {
3754 a.distance_to_point(center)
3755 .total_cmp(&b.distance_to_point(center))
3756 })
3757 .copied()
3758}
3759
3760fn clamp_rect_to_monitor_delta(
3761 rect: DesktopRect,
3762 monitor: DesktopRect,
3763 margin: f32,
3764) -> cranpose_ui::Point {
3765 let target_x = clamped_axis_origin(rect.x, rect.width, monitor.x, monitor.width, margin);
3766 let target_y = clamped_axis_origin(rect.y, rect.height, monitor.y, monitor.height, margin);
3767 cranpose_ui::Point::new(target_x - rect.x, target_y - rect.y)
3768}
3769
3770fn clamped_axis_origin(
3771 origin: f32,
3772 length: f32,
3773 monitor_origin: f32,
3774 monitor_length: f32,
3775 margin: f32,
3776) -> f32 {
3777 let min = monitor_origin + margin;
3778 let max = monitor_origin + monitor_length - margin - length;
3779 if max >= min {
3780 origin.clamp(min, max)
3781 } else {
3782 monitor_origin + (monitor_length - length) / 2.0
3783 }
3784}
3785
3786fn a_new_window_comes_up_key(
3787 headless: bool,
3788 visible: bool,
3789 anything_focused: bool,
3790 focus: WindowFocus,
3791) -> bool {
3792 let wanted = match focus {
3793 WindowFocus::Never => false,
3794 WindowFocus::WhenNoneFocused => !anything_focused,
3795 WindowFocus::Always => true,
3796 };
3797 !headless && visible && wanted
3798}
3799
3800fn native_window_polling_drag_pointer(
3801 global: Option<NativeWindowPointerState>,
3802 start_pointer_screen: Option<PhysicalPosition<f64>>,
3803) -> Option<PhysicalPosition<f64>> {
3804 start_pointer_screen.or_else(|| global.map(|global| global.position))
3805}
3806
3807fn winit_window_icon(bitmap: &cranpose_ui::ImageBitmap) -> Option<Icon> {
3808 match RgbaIcon::new(
3809 bitmap.rgba8_pixels().into_owned(),
3810 bitmap.width(),
3811 bitmap.height(),
3812 ) {
3813 Ok(icon) => Some(icon.into()),
3814 Err(error) => {
3815 log::warn!("cranpose: the window icon is unusable: {error}");
3816 None
3817 }
3818 }
3819}
3820
3821fn with_application_icon(
3822 attributes: WindowAttributes,
3823 icon: Option<&cranpose_ui::ImageBitmap>,
3824) -> WindowAttributes {
3825 let attributes = attributes.with_window_icon(icon.and_then(winit_window_icon));
3826 #[cfg(target_os = "windows")]
3827 let attributes = {
3828 use winit::platform::windows::WindowAttributesWindows;
3829
3830 attributes.with_platform_attributes(Box::new(
3831 WindowAttributesWindows::default().with_taskbar_icon(icon.and_then(winit_window_icon)),
3832 ))
3833 };
3834 attributes
3835}
3836
3837fn native_window_attributes(
3838 options: &NativeWindowOptions,
3839 headless: bool,
3840 active: bool,
3841 window_icon: Option<&cranpose_ui::ImageBitmap>,
3842) -> WindowAttributes {
3843 let mut attributes = WindowAttributes::default()
3844 .with_active(active)
3845 .with_title(options.title.clone())
3846 .with_surface_size(LogicalSize::new(
3847 options.width.max(1.0) as f64,
3848 options.height.max(1.0) as f64,
3849 ))
3850 .with_decorations(options.decorations)
3851 .with_transparent(options.transparent)
3852 .with_resizable(options.resizable)
3853 .with_visible(!headless && options.visible)
3854 .with_window_level(native_window_level(options.always_on_top));
3855 attributes = with_application_icon(attributes, window_icon);
3856 attributes = with_native_window_shadow(attributes, options.shadow);
3857 if let (Some(width), Some(height)) = (options.min_width, options.min_height) {
3858 attributes = attributes.with_min_surface_size(LogicalSize::new(
3859 width.max(1.0) as f64,
3860 height.max(1.0) as f64,
3861 ));
3862 }
3863 if let (Some(width), Some(height)) = (options.max_width, options.max_height) {
3864 attributes = attributes.with_max_surface_size(LogicalSize::new(
3865 width.max(1.0) as f64,
3866 height.max(1.0) as f64,
3867 ));
3868 }
3869 if let (Some(x), Some(y)) = (options.x, options.y) {
3870 attributes =
3871 attributes.with_position(Position::Logical(LogicalPosition::new(x as f64, y as f64)));
3872 }
3873 attributes
3874}
3875
3876fn desktop_present_mode(
3877 surface_caps: &wgpu::SurfaceCapabilities,
3878 frame_pacing_mode: FramePacingMode,
3879) -> wgpu::PresentMode {
3880 if std::env::var_os("CRANPOSE_PRESENT_MODE").is_some() {
3881 crate::present_mode::select_present_mode(surface_caps)
3882 } else {
3883 crate::present_mode::select_present_mode_for_frame_pacing(surface_caps, frame_pacing_mode)
3884 }
3885}
3886
3887fn surface_config_for_window(
3888 surface_caps: &wgpu::SurfaceCapabilities,
3889 surface_format: wgpu::TextureFormat,
3890 width: u32,
3891 height: u32,
3892 present_mode: wgpu::PresentMode,
3893 transparent: bool,
3894 frame_latency: u32,
3895) -> Result<wgpu::SurfaceConfiguration, LaunchError> {
3896 Ok(wgpu::SurfaceConfiguration {
3897 usage: cranpose_render_wgpu::presentable_root_usages(surface_caps.usages),
3898 format: surface_format,
3899 width,
3900 height,
3901 present_mode,
3902 alpha_mode: select_alpha_mode(surface_caps, transparent)?,
3903 color_space: wgpu::SurfaceColorSpace::Auto,
3904 view_formats: crate::surface_format::display_surface_view_formats(surface_format),
3905 desired_maximum_frame_latency: frame_latency,
3906 })
3907}
3908
3909fn wgpu_renderer_for_surface(
3910 text_system: WgpuTextSystem,
3911 device: Arc<wgpu::Device>,
3912 queue: Arc<wgpu::Queue>,
3913 surface_format: wgpu::TextureFormat,
3914 backend: wgpu::Backend,
3915 downlevel: wgpu::DownlevelFlags,
3916 scale_factor: f64,
3917) -> WgpuRenderer {
3918 let mut renderer = WgpuRenderer::with_text_system(text_system);
3919 renderer.set_root_scale(scale_factor as f32);
3920 renderer.init_gpu(device, queue, surface_format, backend, downlevel);
3921 renderer
3922}
3923
3924fn select_surface_format(
3925 surface_caps: &wgpu::SurfaceCapabilities,
3926) -> Result<wgpu::TextureFormat, LaunchError> {
3927 crate::surface_format::select_display_surface_format(&surface_caps.formats)
3928 .ok_or(LaunchError::NoSurfaceFormat)
3929}
3930
3931fn select_alpha_mode(
3932 surface_caps: &wgpu::SurfaceCapabilities,
3933 transparent: bool,
3934) -> Result<wgpu::CompositeAlphaMode, LaunchError> {
3935 if transparent {
3936 return transparent_alpha_mode(&surface_caps.alpha_modes)
3937 .ok_or(LaunchError::NoSurfaceAlphaMode);
3938 }
3939
3940 surface_caps
3941 .alpha_modes
3942 .iter()
3943 .copied()
3944 .find(|mode| *mode == wgpu::CompositeAlphaMode::Opaque)
3945 .or_else(|| surface_caps.alpha_modes.first().copied())
3946 .ok_or(LaunchError::NoSurfaceAlphaMode)
3947}
3948
3949fn transparent_alpha_mode(
3950 offered: &[wgpu::CompositeAlphaMode],
3951) -> Option<wgpu::CompositeAlphaMode> {
3952 [
3953 wgpu::CompositeAlphaMode::PreMultiplied,
3954 wgpu::CompositeAlphaMode::PostMultiplied,
3955 ]
3956 .into_iter()
3957 .find(|wanted| offered.contains(wanted))
3958 .or_else(|| offered.first().copied())
3959}
3960
3961fn native_window_level(always_on_top: bool) -> WindowLevel {
3962 if always_on_top {
3963 WindowLevel::AlwaysOnTop
3964 } else {
3965 WindowLevel::Normal
3966 }
3967}
3968
3969#[cfg(target_os = "macos")]
3970fn with_native_window_shadow(attributes: WindowAttributes, shadow: bool) -> WindowAttributes {
3971 use winit::platform::macos::WindowAttributesMacOS;
3972
3973 attributes.with_platform_attributes(Box::new(
3974 WindowAttributesMacOS::default().with_has_shadow(shadow),
3975 ))
3976}
3977
3978#[cfg(not(target_os = "macos"))]
3979fn with_native_window_shadow(attributes: WindowAttributes, _shadow: bool) -> WindowAttributes {
3980 attributes
3981}
3982
3983#[cfg(target_os = "macos")]
3984fn set_native_window_shadow(window: &Arc<dyn Window>, shadow: bool) {
3985 use winit::platform::macos::WindowExtMacOS;
3986
3987 window.set_has_shadow(shadow);
3988}
3989
3990#[cfg(not(target_os = "macos"))]
3991fn set_native_window_shadow(_window: &Arc<dyn Window>, _shadow: bool) {}
3992
3993fn current_native_window_physical_position(
3994 platform_probe: &NativeWindowPlatformProbe,
3995 window: &Arc<dyn Window>,
3996) -> Option<PhysicalPosition<i32>> {
3997 native_window_x11_surface_position_physical(platform_probe, window)
3998 .map(|surface_origin| {
3999 physical_outer_origin_from_surface(surface_origin, window.surface_position())
4000 })
4001 .or_else(|| window.outer_position().ok())
4002}
4003
4004fn current_native_window_surface_physical_position(
4005 platform_probe: &NativeWindowPlatformProbe,
4006 window: &Arc<dyn Window>,
4007) -> Option<PhysicalPosition<i32>> {
4008 native_window_x11_surface_position_physical(platform_probe, window).or_else(|| {
4009 window
4010 .outer_position()
4011 .ok()
4012 .map(|outer| physical_surface_origin_from_outer(outer, window.surface_position()))
4013 })
4014}
4015
4016fn current_native_window_position(
4017 platform_probe: &NativeWindowPlatformProbe,
4018 native: &NativeWindowSurface,
4019) -> Option<(f32, f32)> {
4020 current_native_window_physical_position(platform_probe, &native.window).map(|position| {
4021 let logical = position.to_logical::<f64>(native.window.scale_factor());
4022 (logical.x as f32, logical.y as f32)
4023 })
4024}
4025
4026fn native_window_positions_close(a: (f32, f32), b: (f32, f32)) -> bool {
4027 (a.0 - b.0).abs() <= 1.0 && (a.1 - b.1).abs() <= 1.0
4028}
4029
4030fn native_window_surface_origin(
4031 platform_probe: &NativeWindowPlatformProbe,
4032 window: &Arc<dyn Window>,
4033) -> Option<cranpose_ui::Point> {
4034 let physical = current_native_window_surface_physical_position(platform_probe, window)?;
4035 let scale_factor = window.scale_factor();
4036 let logical = physical.to_logical::<f64>(scale_factor);
4037 Some(cranpose_ui::Point::new(logical.x as f32, logical.y as f32))
4038}
4039
4040fn native_window_screen_pointer_physical(
4041 platform_probe: &NativeWindowPlatformProbe,
4042 window: &Arc<dyn Window>,
4043 local: PhysicalPosition<f64>,
4044) -> Option<PhysicalPosition<f64>> {
4045 let surface = current_native_window_surface_physical_position(platform_probe, window)?;
4046 Some(PhysicalPosition::new(
4047 surface.x as f64 + local.x,
4048 surface.y as f64 + local.y,
4049 ))
4050}
4051
4052fn native_window_local_pointer_physical(
4053 platform_probe: &NativeWindowPlatformProbe,
4054 window: &Arc<dyn Window>,
4055 screen: PhysicalPosition<f64>,
4056) -> Option<PhysicalPosition<f64>> {
4057 let surface = current_native_window_surface_physical_position(platform_probe, window)?;
4058 Some(physical_surface_local_pointer(surface, screen))
4059}
4060
4061fn native_window_surface_contains_pointer(
4062 platform_probe: &NativeWindowPlatformProbe,
4063 native: &NativeWindowSurface,
4064 pointer: PhysicalPosition<f64>,
4065) -> bool {
4066 let Some(surface) =
4067 current_native_window_surface_physical_position(platform_probe, &native.window)
4068 else {
4069 return false;
4070 };
4071 physical_surface_rect_contains_pointer(
4072 surface,
4073 PhysicalPosition::new(0, 0),
4074 native.window.surface_size(),
4075 pointer,
4076 )
4077}
4078
4079fn physical_surface_origin_from_outer(
4080 outer: PhysicalPosition<i32>,
4081 surface_offset: PhysicalPosition<i32>,
4082) -> PhysicalPosition<i32> {
4083 PhysicalPosition::new(outer.x + surface_offset.x, outer.y + surface_offset.y)
4084}
4085
4086fn physical_outer_origin_from_surface(
4087 surface_origin: PhysicalPosition<i32>,
4088 surface_offset: PhysicalPosition<i32>,
4089) -> PhysicalPosition<i32> {
4090 PhysicalPosition::new(
4091 surface_origin.x - surface_offset.x,
4092 surface_origin.y - surface_offset.y,
4093 )
4094}
4095
4096fn physical_surface_local_pointer(
4097 surface_origin: PhysicalPosition<i32>,
4098 screen: PhysicalPosition<f64>,
4099) -> PhysicalPosition<f64> {
4100 PhysicalPosition::new(
4101 screen.x - surface_origin.x as f64,
4102 screen.y - surface_origin.y as f64,
4103 )
4104}
4105
4106fn recovered_native_window_drag_start_pointer(
4107 event_pointer: Option<PhysicalPosition<f64>>,
4108 global_pointer: Option<NativeWindowPointerState>,
4109) -> Option<PhysicalPosition<f64>> {
4110 event_pointer.or_else(|| global_pointer.map(|pointer| pointer.position))
4111}
4112
4113fn held_press_to_hand_over(
4114 pointer: Option<NativeWindowPointerState>,
4115 window_can_take_it: bool,
4116 belongs: PressBelongsHere,
4117 window_contains: impl FnOnce(PhysicalPosition<f64>) -> bool,
4118) -> Option<NativeWindowPointerState> {
4119 let pointer = pointer.filter(|pointer| pointer.primary_down)?;
4120 if !window_can_take_it {
4121 return None;
4122 }
4123 let takes_it = match belongs {
4124 PressBelongsHere::ItsNodeMovedHere => true,
4125 PressBelongsHere::AskTheRectangle => window_contains(pointer.position),
4126 };
4127 takes_it.then_some(pointer)
4128}
4129
4130fn primary_pointer_move_should_recover_press(
4131 active_pointer_gesture: bool,
4132 synthetic_primary_down: bool,
4133 global_pointer: Option<NativeWindowPointerState>,
4134 pointer_over_surface: bool,
4135) -> bool {
4136 !active_pointer_gesture
4137 && !synthetic_primary_down
4138 && pointer_over_surface
4139 && global_pointer.is_some_and(|pointer| pointer.primary_down)
4140}
4141
4142fn physical_surface_rect_contains_pointer(
4143 outer: PhysicalPosition<i32>,
4144 surface: PhysicalPosition<i32>,
4145 size: PhysicalSize<u32>,
4146 pointer: PhysicalPosition<f64>,
4147) -> bool {
4148 let x = outer.x as f64 + surface.x as f64;
4149 let y = outer.y as f64 + surface.y as f64;
4150 let right = x + size.width as f64;
4151 let bottom = y + size.height as f64;
4152 pointer.x >= x && pointer.x <= right && pointer.y >= y && pointer.y <= bottom
4153}
4154
4155#[derive(Clone, Copy, Debug, PartialEq)]
4156struct NativeWindowPointerState {
4157 position: PhysicalPosition<f64>,
4158 primary_down: bool,
4159}
4160
4161#[derive(Clone, Copy, Debug, Eq, PartialEq)]
4162struct HandedPress {
4163 holder: WinitWindowId,
4164 taker: WinitWindowId,
4165}
4166
4167impl HandedPress {
4168 fn taker_for(self, holder: WinitWindowId) -> Option<WinitWindowId> {
4169 (self.holder == holder).then_some(self.taker)
4170 }
4171}
4172
4173#[derive(Clone, Copy, Debug, PartialEq)]
4174enum HeldPressStep {
4175 Pressed(PhysicalPosition<f64>),
4176 Moved(PhysicalPosition<f64>),
4177 Released(PhysicalPosition<f64>),
4178}
4179
4180impl HeldPressStep {
4181 fn position(self) -> PhysicalPosition<f64> {
4182 match self {
4183 Self::Pressed(position) | Self::Moved(position) | Self::Released(position) => position,
4184 }
4185 }
4186}
4187
4188fn held_press_step(event: &WindowEvent) -> Option<HeldPressStep> {
4189 match event {
4190 WindowEvent::PointerButton {
4191 state: ElementState::Pressed,
4192 position,
4193 button,
4194 ..
4195 } if is_primary_pointer_button(button) => Some(HeldPressStep::Pressed(*position)),
4196 WindowEvent::PointerButton {
4197 state: ElementState::Released,
4198 position,
4199 button,
4200 ..
4201 } if is_primary_pointer_button(button) => Some(HeldPressStep::Released(*position)),
4202 WindowEvent::PointerMoved { position, .. } => Some(HeldPressStep::Moved(*position)),
4203 _ => None,
4204 }
4205}
4206
4207fn held_press_after_step(
4208 held: Option<PhysicalPosition<f64>>,
4209 step: HeldPressStep,
4210) -> Option<PhysicalPosition<f64>> {
4211 match step {
4212 HeldPressStep::Pressed(position) => Some(position),
4213 HeldPressStep::Moved(position) => held.map(|_| position),
4214 HeldPressStep::Released(_) => None,
4215 }
4216}
4217
4218fn native_window_press_anchor(
4219 platform_probe: &NativeWindowPlatformProbe,
4220 native: &NativeWindowSurface,
4221) -> Option<PhysicalPosition<f64>> {
4222 let local = native.last_cursor_physical_position?;
4223 native_window_screen_pointer_physical(platform_probe, &native.window, local)
4224}
4225
4226fn held_press_on_screen(
4227 platform_probe: &NativeWindowPlatformProbe,
4228 window: &Arc<dyn Window>,
4229 local: PhysicalPosition<f64>,
4230) -> Option<NativeWindowPointerState> {
4231 let position = native_window_screen_pointer_physical(platform_probe, window, local)?;
4232 Some(NativeWindowPointerState {
4233 position,
4234 primary_down: true,
4235 })
4236}
4237
4238#[derive(Clone, Copy, Debug, PartialEq)]
4239struct PressToHandOver {
4240 pointer: NativeWindowPointerState,
4241 relayed_by: Option<WinitWindowId>,
4242}
4243
4244#[derive(Clone, Copy, Debug, Eq, PartialEq)]
4246enum PressBelongsHere {
4247 ItsNodeMovedHere,
4250 AskTheRectangle,
4255}
4256
4257fn press_belongs_here(holder: Option<RootId>, window: RootId) -> PressBelongsHere {
4258 match holder == Some(window) {
4259 true => PressBelongsHere::ItsNodeMovedHere,
4260 false => PressBelongsHere::AskTheRectangle,
4261 }
4262}
4263
4264fn press_to_hand_over(
4265 platform_pointer: Option<NativeWindowPointerState>,
4266 held: Option<(WinitWindowId, NativeWindowPointerState)>,
4267 window_can_take_it: bool,
4268 belongs: PressBelongsHere,
4269 window_contains: impl FnOnce(PhysicalPosition<f64>) -> bool,
4270) -> Option<PressToHandOver> {
4271 let relayed_by = match platform_pointer {
4272 Some(_) => None,
4273 None => Some(held?.0),
4274 };
4275 let pointer = held
4276 .map(|(_, pointer)| pointer)
4277 .or(platform_pointer)
4278 .and_then(|pointer| {
4279 held_press_to_hand_over(Some(pointer), window_can_take_it, belongs, window_contains)
4280 })?;
4281 Some(PressToHandOver {
4282 pointer,
4283 relayed_by,
4284 })
4285}
4286
4287#[derive(Clone, Copy, Debug, PartialEq)]
4288enum PrimaryPointerGesturePollAction {
4289 Inactive,
4290 Pressed(NativeWindowPointerState),
4291 ReleaseAt(PhysicalPosition<f64>),
4292}
4293
4294fn primary_pointer_gesture_poll_action(
4295 active: bool,
4296 synthetic_primary_down: bool,
4297 pointer: Option<NativeWindowPointerState>,
4298) -> PrimaryPointerGesturePollAction {
4299 if !active || synthetic_primary_down {
4300 return PrimaryPointerGesturePollAction::Inactive;
4301 }
4302
4303 match pointer {
4304 Some(pointer) if pointer.primary_down => PrimaryPointerGesturePollAction::Pressed(pointer),
4305 Some(pointer) => PrimaryPointerGesturePollAction::ReleaseAt(pointer.position),
4306 None => PrimaryPointerGesturePollAction::Inactive,
4307 }
4308}
4309
4310#[cfg(all(
4311 target_os = "linux",
4312 not(target_arch = "wasm32"),
4313 feature = "desktop-x11"
4314))]
4315struct X11WindowClient {
4316 connection: x11rb::rust_connection::RustConnection,
4317 root: u32,
4318}
4319
4320#[cfg(all(
4321 target_os = "linux",
4322 not(target_arch = "wasm32"),
4323 feature = "desktop-x11"
4324))]
4325enum X11WindowClientState {
4326 Available(Box<X11WindowClient>),
4327 Unavailable,
4328}
4329
4330#[cfg(all(
4331 target_os = "linux",
4332 not(target_arch = "wasm32"),
4333 feature = "desktop-x11"
4334))]
4335struct NativeWindowPlatformProbe {
4336 x11_window_client: RefCell<Option<X11WindowClientState>>,
4337}
4338
4339#[cfg(all(
4340 target_os = "linux",
4341 not(target_arch = "wasm32"),
4342 feature = "desktop-x11"
4343))]
4344impl Default for NativeWindowPlatformProbe {
4345 fn default() -> Self {
4346 Self {
4347 x11_window_client: RefCell::new(None),
4348 }
4349 }
4350}
4351
4352#[cfg(not(all(
4353 target_os = "linux",
4354 not(target_arch = "wasm32"),
4355 feature = "desktop-x11"
4356)))]
4357#[derive(Default)]
4358struct NativeWindowPlatformProbe;
4359
4360#[cfg(all(
4361 target_os = "linux",
4362 not(target_arch = "wasm32"),
4363 feature = "desktop-x11"
4364))]
4365impl NativeWindowPlatformProbe {
4366 fn probe_x11_window_client<R>(&self, f: impl FnOnce(&X11WindowClient) -> R) -> Option<R> {
4367 if self.x11_window_client.borrow().is_none() {
4368 *self.x11_window_client.borrow_mut() =
4369 Some(X11WindowClient::connect().map(Box::new).map_or(
4370 X11WindowClientState::Unavailable,
4371 X11WindowClientState::Available,
4372 ));
4373 }
4374
4375 match self.x11_window_client.borrow().as_ref()? {
4376 X11WindowClientState::Available(client) => Some(f(client)),
4377 X11WindowClientState::Unavailable => None,
4378 }
4379 }
4380}
4381
4382#[cfg(all(
4383 target_os = "linux",
4384 not(target_arch = "wasm32"),
4385 feature = "desktop-x11"
4386))]
4387impl X11WindowClient {
4388 fn connect() -> Option<Self> {
4389 use x11rb::connection::Connection;
4390
4391 let (connection, screen_num) = x11rb::connect(None).ok()?;
4392 let root = connection.setup().roots.get(screen_num)?.root;
4393 Some(Self { connection, root })
4394 }
4395
4396 fn pointer_state(&self) -> Option<NativeWindowPointerState> {
4397 use x11rb::protocol::xproto::{ConnectionExt, KeyButMask};
4398
4399 let reply = self
4400 .connection
4401 .query_pointer(self.root)
4402 .ok()?
4403 .reply()
4404 .ok()?;
4405 let mask = u16::from(reply.mask);
4406 Some(NativeWindowPointerState {
4407 position: PhysicalPosition::new(reply.root_x as f64, reply.root_y as f64),
4408 primary_down: mask & u16::from(KeyButMask::BUTTON1) != 0,
4409 })
4410 }
4411
4412 fn configure_windows(&self, windows: &[(u32, PhysicalPosition<i32>)]) -> Option<()> {
4413 use x11rb::{
4414 connection::Connection,
4415 protocol::xproto::{ConfigureWindowAux, ConnectionExt},
4416 };
4417
4418 for (window, position) in windows {
4419 self.connection
4420 .configure_window(
4421 *window,
4422 &ConfigureWindowAux::new().x(position.x).y(position.y),
4423 )
4424 .ok()?;
4425 }
4426 self.connection.flush().ok()?;
4427 Some(())
4428 }
4429
4430 fn window_surface_position(&self, window: u32) -> Option<PhysicalPosition<i32>> {
4431 use x11rb::protocol::xproto::ConnectionExt;
4432
4433 let reply = self
4434 .connection
4435 .translate_coordinates(window, self.root, 0, 0)
4436 .ok()?
4437 .reply()
4438 .ok()?;
4439 Some(PhysicalPosition::new(
4440 reply.dst_x as i32,
4441 reply.dst_y as i32,
4442 ))
4443 }
4444}
4445
4446#[cfg(all(
4447 target_os = "linux",
4448 not(target_arch = "wasm32"),
4449 feature = "desktop-x11"
4450))]
4451fn native_window_global_pointer_state(
4452 platform_probe: &NativeWindowPlatformProbe,
4453) -> Option<NativeWindowPointerState> {
4454 platform_probe
4455 .probe_x11_window_client(X11WindowClient::pointer_state)
4456 .flatten()
4457}
4458
4459#[cfg(not(all(
4460 target_os = "linux",
4461 not(target_arch = "wasm32"),
4462 feature = "desktop-x11"
4463)))]
4464fn native_window_global_pointer_state(
4465 _platform_probe: &NativeWindowPlatformProbe,
4466) -> Option<NativeWindowPointerState> {
4467 None
4468}
4469
4470#[cfg(all(
4471 target_os = "linux",
4472 not(target_arch = "wasm32"),
4473 feature = "desktop-x11"
4474))]
4475fn native_window_x11_id(window: &Arc<dyn Window>) -> Option<u32> {
4476 use winit::raw_window_handle::{HasWindowHandle, RawWindowHandle};
4477
4478 match window.window_handle().ok()?.as_raw() {
4479 RawWindowHandle::Xlib(handle) => Some(handle.window as u32),
4480 RawWindowHandle::Xcb(handle) => Some(handle.window.get()),
4481 _ => None,
4482 }
4483}
4484
4485#[cfg(all(
4486 target_os = "linux",
4487 not(target_arch = "wasm32"),
4488 feature = "desktop-x11"
4489))]
4490fn native_window_x11_surface_position_physical(
4491 platform_probe: &NativeWindowPlatformProbe,
4492 window: &Arc<dyn Window>,
4493) -> Option<PhysicalPosition<i32>> {
4494 let window_id = native_window_x11_id(window)?;
4495 platform_probe
4496 .probe_x11_window_client(|client| client.window_surface_position(window_id))
4497 .flatten()
4498}
4499
4500#[cfg(not(all(
4501 target_os = "linux",
4502 not(target_arch = "wasm32"),
4503 feature = "desktop-x11"
4504)))]
4505fn native_window_x11_surface_position_physical(
4506 _platform_probe: &NativeWindowPlatformProbe,
4507 _window: &Arc<dyn Window>,
4508) -> Option<PhysicalPosition<i32>> {
4509 None
4510}
4511
4512#[cfg(all(
4513 target_os = "linux",
4514 not(target_arch = "wasm32"),
4515 feature = "desktop-x11"
4516))]
4517fn native_window_set_outer_position_physical(
4518 platform_probe: &NativeWindowPlatformProbe,
4519 window: &Arc<dyn Window>,
4520 position: PhysicalPosition<i32>,
4521) -> bool {
4522 let request = NativeWindowPositionRequest {
4523 window: Arc::clone(window),
4524 logical: position.to_logical(window.scale_factor()),
4525 physical: position,
4526 };
4527 native_window_set_outer_positions_physical(platform_probe, &[request])
4528}
4529
4530#[cfg(not(all(
4531 target_os = "linux",
4532 not(target_arch = "wasm32"),
4533 feature = "desktop-x11"
4534)))]
4535fn native_window_set_outer_position_physical(
4536 _platform_probe: &NativeWindowPlatformProbe,
4537 _window: &Arc<dyn Window>,
4538 _position: PhysicalPosition<i32>,
4539) -> bool {
4540 false
4541}
4542
4543#[cfg(all(
4544 target_os = "linux",
4545 not(target_arch = "wasm32"),
4546 feature = "desktop-x11"
4547))]
4548fn native_window_set_outer_positions_physical(
4549 platform_probe: &NativeWindowPlatformProbe,
4550 requests: &[NativeWindowPositionRequest],
4551) -> bool {
4552 if requests.is_empty() {
4553 return true;
4554 }
4555 platform_probe
4556 .probe_x11_window_client(|client| {
4557 let mut operations = Vec::with_capacity(requests.len());
4558 for request in requests {
4559 let Some(window_id) = native_window_x11_id(&request.window) else {
4560 return false;
4561 };
4562 operations.push((window_id, request.physical));
4563 }
4564 client.configure_windows(&operations).is_some()
4565 })
4566 .unwrap_or(false)
4567}
4568
4569#[cfg(not(all(
4570 target_os = "linux",
4571 not(target_arch = "wasm32"),
4572 feature = "desktop-x11"
4573)))]
4574fn native_window_set_outer_positions_physical(
4575 _platform_probe: &NativeWindowPlatformProbe,
4576 _requests: &[NativeWindowPositionRequest],
4577) -> bool {
4578 false
4579}
4580
4581fn wait_native_window_positions_physical(
4582 platform_probe: &NativeWindowPlatformProbe,
4583 requests: &[NativeWindowPositionRequest],
4584) {
4585 if requests.is_empty() {
4586 return;
4587 }
4588 if current_native_window_physical_position(platform_probe, &requests[0].window).is_none() {
4589 return;
4590 }
4591 let deadline = Instant::now() + NATIVE_WINDOW_POSITION_SETTLE_TIMEOUT;
4592 while Instant::now() < deadline {
4593 if requests.iter().all(|request| {
4594 current_native_window_physical_position(platform_probe, &request.window).is_some_and(
4595 |current| native_window_physical_positions_close(current, request.physical),
4596 )
4597 }) {
4598 return;
4599 }
4600 std::thread::sleep(NATIVE_WINDOW_POSITION_SETTLE_POLL);
4601 }
4602}
4603
4604fn native_window_physical_positions_close(
4605 a: PhysicalPosition<i32>,
4606 b: PhysicalPosition<i32>,
4607) -> bool {
4608 (a.x - b.x).abs() <= 1 && (a.y - b.y).abs() <= 1
4609}
4610
4611fn update_native_options_position(options: &mut NativeWindowOptions, x: f32, y: f32) {
4612 options.x = Some(x);
4613 options.y = Some(y);
4614 options.position_origin = NativeWindowPositionOrigin::Screen;
4615}
4616
4617fn update_native_options_size(
4618 options: &mut NativeWindowOptions,
4619 window: &Arc<dyn Window>,
4620 width: u32,
4621 height: u32,
4622) {
4623 let scale_factor = window.scale_factor() as f32;
4624 if scale_factor > 0.0 {
4625 options.width = width.max(1) as f32 / scale_factor;
4626 options.height = height.max(1) as f32 / scale_factor;
4627 }
4628}
4629
4630fn sync_native_window_state_position(
4631 state: Option<WindowState>,
4632 previous_position: Option<cranpose_ui::Point>,
4633 x: f32,
4634 y: f32,
4635) {
4636 let Some(state) = state else {
4637 return;
4638 };
4639 if state.position_non_reactive() == previous_position {
4640 state.set_position(Some(cranpose_ui::Point::new(x, y)));
4641 }
4642}
4643
4644fn sync_native_window_state_size(
4645 state: Option<WindowState>,
4646 previous_size: Option<cranpose_ui::Size>,
4647 window: &Arc<dyn Window>,
4648 width: u32,
4649 height: u32,
4650) {
4651 let Some(state) = state else {
4652 return;
4653 };
4654 let Some(previous_size) = previous_size else {
4655 return;
4656 };
4657 if state.size_non_reactive() != previous_size {
4658 return;
4659 }
4660 let scale_factor = window.scale_factor() as f32;
4661 if scale_factor > 0.0 {
4662 state.set_size(cranpose_ui::Size::new(
4663 width.max(1) as f32 / scale_factor,
4664 height.max(1) as f32 / scale_factor,
4665 ));
4666 }
4667}
4668
4669fn native_window_options_change_is_position_only(
4670 previous: &NativeWindowOptions,
4671 next: &NativeWindowOptions,
4672) -> bool {
4673 if previous == next {
4674 return false;
4675 }
4676
4677 let mut previous = previous.clone();
4678 let next = next.clone();
4679 previous.x = next.x;
4680 previous.y = next.y;
4681 previous.position_origin = next.position_origin;
4682 previous == next
4683}
4684
4685fn native_window_position_poll_needed(
4686 visible: bool,
4687 active_drag: bool,
4688 pending_programmatic_position: bool,
4689) -> bool {
4690 visible && !active_drag && pending_programmatic_position
4691}
4692
4693fn native_window_timing_enabled() -> bool {
4694 static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
4695 *ENABLED.get_or_init(|| std::env::var_os("CRANPOSE_NATIVE_WINDOW_TIMING").is_some())
4696}
4697
4698fn print_native_window_timing(args: std::fmt::Arguments<'_>) {
4699 println!(
4700 "native window timing: t={:.1}ms {args}",
4701 timing_trace_clock().elapsed().as_secs_f64() * 1000.0
4702 );
4703}
4704
4705fn timing_trace_clock() -> Instant {
4706 static START: std::sync::OnceLock<Instant> = std::sync::OnceLock::new();
4707 *START.get_or_init(Instant::now)
4708}
4709
4710fn native_window_trace_enabled() -> bool {
4711 static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
4712 *ENABLED.get_or_init(|| std::env::var_os("CRANPOSE_NATIVE_TRACE").is_some())
4713}
4714
4715fn print_native_window_trace(args: std::fmt::Arguments<'_>) {
4716 println!("native window trace: {args}");
4717}
4718
4719fn primary_surface_redraw_drives_app(primary_window_visible: bool, headless: bool) -> bool {
4720 primary_window_visible && !headless
4721}
4722
4723fn primary_window_should_show(headless: bool, has_content: bool) -> bool {
4724 !headless && has_content
4725}
4726
4727fn show_primary_when_it_has_content(
4728 shown: &std::sync::atomic::AtomicBool,
4729 app: &mut AppShell<WgpuRenderer>,
4730 window: &dyn Window,
4731 headless: bool,
4732) {
4733 let show = primary_window_should_show(headless, app.primary_has_content());
4734 if show != shown.load(std::sync::atomic::Ordering::Relaxed) {
4735 shown.store(show, std::sync::atomic::Ordering::Relaxed);
4736 trace_native_window!("primary window {}", if show { "shown" } else { "hidden" });
4737 window.set_visible(show);
4738 }
4739}
4740
4741fn primary_frame_waker_uses_event_proxy(
4742 on_event_loop_thread: bool,
4743 primary_window_visible: bool,
4744 headless: bool,
4745) -> bool {
4746 !on_event_loop_thread || !primary_surface_redraw_drives_app(primary_window_visible, headless)
4747}
4748
4749fn primary_frame_waker(
4750 event_loop_thread: std::thread::ThreadId,
4751 primary_shown: Arc<std::sync::atomic::AtomicBool>,
4752 headless: bool,
4753 request_redraw: impl Fn() + Send + Sync + 'static,
4754 wake_event_loop: impl Fn() + Send + Sync + 'static,
4755) -> impl Fn() + Send + Sync + 'static {
4756 move || {
4757 let on_event_loop_thread = std::thread::current().id() == event_loop_thread;
4758 let shown = primary_shown.load(std::sync::atomic::Ordering::Relaxed);
4759 if primary_frame_waker_uses_event_proxy(on_event_loop_thread, shown, headless) {
4760 wake_event_loop();
4761 } else {
4762 request_redraw();
4763 }
4764 }
4765}
4766
4767fn primary_launch_requires_initial_redraw(primary_window_visible: bool, headless: bool) -> bool {
4768 primary_surface_redraw_drives_app(primary_window_visible, headless)
4769}
4770
4771fn surface_reconfigure_requires_redraw(width: u32, height: u32) -> bool {
4772 width > 0 && height > 0
4773}
4774
4775fn occlusion_leaves_a_frame_owed(occluded: bool) -> bool {
4776 !occluded
4777}
4778
4779fn window_shown_again_takes_a_held_press(was_hidden: bool, visible_now: bool) -> bool {
4780 was_hidden && visible_now
4781}
4782
4783fn native_window_lifecycle_event(event: &WindowEvent) -> Option<&'static str> {
4784 Some(match event {
4785 WindowEvent::Occluded(true) => "occluded",
4786 WindowEvent::Occluded(false) => "unoccluded",
4787 WindowEvent::Focused(true) => "focused",
4788 WindowEvent::Focused(false) => "unfocused",
4789 WindowEvent::Moved(_) => "moved",
4790 WindowEvent::SurfaceResized(_) => "resized",
4791 WindowEvent::ScaleFactorChanged { .. } => "rescaled",
4792 WindowEvent::PointerEntered { .. } => "pointer-entered",
4793 WindowEvent::PointerLeft { .. } => "pointer-left",
4794 WindowEvent::CloseRequested => "close-requested",
4795 _ => return None,
4796 })
4797}
4798
4799fn native_window_redraw_held_while_hidden(visible: bool) -> bool {
4800 !visible
4801}
4802
4803fn pointer_icon_is_owed_again(event: &WindowEvent) -> bool {
4804 matches!(
4806 event,
4807 WindowEvent::Focused(true)
4808 | WindowEvent::PointerEntered { .. }
4809 | WindowEvent::ScaleFactorChanged { .. }
4810 )
4811}
4812
4813fn restore_pointer_icon_the_window_system_drew_over(
4814 app: Option<&AppShell<WgpuRenderer>>,
4815 event: &WindowEvent,
4816) {
4817 if let Some(app) = app.filter(|_| pointer_icon_is_owed_again(event)) {
4818 app.refresh_pointer_icon();
4819 }
4820}
4821
4822fn primary_window_focus_changed(app: &mut AppShell<WgpuRenderer>, focused: bool) {
4823 if focused {
4824 app.set_active_root(RootId::Primary);
4825 app.refresh_pointer_icon();
4826 } else {
4827 cancel_app_input(app);
4828 }
4829}
4830
4831fn native_window_focused(app: &mut AppShell<WgpuRenderer>, native: &NativeWindowSurface) {
4832 app.set_active_root(native.root_id());
4833 if let Some(surface) = native_surface(app, native) {
4834 surface.refresh_pointer_icon();
4835 }
4836}
4837
4838fn deliver_native_surface_event(
4839 app: &mut AppShell<WgpuRenderer>,
4840 native: &NativeWindowSurface,
4841 current_modifiers: winit::keyboard::ModifiersState,
4842 event: WindowEvent,
4843) -> Option<WindowEvent> {
4844 let Some(mut surface) = native_surface(app, native) else {
4845 return Some(event);
4846 };
4847 match event {
4848 WindowEvent::KeyboardInput { event, .. } => {
4849 dispatch_keyboard_input(&mut surface, current_modifiers, event);
4850 }
4851 WindowEvent::Ime(ime_event) => dispatch_ime_event(&mut surface, ime_event),
4852 WindowEvent::Focused(false) if native.active_drag.is_none() => {
4853 cancel_surface_input(&mut surface);
4854 }
4855 WindowEvent::PointerLeft { .. } if native.active_drag.is_none() => {
4856 surface.cancel_gesture_unless_pressed();
4857 }
4858 event => return Some(event),
4859 }
4860 None
4861}
4862
4863fn present_native_frame_owed_while_hidden(native: &mut NativeWindowSurface, event: &WindowEvent) {
4864 if let WindowEvent::Occluded(occluded) = event
4865 && occlusion_leaves_a_frame_owed(*occluded)
4866 {
4867 native.surface_dirty = true;
4868 native.window.request_redraw();
4869 }
4870}
4871
4872fn present_primary_frame_owed_while_hidden(
4873 event: &WindowEvent,
4874 window: &Arc<dyn Window>,
4875 surface_dirty: &mut bool,
4876 redraw_pending: &mut bool,
4877) {
4878 if let WindowEvent::Occluded(occluded) = event
4879 && occlusion_leaves_a_frame_owed(*occluded)
4880 {
4881 *surface_dirty = true;
4882 *redraw_pending = false;
4883 request_redraw_once(window, redraw_pending);
4884 }
4885}
4886
4887fn initial_present_redraw_needed(initial_present_pending: bool, redraw_pending: bool) -> bool {
4888 initial_present_pending && !redraw_pending
4889}
4890
4891fn primary_declaration_host_needs_direct_update(
4892 primary_window_visible: bool,
4893 headless: bool,
4894 needs_redraw: bool,
4895 waiting_for_frame_cap: bool,
4896) -> bool {
4897 needs_redraw
4898 && !waiting_for_frame_cap
4899 && !primary_surface_redraw_drives_app(primary_window_visible, headless)
4900}
4901
4902#[cfg(feature = "robot")]
4903fn robot_visible_present_target(
4904 primary_window_visible: bool,
4905 headless: bool,
4906 surface_dirty: bool,
4907 presented_frame_generation: u64,
4908) -> Option<u64> {
4909 (surface_dirty && primary_surface_redraw_drives_app(primary_window_visible, headless))
4910 .then(|| presented_frame_generation.saturating_add(1))
4911}
4912
4913#[cfg(feature = "robot")]
4914fn robot_visible_pump_present_target(
4915 primary_window_visible: bool,
4916 headless: bool,
4917 frame_count: u32,
4918 presented_frame_generation: u64,
4919) -> Option<u64> {
4920 (frame_count > 0 && primary_surface_redraw_drives_app(primary_window_visible, headless))
4921 .then(|| presented_frame_generation.saturating_add(frame_count as u64))
4922}
4923
4924#[derive(Clone, Copy, Debug, Eq, PartialEq)]
4925struct PointerButtonFrameRequest {
4926 request_redraw: bool,
4927 reset_frame_cap: bool,
4928}
4929
4930fn pointer_button_frame_request(input_handled: bool) -> PointerButtonFrameRequest {
4931 immediate_input_frame_request(input_handled)
4932}
4933
4934fn scroll_frame_request(input_handled: bool) -> PointerButtonFrameRequest {
4935 immediate_input_frame_request(input_handled)
4936}
4937
4938fn immediate_input_frame_request(input_handled: bool) -> PointerButtonFrameRequest {
4939 PointerButtonFrameRequest {
4940 request_redraw: input_handled,
4941 reset_frame_cap: input_handled,
4942 }
4943}
4944
4945fn request_redraw_once(window: &Arc<dyn Window>, redraw_pending: &mut bool) {
4946 if *redraw_pending {
4947 return;
4948 }
4949 *redraw_pending = true;
4950 window.request_redraw();
4951}
4952
4953fn apply_pointer_button_frame_request(
4954 window: &Arc<dyn Window>,
4955 last_frame_start_time: &mut Option<Instant>,
4956 request: PointerButtonFrameRequest,
4957) {
4958 if request.reset_frame_cap {
4959 *last_frame_start_time = None;
4960 }
4961 if request.request_redraw {
4962 window.request_redraw();
4963 }
4964}
4965
4966fn apply_primary_pointer_button_frame_request(
4967 window: &Arc<dyn Window>,
4968 last_frame_start_time: &mut Option<Instant>,
4969 redraw_pending: &mut bool,
4970 request: PointerButtonFrameRequest,
4971) {
4972 if request.reset_frame_cap {
4973 *last_frame_start_time = None;
4974 }
4975 if request.request_redraw {
4976 request_redraw_once(window, redraw_pending);
4977 }
4978}
4979
4980fn configure_app_surface_size(
4981 app: &mut AppShell<WgpuRenderer>,
4982 surface: &wgpu::Surface<'static>,
4983 surface_config: &mut wgpu::SurfaceConfiguration,
4984 width: u32,
4985 height: u32,
4986 viewport: (f32, f32),
4987) {
4988 if width == 0 || height == 0 {
4989 return;
4990 }
4991
4992 let Some(device) = app.renderer().try_device() else {
4993 log::error!("desktop surface resize skipped: GPU renderer is not initialized");
4994 return;
4995 };
4996 surface_config.width = width;
4997 surface_config.height = height;
4998 surface.configure(device, surface_config);
4999 update_app_viewport(app, width, height, viewport);
5000}
5001
5002fn update_app_viewport(
5003 app: &mut AppShell<WgpuRenderer>,
5004 width: u32,
5005 height: u32,
5006 viewport: (f32, f32),
5007) {
5008 let (logical_width, logical_height) = viewport;
5009 app.set_buffer_size(width, height);
5010 app.set_viewport(logical_width, logical_height);
5011 cranpose_services::publish_host_surface_size(
5012 cranpose_services::host_surface::HostSurfaceSize {
5013 width: logical_width,
5014 height: logical_height,
5015 scale: if logical_width > 0.0 {
5016 width as f32 / logical_width
5017 } else {
5018 1.0
5019 },
5020 },
5021 );
5022}
5023
5024fn surface_logical_viewport_size(width: u32, height: u32, scale_factor: f64) -> (f32, f32) {
5025 (
5026 width as f32 / scale_factor as f32,
5027 height as f32 / scale_factor as f32,
5028 )
5029}
5030
5031fn headless_requested_viewport(settings: &AppSettings) -> Option<(f32, f32)> {
5032 settings.headless.then_some((
5033 settings.initial_width.max(1) as f32,
5034 settings.initial_height.max(1) as f32,
5035 ))
5036}
5037
5038fn viewport_for_surface_size(
5039 requested_viewport: Option<(f32, f32)>,
5040 width: u32,
5041 height: u32,
5042 scale_factor: f64,
5043) -> (f32, f32) {
5044 requested_viewport.unwrap_or_else(|| surface_logical_viewport_size(width, height, scale_factor))
5045}
5046
5047#[expect(clippy::too_many_arguments)]
5048fn apply_primary_surface_resize(
5049 app: &mut AppShell<WgpuRenderer>,
5050 surface: &wgpu::Surface<'static>,
5051 surface_config: &mut wgpu::SurfaceConfiguration,
5052 window: &Arc<dyn Window>,
5053 primary_viewport_override: Option<(f32, f32)>,
5054 width: u32,
5055 height: u32,
5056 primary_surface_dirty: &mut bool,
5057 primary_redraw_pending: &mut bool,
5058) {
5059 if width == 0 || height == 0 {
5060 return;
5061 }
5062 let viewport = viewport_for_surface_size(
5063 primary_viewport_override,
5064 width,
5065 height,
5066 window.scale_factor(),
5067 );
5068 configure_app_surface_size(app, surface, surface_config, width, height, viewport);
5069 *primary_surface_dirty = true;
5070 *primary_redraw_pending = false;
5071 request_redraw_once(window, primary_redraw_pending);
5072}
5073
5074fn primary_viewport_for_surface_size(
5075 settings: &AppSettings,
5076 width: u32,
5077 height: u32,
5078 scale_factor: f64,
5079) -> (f32, f32) {
5080 viewport_for_surface_size(
5081 headless_requested_viewport(settings),
5082 width,
5083 height,
5084 scale_factor,
5085 )
5086}
5087
5088fn update_app_scale_factor(
5089 app: &mut AppShell<WgpuRenderer>,
5090 platform: &mut DesktopWinitPlatform,
5091 scale_factor: f64,
5092) {
5093 platform.set_scale_factor(scale_factor);
5094 app.set_density(scale_factor as f32);
5095}
5096
5097fn dispatch_mouse_wheel(
5098 surface: &mut SurfaceMut<'_, WgpuRenderer>,
5099 platform: &DesktopWinitPlatform,
5100 current_modifiers: winit::keyboard::ModifiersState,
5101 cursor_position: Option<(f32, f32)>,
5102 delta: winit::event::MouseScrollDelta,
5103) -> bool {
5104 let cursor_dirty = if let Some((x, y)) = cursor_position {
5105 surface.set_cursor(x, y)
5106 } else {
5107 false
5108 };
5109
5110 let scroll_dirty = platform.scroll_delta(delta).is_some_and(|logical_delta| {
5111 surface.wheel_scrolled(crate::winit_wheel::wheel_scroll_from_winit(
5112 logical_delta,
5113 current_modifiers,
5114 wheel_uptime_millis(),
5115 ))
5116 });
5117 cursor_dirty || scroll_dirty
5118}
5119
5120fn wheel_uptime_millis() -> u64 {
5121 use std::sync::OnceLock;
5122 static EPOCH: OnceLock<web_time::Instant> = OnceLock::new();
5123 EPOCH
5124 .get_or_init(web_time::Instant::now)
5125 .elapsed()
5126 .as_millis() as u64
5127}
5128
5129fn dispatch_middle_click_paste(
5130 surface: &mut SurfaceMut<'_, WgpuRenderer>,
5131 cursor_position: Option<(f32, f32)>,
5132) {
5133 if let Some((x, y)) = cursor_position {
5134 surface.set_cursor(x, y);
5135 }
5136 #[cfg(all(
5137 not(target_arch = "wasm32"),
5138 not(target_os = "android"),
5139 not(target_os = "ios")
5140 ))]
5141 if let Some(text) = surface.shell().get_primary_selection() {
5142 surface.on_paste(&text);
5143 }
5144}
5145
5146fn cancel_app_input(app: &mut AppShell<WgpuRenderer>) {
5147 cancel_surface_input(&mut app.primary());
5148}
5149
5150fn cancel_surface_input(surface: &mut SurfaceMut<'_, WgpuRenderer>) {
5151 surface.cancel_gesture();
5152 let _ = surface.on_ime_preedit("", None);
5153}
5154
5155fn logical_outer_position(window: &Arc<dyn Window>) -> Option<(f32, f32)> {
5156 window.outer_position().ok().map(|position| {
5157 let logical = position.to_logical::<f64>(window.scale_factor());
5158 (logical.x as f32, logical.y as f32)
5159 })
5160}
5161
5162fn notify_native_window_moved(events: &NativeWindowEvents, x: f32, y: f32) {
5163 if let Some(on_moved) = &events.on_moved {
5164 on_moved(x, y);
5165 }
5166}
5167
5168fn tell_the_application_the_window_frame(state: Option<WindowState>, window: &Arc<dyn Window>) {
5169 let Some(state) = state else {
5170 return;
5171 };
5172 let scale = window.scale_factor() as f32;
5173 if scale <= 0.0 {
5174 return;
5175 }
5176 let outer = window.outer_size();
5177 state.set_frame_size(cranpose_ui::Size::new(
5178 outer.width as f32 / scale,
5179 outer.height as f32 / scale,
5180 ));
5181}
5182
5183fn tell_the_application_the_window_moved(
5184 events: &NativeWindowEvents,
5185 state: Option<WindowState>,
5186 x: f32,
5187 y: f32,
5188) {
5189 let already_knows = state
5190 .and_then(WindowState::position_non_reactive)
5191 .is_some_and(|known| (known.x - x).abs() <= 0.5 && (known.y - y).abs() <= 0.5);
5192 if already_knows {
5193 return;
5194 }
5195 notify_native_window_moved(events, x, y);
5196}
5197
5198fn notify_native_window_resized(
5199 events: &NativeWindowEvents,
5200 window: &Arc<dyn Window>,
5201 width: u32,
5202 height: u32,
5203) {
5204 if let Some(on_resized) = &events.on_resized {
5205 let scale_factor = window.scale_factor() as f32;
5206 if scale_factor > 0.0 {
5207 on_resized(width as f32 / scale_factor, height as f32 / scale_factor);
5208 }
5209 }
5210}
5211
5212fn notify_native_window_close_requested(events: &NativeWindowEvents) {
5213 if let Some(on_close_requested) = &events.on_close_requested {
5214 on_close_requested();
5215 }
5216}
5217
5218fn native_resize_direction(direction: WindowResizeDirection) -> ResizeDirection {
5219 match direction {
5220 WindowResizeDirection::East => ResizeDirection::East,
5221 WindowResizeDirection::North => ResizeDirection::North,
5222 WindowResizeDirection::NorthEast => ResizeDirection::NorthEast,
5223 WindowResizeDirection::NorthWest => ResizeDirection::NorthWest,
5224 WindowResizeDirection::South => ResizeDirection::South,
5225 WindowResizeDirection::SouthEast => ResizeDirection::SouthEast,
5226 WindowResizeDirection::SouthWest => ResizeDirection::SouthWest,
5227 WindowResizeDirection::West => ResizeDirection::West,
5228 }
5229}
5230
5231fn default_vsync_interval() -> Duration {
5232 Duration::from_nanos(16_666_667)
5233}
5234
5235fn system_theme_from_winit(theme: winit::window::Theme) -> cranpose_services::SystemTheme {
5236 match theme {
5237 winit::window::Theme::Dark => cranpose_services::SystemTheme::Dark,
5238 winit::window::Theme::Light => cranpose_services::SystemTheme::Light,
5239 }
5240}
5241
5242fn monitor_refresh_interval(window: &Arc<dyn Window>) -> Duration {
5243 window
5244 .current_monitor()
5245 .and_then(|monitor| monitor.current_video_mode())
5246 .and_then(|mode| mode.refresh_rate_millihertz())
5247 .map_or_else(default_vsync_interval, |millihertz| {
5248 let nanos = 1_000_000_000_000u64 / u64::from(millihertz.get());
5249 Duration::from_nanos(nanos)
5250 })
5251}
5252
5253fn dispatch_ime_event(surface: &mut SurfaceMut<'_, WgpuRenderer>, ime_event: winit::event::Ime) {
5254 use winit::event::Ime;
5255
5256 match ime_event {
5257 Ime::Preedit(text, cursor) => {
5258 surface.on_ime_preedit(&text, cursor);
5259 }
5260 Ime::Commit(text) => {
5261 let _ = surface.on_ime_preedit("", None);
5262 surface.on_paste(&text);
5263 }
5264 Ime::Enabled => {}
5265 Ime::Disabled => {
5266 surface.on_ime_preedit("", None);
5267 }
5268 Ime::DeleteSurrounding {
5269 before_bytes,
5270 after_bytes,
5271 } => {
5272 let _ = surface.on_ime_delete_surrounding(before_bytes, after_bytes);
5273 }
5274 _ => {}
5275 }
5276}
5277
5278struct DesktopTextInput {
5279 window: std::sync::Weak<dyn Window>,
5280}
5281
5282impl DesktopTextInput {
5283 fn install(surface: &mut SurfaceMut<'_, WgpuRenderer>, window: &Arc<dyn Window>) {
5284 surface.set_platform_text_input(Rc::new(DesktopTextInput {
5285 window: Arc::downgrade(window),
5286 }));
5287 }
5288}
5289
5290impl cranpose_app_shell::PlatformTextInputHandler for DesktopTextInput {
5291 fn show_keyboard(&self) {
5292 use winit::window::{ImeCapabilities, ImeEnableRequest, ImeRequest, ImeRequestData};
5293
5294 let Some(window) = self.window.upgrade() else {
5295 return;
5296 };
5297 let Some(enable) = ImeEnableRequest::new(ImeCapabilities::new(), ImeRequestData::default())
5298 else {
5299 return;
5300 };
5301 match window.request_ime_update(ImeRequest::Enable(enable)) {
5302 Ok(()) | Err(winit::window::ImeRequestError::AlreadyEnabled) => {}
5303 Err(error) => log::debug!("winit IME enable failed: {error}"),
5304 }
5305 }
5306
5307 fn hide_keyboard(&self) {
5308 use winit::window::{ImeRequest, ImeRequestError};
5309
5310 let Some(window) = self.window.upgrade() else {
5311 return;
5312 };
5313 match window.request_ime_update(ImeRequest::Disable) {
5314 Ok(()) | Err(ImeRequestError::NotEnabled) => {}
5315 Err(error) => log::debug!("winit IME disable failed: {error}"),
5316 }
5317 }
5318}
5319
5320impl App {
5321 fn observe_presence(&mut self, window_id: WinitWindowId, event: &WindowEvent) {
5322 if let Some(native) = self.native_windows.get_mut(&window_id) {
5323 match event {
5324 WindowEvent::Occluded(occluded) => native.occluded = *occluded,
5325 WindowEvent::SurfaceResized(size) => {
5326 native.minimized = size.width == 0 || size.height == 0;
5327 }
5328 _ => {}
5329 }
5330 }
5331 if let Some(presence) = &mut self.primary_presence {
5332 presence.observe(window_id, event);
5333 }
5334 }
5335
5336 fn publish_settled_presence(&mut self) {
5337 if let Some(state) = self
5338 .primary_presence
5339 .as_mut()
5340 .and_then(crate::desktop_lifecycle::WindowPresence::settled)
5341 {
5342 cranpose_services::advance_lifecycle(state);
5343 }
5344 }
5345}
5346
5347impl Drop for App {
5348 fn drop(&mut self) {
5349 cranpose_services::advance_lifecycle(cranpose_services::LifecycleState::Destroyed);
5350 }
5351}
5352
5353impl ApplicationHandler for App {
5354 fn proxy_wake_up(&mut self, event_loop: &dyn ActiveEventLoop) {
5355 if self.exiting {
5356 return;
5357 }
5358 #[cfg(feature = "robot")]
5359 if self
5360 .robot_controller
5361 .as_mut()
5362 .is_some_and(RobotController::stage_pending_command)
5363 {
5364 self.about_to_wait(event_loop);
5365 return;
5366 }
5367 self.handle_primary_frame_requested(event_loop);
5368 }
5369
5370 fn can_create_surfaces(&mut self, event_loop: &dyn ActiveEventLoop) {
5371 if self.window.is_some() {
5372 return;
5373 }
5374
5375 let initial_width = self.settings.initial_width;
5376 let initial_height = self.settings.initial_height;
5377 let headless = self.settings.headless;
5378
5379 let window: Arc<dyn Window> = match event_loop.create_window(with_application_icon(
5380 WindowAttributes::default()
5381 .with_title(self.settings.window_title.clone())
5382 .with_surface_size(LogicalSize::new(
5383 initial_width as f64,
5384 initial_height as f64,
5385 ))
5386 .with_visible(false),
5387 self.settings.window_icon.as_ref(),
5388 )) {
5389 Ok(window) => window.into(),
5390 Err(error) => {
5391 self.abort_launch(event_loop, LaunchError::WindowCreate(error));
5392 return;
5393 }
5394 };
5395
5396 let (instance, surface, adapter) =
5397 match crate::wgpu_surface::create_wgpu_surface_and_adapter(&window) {
5398 Ok(triple) => triple,
5399 Err(error) => {
5400 self.abort_launch(event_loop, error);
5401 return;
5402 }
5403 };
5404 let adapter_info = adapter.get_info();
5405 self.vsync_interval = monitor_refresh_interval(&window);
5406
5407 let (device, queue) = match pollster::block_on(
5408 adapter.request_device(&wgpu::DeviceDescriptor {
5409 label: Some("Main Device"),
5410 required_features: cranpose_render_wgpu::optional_device_features(&adapter)
5411 | self.settings.gpu_requirements.features_on(&adapter),
5412 required_limits: self
5413 .settings
5414 .gpu_requirements
5415 .limits_on(&adapter, wgpu::Limits::default()),
5416 experimental_features: wgpu::ExperimentalFeatures::disabled(),
5417 memory_hints: wgpu::MemoryHints::default(),
5418 trace: wgpu::Trace::Off,
5419 }),
5420 ) {
5421 Ok(pair) => pair,
5422 Err(error) => {
5423 self.abort_launch(event_loop, LaunchError::DeviceCreate(error));
5424 return;
5425 }
5426 };
5427
5428 let size = window.surface_size();
5429 let surface_caps = surface.get_capabilities(&adapter);
5430 let surface_format = match select_surface_format(&surface_caps) {
5431 Ok(format) => format,
5432 Err(error) => {
5433 self.abort_launch(event_loop, error);
5434 return;
5435 }
5436 };
5437
5438 let present_mode = desktop_present_mode(&surface_caps, self.frame_pacing_mode());
5439 log_desktop_present_mode(self.frame_pacing_mode(), present_mode, &surface_caps);
5440 let surface_config = match surface_config_for_window(
5441 &surface_caps,
5442 surface_format,
5443 size.width.max(1),
5444 size.height.max(1),
5445 present_mode,
5446 false,
5447 desired_frame_latency(self.frame_pacing_mode(), self.vsync_interval),
5448 ) {
5449 Ok(config) => config,
5450 Err(error) => {
5451 self.abort_launch(event_loop, error);
5452 return;
5453 }
5454 };
5455
5456 let device = Arc::new(device);
5457 let queue = Arc::new(queue);
5458
5459 surface.configure(&device, &surface_config);
5460 present_initial_placeholder_frame(
5461 &surface,
5462 &device,
5463 &queue,
5464 surface_format,
5465 "primary window initial present",
5466 );
5467
5468 if let Some(theme) = window.theme() {
5472 self.platform_env
5473 .set_system_theme(system_theme_from_winit(theme));
5474 }
5475
5476 let text_system = WgpuTextSystem::from_font_set(self.settings.resolve_font_set());
5477 let initial_scale = window.scale_factor();
5478 let renderer = wgpu_renderer_for_surface(
5479 text_system.clone(),
5480 Arc::clone(&device),
5481 Arc::clone(&queue),
5482 crate::surface_format::display_surface_view_format(surface_format),
5483 adapter_info.backend,
5484 adapter.get_downlevel_capabilities().flags,
5485 initial_scale,
5486 );
5487
5488 let viewport = primary_viewport_for_surface_size(
5489 &self.settings,
5490 size.width,
5491 size.height,
5492 initial_scale,
5493 );
5494
5495 let Some(content) = self.content.take() else {
5496 self.abort_launch(event_loop, LaunchError::ContentUnavailable);
5497 return;
5498 };
5499 let gpu = crate::GpuContext {
5500 instance,
5501 adapter,
5502 device,
5503 queue,
5504 };
5505 let content = crate::gpu_context::provide_gpu_context(gpu.clone(), content);
5506 let registry = Rc::clone(&self.native_window_registry);
5507 let mut app = native_window::with_native_window_registry(®istry, || {
5508 AppShell::new_with_size_and_density(
5509 renderer,
5510 default_root_key(),
5511 content,
5512 (size.width, size.height),
5513 viewport,
5514 initial_scale as f32,
5515 )
5516 });
5517 app.set_semantics_enabled(true);
5518 crate::accessibility::install_inspector(&mut app, self.settings.developer_inspector);
5519
5520 let mut accessibility = crate::desktop_accessibility::DesktopAccessibilityBridge::new(
5521 window.as_ref(),
5522 self.event_proxy.clone(),
5523 self.robot_drives(),
5524 );
5525 accessibility.sync(&mut app);
5526 self.primary_wrap_size = wrap_primary_window_to_content(
5527 window.as_ref(),
5528 &mut app,
5529 self.settings.primary_wraps_content,
5530 self.primary_wrap_size,
5531 )
5532 .or(self.primary_wrap_size);
5533 show_primary_when_it_has_content(&self.primary_shown, &mut app, window.as_ref(), headless);
5534
5535 let mut dev_options = self.settings.dev_options.clone();
5536 dev_options.frame_pacing_mode = self.frame_pacing_mode();
5537 app.set_dev_options(dev_options);
5538
5539 DesktopTextInput::install(&mut app.primary(), &window);
5540 app.set_screen_origin(native_window_surface_origin(
5541 &self.native_window_platform_probe,
5542 &window,
5543 ));
5544
5545 let frame_waker_window = window.clone();
5546 let frame_waker_event_proxy = self.event_proxy.clone();
5547 app.set_frame_waker(primary_frame_waker(
5548 std::thread::current().id(),
5549 Arc::clone(&self.primary_shown),
5550 headless,
5551 move || frame_waker_window.request_redraw(),
5552 move || frame_waker_event_proxy.wake_up(),
5553 ));
5554
5555 let mut platform = DesktopWinitPlatform::default();
5556 platform.set_scale_factor(initial_scale);
5557 let request_initial_redraw =
5558 primary_launch_requires_initial_redraw(self.primary_visible(), self.settings.headless);
5559
5560 self.window = Some(window);
5561 self.surface = Some(surface);
5562 self.surface_config = Some(surface_config);
5563 self.surface_caps = Some(surface_caps);
5564 self.app = Some(app);
5565 self.accessibility = Some(accessibility);
5566 self.platform = Some(platform);
5567 cranpose_services::advance_lifecycle(cranpose_services::LifecycleState::Resumed);
5568 self.primary_presence = self
5569 .primary_window_id()
5570 .filter(|_| !self.settings.headless)
5571 .map(crate::desktop_lifecycle::WindowPresence::shown);
5572 self.gpu_context = Some(DesktopGpuContext {
5573 gpu,
5574 adapter_backend: adapter_info.backend,
5575 text_system,
5576 });
5577 self.primary_surface_dirty = request_initial_redraw;
5578 self.primary_initial_present_pending = request_initial_redraw;
5579 self.refresh_native_window_requests();
5580 self.sync_native_windows(event_loop);
5581 if request_initial_redraw && let Some(window) = self.window.clone() {
5582 request_redraw_once(&window, &mut self.primary_redraw_pending);
5583 }
5584 }
5585
5586 fn window_event(
5587 &mut self,
5588 event_loop: &dyn ActiveEventLoop,
5589 window_id: WinitWindowId,
5590 event: WindowEvent,
5591 ) {
5592 self.sync_frame_pacing();
5593 self.track_pointer_for_cursors(window_id, &event);
5594 self.observe_presence(window_id, &event);
5595 if self.primary_window_id() != Some(window_id) {
5596 self.dispatch_native_window_event(event_loop, window_id, event);
5597 return;
5598 }
5599 self.relay_primary_held_press(event_loop, &event);
5600 let primary_frames_enabled = self.primary_frames_enabled();
5601 let Some(window) = &self.window else {
5602 return;
5603 };
5604 if let Some(accessibility) = &mut self.accessibility {
5605 accessibility.process_event(window.as_ref(), &event);
5606 }
5607
5608 let frame_interval = self.frame_interval();
5609 let last_frame_start_time = self.last_frame_start_time;
5610 let frame_cap_deadline = last_frame_start_time
5611 .and_then(|started_at| frame_interval.map(|interval| started_at + interval));
5612
5613 let primary_viewport_override = headless_requested_viewport(&self.settings);
5614
5615 let registry = Rc::clone(&self.native_window_registry);
5616 let Some(app) = &mut self.app else { return };
5617 if let Some(accessibility) = &mut self.accessibility {
5618 for (node_id, canvas_key) in accessibility.drain_clicks() {
5619 app.accessibility_activate(node_id, canvas_key);
5620 }
5621 accessibility.run_custom_actions(app);
5622 accessibility.run_value_requests(app);
5623 accessibility.run_scroll_requests(app);
5624 accessibility.run_focus_requests(app);
5625 }
5626 let Some(platform) = &mut self.platform else {
5627 return;
5628 };
5629 let Some(surface) = &self.surface else { return };
5630 let Some(surface_config) = &mut self.surface_config else {
5631 return;
5632 };
5633
5634 let mut sync_native_windows_after_event = false;
5635 match event {
5636 WindowEvent::CloseRequested => {
5637 if let Some(recorder) = self.recorder.take()
5638 && let Err(e) = recorder.finish()
5639 {
5640 eprintln!("[Recorder] Error saving recording: {e}");
5641 }
5642 self.exiting = true;
5643 event_loop.exit();
5644 }
5645 ref drag @ (WindowEvent::DragEntered { .. }
5646 | WindowEvent::DragDropped { .. }
5647 | WindowEvent::DataTransferReceived { .. }) => {
5648 self.file_drops.handle(event_loop, drag);
5649 }
5650 WindowEvent::SurfaceResized(new_size) => {
5651 apply_primary_surface_resize(
5652 app,
5653 surface,
5654 surface_config,
5655 window,
5656 primary_viewport_override,
5657 new_size.width,
5658 new_size.height,
5659 &mut self.primary_surface_dirty,
5660 &mut self.primary_redraw_pending,
5661 );
5662 }
5663 WindowEvent::ScaleFactorChanged {
5664 scale_factor,
5665 mut surface_size_writer,
5666 } => {
5667 update_app_scale_factor(app, platform, scale_factor);
5668
5669 let new_size = window.surface_size();
5670 let _ = surface_size_writer.request_surface_size(new_size);
5671 apply_primary_surface_resize(
5672 app,
5673 surface,
5674 surface_config,
5675 window,
5676 primary_viewport_override,
5677 new_size.width,
5678 new_size.height,
5679 &mut self.primary_surface_dirty,
5680 &mut self.primary_redraw_pending,
5681 );
5682 }
5683 WindowEvent::Moved(_) => {
5684 self.vsync_interval = monitor_refresh_interval(window);
5685 app.set_screen_origin(native_window_surface_origin(
5686 &self.native_window_platform_probe,
5687 window,
5688 ));
5689 }
5690 WindowEvent::ThemeChanged(theme)
5691 if self
5692 .platform_env
5693 .set_system_theme(system_theme_from_winit(theme)) =>
5694 {
5695 app.request_root_render();
5696 self.primary_surface_dirty = true;
5697 request_redraw_once(window, &mut self.primary_redraw_pending);
5698 }
5699 WindowEvent::PointerMoved {
5700 position, source, ..
5701 } => {
5702 let logical = platform.pointer_position(position);
5703 self.last_cursor_position = Some((logical.x, logical.y));
5704 log::trace!(
5705 target: "cranpose::input",
5706 "desktop pointer move ({:.2},{:.2})",
5707 logical.x,
5708 logical.y
5709 );
5710 app.set_pointer_source(pointer_source_from_winit(&source));
5711 let event_time = app.realtime_pointer_event_time(None);
5712 if app.set_cursor_at_event_time(logical.x, logical.y, event_time) {
5713 request_redraw_once(window, &mut self.primary_redraw_pending);
5714 }
5715 let global_pointer =
5716 native_window_global_pointer_state(&self.native_window_platform_probe);
5717 let pointer_over_surface = global_pointer.is_some_and(|pointer| {
5718 native_window_local_pointer_physical(
5719 &self.native_window_platform_probe,
5720 window,
5721 pointer.position,
5722 )
5723 .is_some()
5724 });
5725 #[cfg(feature = "robot")]
5726 let synthetic_primary_down = self
5727 .robot_controller
5728 .as_ref()
5729 .is_some_and(RobotController::synthetic_primary_down);
5730 #[cfg(not(feature = "robot"))]
5731 let synthetic_primary_down = false;
5732 if primary_pointer_move_should_recover_press(
5733 app.has_active_pointer_gesture(),
5734 synthetic_primary_down,
5735 global_pointer,
5736 pointer_over_surface,
5737 ) {
5738 log::trace!(
5739 target: "cranpose::input",
5740 "desktop pointer move recovered primary press"
5741 );
5742 let request = pointer_button_frame_request(app.pointer_pressed());
5743 apply_primary_pointer_button_frame_request(
5744 window,
5745 &mut self.last_frame_start_time,
5746 &mut self.primary_redraw_pending,
5747 request,
5748 );
5749 }
5750 if let Some(recorder) = &mut self.recorder {
5751 recorder.record_mouse_move(logical.x, logical.y);
5752 }
5753 }
5754 WindowEvent::ModifiersChanged(modifiers) => {
5755 self.current_modifiers = modifiers.state();
5756 app.set_modifiers(app_modifiers(self.current_modifiers));
5757 }
5758 WindowEvent::MouseWheel { delta, .. } => {
5759 if self.last_cursor_position.is_none()
5760 && Self::refresh_primary_cursor_from_platform_pointer(
5761 &self.native_window_platform_probe,
5762 window,
5763 platform,
5764 app,
5765 &mut self.last_cursor_position,
5766 )
5767 {
5768 request_redraw_once(window, &mut self.primary_redraw_pending);
5769 }
5770 let request = scroll_frame_request(dispatch_mouse_wheel(
5771 &mut app.primary(),
5772 platform,
5773 self.current_modifiers,
5774 self.last_cursor_position,
5775 delta,
5776 ));
5777 apply_primary_pointer_button_frame_request(
5778 window,
5779 &mut self.last_frame_start_time,
5780 &mut self.primary_redraw_pending,
5781 request,
5782 );
5783 }
5784 WindowEvent::PointerButton {
5785 state,
5786 position,
5787 button,
5788 ..
5789 } if is_primary_pointer_button(&button) => {
5790 let source = pointer_source_from_button(&button);
5791 app.set_pointer_source(source);
5792 let logical = platform.pointer_position(position);
5793 self.last_cursor_position = Some((logical.x, logical.y));
5794 let event_time = app.realtime_pointer_event_time(None);
5795 log::trace!(
5796 target: "cranpose::input",
5797 "desktop pointer button {:?} at ({:.2},{:.2})",
5798 state,
5799 logical.x,
5800 logical.y
5801 );
5802 if app.set_cursor_at_event_time(logical.x, logical.y, event_time) {
5803 request_redraw_once(window, &mut self.primary_redraw_pending);
5804 }
5805 match state {
5806 ElementState::Pressed => {
5807 let request = pointer_button_frame_request(
5808 app.pointer_pressed_at_event_time(event_time),
5809 );
5810 apply_primary_pointer_button_frame_request(
5811 window,
5812 &mut self.last_frame_start_time,
5813 &mut self.primary_redraw_pending,
5814 request,
5815 );
5816 if let Some(recorder) = &mut self.recorder {
5817 recorder.record_mouse_down();
5818 }
5819 }
5820 ElementState::Released => {
5821 let released = if source.is_touch_like() {
5822 app.pointer_released_at_position_event_time(
5823 logical.x, logical.y, event_time,
5824 )
5825 } else {
5826 app.pointer_released_at_event_time(event_time)
5827 };
5828 let request = pointer_button_frame_request(released);
5829 app.sync_selection_to_primary();
5830 apply_primary_pointer_button_frame_request(
5831 window,
5832 &mut self.last_frame_start_time,
5833 &mut self.primary_redraw_pending,
5834 request,
5835 );
5836 if let Some(recorder) = &mut self.recorder {
5837 recorder.record_mouse_up();
5838 }
5839 }
5840 }
5841 }
5842 WindowEvent::PointerButton {
5843 state: ElementState::Pressed,
5844 button: ButtonSource::Mouse(MouseButton::Middle),
5845 ..
5846 } => {
5847 Self::refresh_primary_cursor_from_platform_pointer(
5848 &self.native_window_platform_probe,
5849 window,
5850 platform,
5851 app,
5852 &mut self.last_cursor_position,
5853 );
5854 dispatch_middle_click_paste(&mut app.primary(), self.last_cursor_position);
5855 }
5856 WindowEvent::KeyboardInput { event, .. } => {
5857 dispatch_keyboard_input(&mut app.primary(), self.current_modifiers, event);
5858 }
5859 WindowEvent::Focused(focused) => {
5860 primary_window_focus_changed(app, focused);
5861 }
5862 WindowEvent::Ime(ime_event) => {
5863 dispatch_ime_event(&mut app.primary(), ime_event);
5864 }
5865 WindowEvent::PointerLeft { .. } => {
5866 app.cancel_gesture_unless_pressed();
5867 }
5868 WindowEvent::RedrawRequested => {
5869 record_pacing_event(|diag| &mut diag.redraw_events);
5870 self.primary_redraw_pending = false;
5871 if defer_window_redraw(event_loop, primary_frames_enabled, frame_cap_deadline) {
5872 return;
5873 }
5874 log::trace!(target: "cranpose::input", "desktop redraw requested");
5875 let frame_started_at = Instant::now();
5876 #[cfg(feature = "robot")]
5877 let robot_surface_dirty_before_update = self.robot_visible_surface_dirty;
5878 #[cfg(not(feature = "robot"))]
5879 let robot_surface_dirty_before_update = false;
5880 let primary_surface_dirty_before_update = self.primary_surface_dirty;
5881 app.set_density(window.scale_factor() as f32);
5882 #[cfg_attr(not(feature = "robot"), expect(unused_variables))]
5883 let update_result = update_app_with_native_window_registry(app, ®istry);
5884 wrap_primary_window_for_frame(
5885 app,
5886 surface,
5887 surface_config,
5888 window,
5889 ®istry,
5890 primary_viewport_override,
5891 self.settings.primary_wraps_content,
5892 &mut self.primary_wrap_size,
5893 );
5894 if let Some(accessibility) = &mut self.accessibility {
5895 accessibility.sync(app);
5896 }
5897 #[cfg(feature = "robot")]
5898 if let Some(controller) = &mut self.robot_controller {
5899 controller.record_idle_update_result(update_result);
5900 }
5901 let after_update = Instant::now();
5902 sync_native_windows_after_event = true;
5903
5904 let frame_owed = app.take_frame_owed();
5905 let present_required = surface_present_required(
5906 primary_surface_dirty_before_update || robot_surface_dirty_before_update,
5907 frame_owed,
5908 app.needs_redraw(),
5909 );
5910 pace_after_empty_redraw(
5911 &mut self.last_frame_start_time,
5912 &mut self.last_redraw_empty,
5913 frame_started_at,
5914 present_required,
5915 );
5916 if present_required {
5917 self.primary_surface_dirty = true;
5918 let acquire_started_at = Instant::now();
5919 let output = match current_surface_texture(surface, "primary window") {
5920 SurfaceFrame::Ready(output) => output,
5921 SurfaceFrame::Reconfigure => {
5922 let size = window.surface_size();
5923 let viewport = viewport_for_surface_size(
5924 primary_viewport_override,
5925 size.width,
5926 size.height,
5927 window.scale_factor(),
5928 );
5929 configure_app_surface_size(
5930 app,
5931 surface,
5932 surface_config,
5933 size.width,
5934 size.height,
5935 viewport,
5936 );
5937 if surface_reconfigure_requires_redraw(size.width, size.height) {
5938 request_redraw_once(window, &mut self.primary_redraw_pending);
5939 }
5940 return;
5941 }
5942 SurfaceFrame::Skip => {
5943 pace_after_empty_redraw(
5948 &mut self.last_frame_start_time,
5949 &mut self.last_redraw_empty,
5950 frame_started_at,
5951 false,
5952 );
5953 #[cfg(feature = "robot")]
5954 {
5955 self.unpresentable_frames_since_present =
5956 self.unpresentable_frames_since_present.saturating_add(1);
5957 }
5958 return;
5959 }
5960 };
5961 let after_acquire = Instant::now();
5962
5963 let view = output.texture.create_view(&wgpu::TextureViewDescriptor {
5964 format: Some(crate::surface_format::display_surface_view_format(
5965 surface_config.format,
5966 )),
5967 ..Default::default()
5968 });
5969
5970 if let Err(err) = app.renderer().render_surface_texture(
5971 &output.texture,
5972 &view,
5973 surface_config.width,
5974 surface_config.height,
5975 ) {
5976 log::error!("render failed: {err:?}");
5977 return;
5978 }
5979 let after_render = Instant::now();
5980
5981 window.pre_present_notify();
5982 app.renderer().present(output);
5983 let after_present = Instant::now();
5984 record_pacing_event(|diag| &mut diag.presents);
5985 self.primary_surface_dirty = false;
5986 self.primary_initial_present_pending = false;
5987 app.record_presented_frame(frame_started_at, after_render);
5988 log_desktop_frame_telemetry(
5989 frame_started_at,
5990 after_update,
5991 after_acquire,
5992 after_render,
5993 after_present,
5994 "primary",
5995 );
5996 self.last_frame_start_time = Some(presented_frame_anchor(
5997 next_frame_anchor(
5998 self.last_frame_start_time,
5999 frame_started_at,
6000 frame_interval,
6001 ),
6002 frame_interval,
6003 after_acquire.duration_since(acquire_started_at),
6004 ));
6005 #[cfg(feature = "robot")]
6006 {
6007 self.presented_frame_generation =
6008 self.presented_frame_generation.saturating_add(1);
6009 self.unpresentable_frames_since_present = 0;
6010 self.robot_visible_surface_dirty = false;
6011 }
6012 #[cfg(feature = "robot")]
6013 let robot_driven = self.robot_controller.is_some();
6014 #[cfg(not(feature = "robot"))]
6015 let robot_driven = false;
6016 if !robot_driven
6017 && should_chain_no_vsync_redraw(
6018 frame_interval,
6019 app.frame_schedule().needs_frame,
6020 )
6021 {
6022 request_redraw_once(window, &mut self.primary_redraw_pending);
6023 }
6024 } else {
6025 record_pacing_event(|diag| &mut diag.skipped_no_present);
6026 #[cfg(feature = "robot")]
6027 {
6028 self.robot_visible_surface_dirty = app.needs_redraw();
6029 }
6030 }
6031 }
6032 event => {
6033 restore_pointer_icon_the_window_system_drew_over(self.app.as_ref(), &event);
6034 present_primary_frame_owed_while_hidden(
6035 &event,
6036 window,
6037 &mut self.primary_surface_dirty,
6038 &mut self.primary_redraw_pending,
6039 );
6040 }
6041 }
6042
6043 if sync_native_windows_after_event {
6044 self.sync_native_windows(event_loop);
6045 }
6046 }
6047
6048 fn about_to_wait(&mut self, event_loop: &dyn ActiveEventLoop) {
6049 #[cfg(feature = "webview")]
6050 self.webviews.dispatch(&self.platform_env.native_views);
6051 self.publish_settled_presence();
6052 if cranpose_services::take_exit_request() {
6053 self.exiting = true;
6054 event_loop.exit();
6055 return;
6056 }
6057 self.serve_app_requests();
6058 let now = Instant::now();
6059 if self.poll_native_window_global_primary_press() {
6060 self.refresh_native_window_requests();
6061 self.sync_native_windows(event_loop);
6062 }
6063 if self.poll_active_native_window_drags(now) {
6064 self.refresh_native_window_requests();
6065 self.sync_native_windows(event_loop);
6066 }
6067 let primary_pointer_polled = self.poll_primary_pointer_gesture();
6068 if self.poll_external_native_window_moves(now) {
6069 self.refresh_native_window_requests();
6070 self.sync_native_windows(event_loop);
6071 }
6072
6073 self.sync_frame_pacing();
6074 self.sync_pointer_icons(event_loop);
6075
6076 let last_frame_start_time = self.last_frame_start_time;
6077 #[cfg(feature = "robot")]
6078 let registry = Rc::clone(&self.native_window_registry);
6079 #[cfg(feature = "robot")]
6080 let primary_visible = self.primary_visible();
6081 let primary_frames_enabled = self.primary_frames_enabled();
6082 let frames_enabled = self.frames_enabled();
6083 let Some(app) = &mut self.app else { return };
6084 let Some(window) = self.window.clone() else {
6085 return;
6086 };
6087 if let Some(accessibility) = &mut self.accessibility
6088 && accessibility.serve(app, now)
6089 {
6090 request_redraw_once(&window, &mut self.primary_redraw_pending);
6091 }
6092
6093 if primary_frames_enabled
6094 && initial_present_redraw_needed(
6095 self.primary_initial_present_pending,
6096 self.primary_redraw_pending,
6097 )
6098 {
6099 request_redraw_once(&window, &mut self.primary_redraw_pending);
6100 }
6101
6102 #[cfg(feature = "robot")]
6103 if let Some(controller) = &mut self.robot_controller {
6104 let mut robot_visual_dirty = false;
6105 while let Some(cmd) = controller.next_command() {
6106 match cmd {
6107 RobotCommand::Click { x, y } => {
6108 app.set_pointer_source(PointerSource::Mouse);
6109 let event_time = app.realtime_pointer_event_time(None);
6110 let cursor_dirty = app.set_cursor_at_event_time(x, y, event_time);
6111 controller.begin_synthetic_primary_gesture();
6112 let press_dirty = app.pointer_pressed_at_event_time(event_time);
6113 let release_dirty = app.pointer_released_at_event_time(event_time);
6114 controller.end_synthetic_primary_gesture();
6115 robot_visual_dirty |= cursor_dirty || press_dirty || release_dirty;
6116 let _ = controller.tx.send(RobotResponse::Ok);
6117 }
6118 RobotCommand::MoveTo { x, y } => {
6119 let event_time = app.realtime_pointer_event_time(None);
6120 robot_visual_dirty |= app.set_cursor_at_event_time(x, y, event_time);
6121 if let Some(recorder) = &mut self.recorder {
6122 recorder.record_mouse_move(x, y);
6123 }
6124 let _ = controller.tx.send(RobotResponse::Ok);
6125 }
6126 RobotCommand::MouseDown => {
6127 let event_time = app.realtime_pointer_event_time(None);
6128 controller.begin_synthetic_primary_gesture();
6129 robot_visual_dirty |= app.pointer_pressed_at_event_time(event_time);
6130 if let Some(recorder) = &mut self.recorder {
6131 recorder.record_mouse_down();
6132 }
6133 let _ = controller.tx.send(RobotResponse::Ok);
6134 }
6135 RobotCommand::MouseUp => {
6136 let event_time = app.realtime_pointer_event_time(None);
6137 robot_visual_dirty |= app.pointer_released_at_event_time(event_time);
6138 controller.end_synthetic_primary_gesture();
6139 if let Some(recorder) = &mut self.recorder {
6140 recorder.record_mouse_up();
6141 }
6142 let _ = controller.tx.send(RobotResponse::Ok);
6143 }
6144 RobotCommand::MouseScroll { delta_x, delta_y } => {
6145 if app.pointer_scrolled(delta_x, delta_y) {
6146 robot_visual_dirty = true;
6147 }
6148 let _ = controller.tx.send(RobotResponse::Ok);
6149 }
6150 RobotCommand::MouseScrollAndWaitForFrame { delta_x, delta_y } => {
6151 let Some(target) = robot_scroll_present_target(
6152 app,
6153 delta_x,
6154 delta_y,
6155 primary_visible,
6156 self.settings.headless,
6157 self.presented_frame_generation,
6158 ) else {
6159 let _ = controller.tx.send(RobotResponse::Ok);
6160 continue;
6161 };
6162 robot_visual_dirty |= target.is_some();
6163 robot_present_or_update(
6164 controller,
6165 &window,
6166 app,
6167 ®istry,
6168 &mut self.last_frame_start_time,
6169 &mut self.primary_redraw_pending,
6170 &mut self.robot_visible_surface_dirty,
6171 target,
6172 );
6173 }
6174 RobotCommand::MouseScrollSequenceAndWaitForFrames {
6175 delta_x,
6176 delta_y,
6177 count,
6178 } => {
6179 if count == 0 {
6180 let _ = controller.tx.send(RobotResponse::Ok);
6181 continue;
6182 }
6183 let Some(target) = robot_scroll_present_target(
6184 app,
6185 delta_x,
6186 delta_y,
6187 primary_visible,
6188 self.settings.headless,
6189 self.presented_frame_generation,
6190 ) else {
6191 let _ = controller.tx.send(RobotResponse::Ok);
6192 continue;
6193 };
6194 if target.is_some() {
6195 controller.scroll_sequence = Some(RobotScrollSequence {
6196 delta_x,
6197 delta_y,
6198 remaining: count.saturating_sub(1),
6199 });
6200 robot_visual_dirty = true;
6201 }
6202 robot_present_or_update(
6203 controller,
6204 &window,
6205 app,
6206 ®istry,
6207 &mut self.last_frame_start_time,
6208 &mut self.primary_redraw_pending,
6209 &mut self.robot_visible_surface_dirty,
6210 target,
6211 );
6212 }
6213
6214 RobotCommand::TouchDown { x, y, source } => {
6215 app.set_pointer_source(source);
6216 let event_time = app.realtime_pointer_event_time(None);
6217 let cursor_dirty = app.set_cursor_at_event_time(x, y, event_time);
6218 controller.begin_synthetic_primary_gesture();
6219 let press_dirty = app.pointer_pressed_at_event_time(event_time);
6220 robot_visual_dirty |= cursor_dirty || press_dirty;
6221 let _ = controller.tx.send(RobotResponse::Ok);
6222 }
6223 RobotCommand::TouchMove { x, y, source } => {
6224 app.set_pointer_source(source);
6225 let event_time = app.realtime_pointer_event_time(None);
6226 robot_visual_dirty |= app.set_cursor_at_event_time(x, y, event_time);
6227 let _ = controller.tx.send(RobotResponse::Ok);
6228 }
6229 RobotCommand::TouchMoveAndWaitForFrame { x, y, source } => {
6230 app.set_pointer_source(source);
6231 let event_time = app.realtime_pointer_event_time(None);
6232 let visual_dirty = app.set_cursor_at_event_time(x, y, event_time);
6233 if visual_dirty {
6234 robot_visual_dirty = true;
6235 }
6236 let present_target = visual_dirty
6237 .then(|| {
6238 robot_visible_pump_present_target(
6239 primary_visible,
6240 self.settings.headless,
6241 1,
6242 self.presented_frame_generation,
6243 )
6244 })
6245 .flatten();
6246 robot_present_or_update(
6247 controller,
6248 &window,
6249 app,
6250 ®istry,
6251 &mut self.last_frame_start_time,
6252 &mut self.primary_redraw_pending,
6253 &mut self.robot_visible_surface_dirty,
6254 present_target,
6255 );
6256 }
6257 RobotCommand::TouchUp { x, y, source } => {
6258 app.set_pointer_source(source);
6259 let event_time = app.realtime_pointer_event_time(None);
6260 let cursor_dirty = app.set_cursor_at_event_time(x, y, event_time);
6261 let release_dirty = app.pointer_released_at_event_time(event_time);
6262 controller.end_synthetic_primary_gesture();
6263 robot_visual_dirty |= cursor_dirty || release_dirty;
6264 let _ = controller.tx.send(RobotResponse::Ok);
6265 }
6266 command @ (RobotCommand::GetSemantics
6267 | RobotCommand::GetSpokenTree
6268 | RobotCommand::GetInspectorState
6269 | RobotCommand::AuditAccessibility) => {
6270 let update_result = pump_robot_frame(app, ®istry);
6271 robot_visual_dirty |=
6272 robot_query_visual_dirty(update_result, app.needs_redraw());
6273 let _ = controller.tx.send(robot_tree_response(app, &command));
6274 }
6275 RobotCommand::FindText { text, match_kind } => {
6276 let update_result = pump_robot_frame(app, ®istry);
6277 robot_visual_dirty |=
6278 robot_query_visual_dirty(update_result, app.needs_redraw());
6279 let result = find_text_in_app(app, &text, match_kind);
6280 let _ = controller.tx.send(RobotResponse::SemanticQuery(result));
6281 }
6282 RobotCommand::FindButton { text, match_kind } => {
6283 let update_result = pump_robot_frame(app, ®istry);
6284 robot_visual_dirty |=
6285 robot_query_visual_dirty(update_result, app.needs_redraw());
6286 let result = find_button_in_app(app, &text, match_kind);
6287 let _ = controller.tx.send(RobotResponse::SemanticQuery(result));
6288 }
6289 RobotCommand::GetScreenshot => {
6290 let update_result = pump_robot_frame(app, ®istry);
6291 robot_visual_dirty |=
6292 robot_query_visual_dirty(update_result, app.needs_redraw());
6293 match capture_screenshot(app) {
6294 Ok(screenshot) => {
6295 let _ = controller.tx.send(RobotResponse::Screenshot(screenshot));
6296 }
6297 Err(err) => {
6298 let _ = controller.tx.send(RobotResponse::Error(err));
6299 }
6300 }
6301 }
6302 RobotCommand::GetScreenshotWithScale(scale) => {
6303 let update_result = pump_robot_frame(app, ®istry);
6304 robot_visual_dirty |=
6305 robot_query_visual_dirty(update_result, app.needs_redraw());
6306 match capture_screenshot_with_scale(app, scale) {
6307 Ok(screenshot) => {
6308 let _ = controller.tx.send(RobotResponse::Screenshot(screenshot));
6309 }
6310 Err(err) => {
6311 let _ = controller.tx.send(RobotResponse::Error(err));
6312 }
6313 }
6314 }
6315 RobotCommand::CaptureKeyframes { scale, steps } => {
6316 let mut shots = Vec::new();
6317 let mut capture_err = None;
6318 for (advance_ms, capture) in steps {
6319 native_window::with_native_window_registry(®istry, || {
6320 app.update_after_exact_interval(Duration::from_secs_f64(
6321 f64::from(advance_ms.max(0.0)) / 1000.0,
6322 ))
6323 });
6324 if capture {
6325 match capture_screenshot_with_scale(app, scale) {
6326 Ok(shot) => shots.push(shot),
6327 Err(err) => {
6328 capture_err = Some(err);
6329 break;
6330 }
6331 }
6332 }
6333 }
6334 let _ = controller.tx.send(match capture_err {
6335 Some(err) => RobotResponse::Error(err),
6336 None => RobotResponse::Screenshots(shots),
6337 });
6338 }
6339 RobotCommand::CaptureInteractionKeyframes { scale, steps } => {
6340 let mut shots = Vec::new();
6341 let mut capture_err = None;
6342 for step in steps {
6343 native_window::with_native_window_registry(®istry, || {
6344 app.update_after_exact_interval(Duration::from_secs_f64(
6345 f64::from(step.advance_ms.max(0.0)) / 1000.0,
6346 ))
6347 });
6348 for action in step.actions {
6349 let action_result = match action {
6350 RobotTimelineAction::MoveTo { x, y } => {
6351 let event_time = app.exact_pointer_event_time(None);
6352 app.set_cursor_at_event_time(x, y, event_time);
6353 Ok(())
6354 }
6355 RobotTimelineAction::MouseDown => {
6356 let event_time = app.exact_pointer_event_time(None);
6357 controller.begin_synthetic_primary_gesture();
6358 app.pointer_pressed_at_event_time(event_time);
6359 Ok(())
6360 }
6361 RobotTimelineAction::MouseUp => {
6362 let event_time = app.exact_pointer_event_time(None);
6363 app.pointer_released_at_event_time(event_time);
6364 controller.end_synthetic_primary_gesture();
6365 Ok(())
6366 }
6367 RobotTimelineAction::InvokeAppHook { name, argument } => {
6368 self.robot_app_hook.as_mut().map_or_else(
6369 || Err("robot app hook not configured".to_string()),
6370 |hook| {
6371 app.debug_enter_app_context(|| hook(name, argument))
6372 .map(|_| ())
6373 },
6374 )
6375 }
6376 };
6377 if let Err(err) = action_result {
6378 capture_err = Some(err);
6379 break;
6380 }
6381 }
6382 if capture_err.is_some() {
6383 break;
6384 }
6385 native_window::with_native_window_registry(®istry, || {
6386 app.update_after_exact_interval(Duration::ZERO)
6387 });
6388 if step.capture {
6389 match capture_screenshot_with_scale(app, scale) {
6390 Ok(shot) => shots.push(shot),
6391 Err(err) => {
6392 capture_err = Some(err);
6393 break;
6394 }
6395 }
6396 }
6397 }
6398 robot_visual_dirty = true;
6399 let _ = controller.tx.send(match capture_err {
6400 Some(err) => RobotResponse::Error(err),
6401 None => RobotResponse::Screenshots(shots),
6402 });
6403 }
6404 RobotCommand::GetRenderStats
6405 | RobotCommand::GetFpsStats
6406 | RobotCommand::GetPresentationInfo => {
6407 let response = robot_frame_diagnostics(
6408 &cmd,
6409 app,
6410 self.surface_config.as_ref(),
6411 self.surface_caps.as_ref(),
6412 self.vsync_interval,
6413 );
6414 let _ = controller.tx.send(response);
6415 }
6416 RobotCommand::GetPacingControlCenter(mode) => {
6417 let center = app.dev_overlay_control_center(mode);
6418 let _ = controller
6419 .tx
6420 .send(RobotResponse::PacingControlCenter(center));
6421 }
6422 RobotCommand::ResetFpsStats => {
6423 app.reset_fps_stats();
6424 let _ = controller.tx.send(RobotResponse::Ok);
6425 }
6426 RobotCommand::GetLastFlingVelocity => {
6427 let velocity =
6428 app.debug_enter_app_context(cranpose_ui::debug_last_fling_velocity);
6429 let _ = controller.tx.send(RobotResponse::F32(velocity));
6430 }
6431 RobotCommand::ResetLastFlingVelocity => {
6432 app.debug_enter_app_context(cranpose_ui::debug_reset_last_fling_velocity);
6433 let _ = controller.tx.send(RobotResponse::Ok);
6434 }
6435 RobotCommand::GetRenderCpuAllocationStats => {
6436 let _ =
6437 controller
6438 .tx
6439 .send(RobotResponse::RenderCpuAllocationStats(Box::new(
6440 app.renderer().debug_cpu_allocation_stats(),
6441 )));
6442 }
6443 RobotCommand::GetRuntimeLeakDebugStats => {
6444 let _ = controller
6445 .tx
6446 .send(RobotResponse::RuntimeLeakDebugStats(Box::new(
6447 app.debug_runtime_leak_stats(),
6448 )));
6449 }
6450 RobotCommand::GetLiveUiTaskLabels => {
6451 let _ = controller.tx.send(RobotResponse::LiveUiTaskLabels(
6452 app.runtime_handle().live_ui_task_labels(),
6453 ));
6454 }
6455 RobotCommand::MeasureText { text, style } => {
6456 let metrics = app.debug_enter_app_context(|| {
6457 let text = cranpose_ui::text::AnnotatedString::from(text.as_str());
6458 cranpose_ui::measure_text(&text, &style)
6459 });
6460 let _ = controller.tx.send(RobotResponse::TextMetrics(metrics));
6461 }
6462 RobotCommand::HasFocusedTextField => {
6463 let focused = app.debug_enter_app_context(|| {
6464 cranpose_ui::text_field_focus::has_focused_field()
6465 });
6466 let _ = controller.tx.send(RobotResponse::Bool(focused));
6467 }
6468 RobotCommand::SetSemanticsEnabled(enabled) => {
6469 app.set_semantics_enabled(enabled);
6470 let _ = controller.tx.send(RobotResponse::Ok);
6471 }
6472 RobotCommand::InvokeAppHook { name, argument } => {
6473 let response = match self.robot_app_hook.as_mut() {
6474 Some(hook) => app
6475 .debug_enter_app_context(|| hook(name, argument))
6476 .map(RobotResponse::AppHookResult),
6477 None => Err("robot app hook not configured".to_string()),
6478 };
6479 match response {
6480 Ok(response) => {
6481 let _ = controller.tx.send(response);
6482 }
6483 Err(err) => {
6484 let _ = controller.tx.send(RobotResponse::Error(err));
6485 }
6486 }
6487 robot_visual_dirty = true;
6488 }
6489 RobotCommand::DriverPanicked(message) => {
6490 self.abort_launch(event_loop, LaunchError::TestDriverPanic(message));
6491 return;
6492 }
6493 RobotCommand::TypeText(text) => {
6494 use cranpose_app_shell::{KeyEvent, KeyEventType, Modifiers};
6495
6496 for ch in text.chars() {
6497 let key_code = char_to_key_code(ch);
6498 let key_event = KeyEvent::new(
6499 key_code,
6500 ch.to_string(),
6501 Modifiers::NONE,
6502 KeyEventType::KeyDown,
6503 );
6504 app.on_key_event(&key_event);
6505 }
6506 let update_result = update_app_with_native_window_registry(app, ®istry);
6507 robot_visual_dirty |= update_result.visual_changed || app.needs_redraw();
6508 let _ = controller.tx.send(RobotResponse::Ok);
6509 }
6510 RobotCommand::SendKey(key) => {
6511 use cranpose_app_shell::{KeyEvent, KeyEventType, Modifiers};
6512
6513 let (key_code, text) = robot_key_code_and_text(&key);
6514 let key_event =
6515 KeyEvent::new(key_code, text, Modifiers::NONE, KeyEventType::KeyDown);
6516 app.on_key_event(&key_event);
6517 let update_result = update_app_with_native_window_registry(app, ®istry);
6518 robot_visual_dirty |= update_result.visual_changed || app.needs_redraw();
6519 let _ = controller.tx.send(RobotResponse::Ok);
6520 }
6521 RobotCommand::SendKeyWithModifiers {
6522 key,
6523 shift,
6524 ctrl,
6525 alt,
6526 meta,
6527 } => {
6528 use cranpose_app_shell::{KeyEvent, KeyEventType, Modifiers};
6529
6530 let (key_code, text) = robot_key_code_and_text(&key);
6531 let modifiers = Modifiers {
6532 shift,
6533 ctrl,
6534 alt,
6535 meta,
6536 };
6537 let key_event =
6538 KeyEvent::new(key_code, text, modifiers, KeyEventType::KeyDown);
6539 app.on_key_event(&key_event);
6540 let update_result = update_app_with_native_window_registry(app, ®istry);
6541 robot_visual_dirty |= update_result.visual_changed || app.needs_redraw();
6542 let _ = controller.tx.send(RobotResponse::Ok);
6543 }
6544 RobotCommand::WaitForIdle => {
6545 controller.start_idle_wait();
6546 let visual_frame_pending = self.robot_visible_surface_dirty
6547 || robot_visual_dirty
6548 || self.primary_redraw_pending;
6549 controller.waiting_for_present_generation = robot_visible_present_target(
6550 primary_visible,
6551 self.settings.headless,
6552 visual_frame_pending,
6553 self.presented_frame_generation,
6554 );
6555 if controller.waiting_for_present_generation.is_some() {
6556 request_redraw_once(&window, &mut self.primary_redraw_pending);
6557 }
6558 }
6559 RobotCommand::PumpFrames { count } => {
6560 let update_result =
6561 native_window::with_native_window_registry(®istry, || {
6562 let mut result = FrameUpdateResult::default();
6563 for _ in 0..count {
6564 let frame_result =
6565 app.update_after_frame_interval(ROBOT_PUMP_FRAME_INTERVAL);
6566 result.visual_changed |= frame_result.visual_changed;
6567 result.structure_changed |= frame_result.structure_changed;
6568 }
6569 result
6570 });
6571 robot_visual_dirty |= update_result.visual_changed || app.needs_redraw();
6572 let present_target = robot_visual_dirty
6573 .then(|| {
6574 robot_visible_pump_present_target(
6575 primary_visible,
6576 self.settings.headless,
6577 count,
6578 self.presented_frame_generation,
6579 )
6580 })
6581 .flatten();
6582 robot_present_or_ack(
6583 controller,
6584 &window,
6585 &mut self.last_frame_start_time,
6586 &mut self.primary_redraw_pending,
6587 present_target,
6588 );
6589 }
6590 RobotCommand::WaitForPresentFrame => {
6591 let mut visual_frame_pending =
6592 self.robot_visible_surface_dirty || app.needs_redraw();
6593 if !visual_frame_pending {
6594 let update_result =
6595 update_app_with_native_window_registry(app, ®istry);
6596 visual_frame_pending =
6597 app.needs_redraw() || update_result.visual_changed;
6598 self.robot_visible_surface_dirty = visual_frame_pending;
6599 }
6600 let present_target = visual_frame_pending
6601 .then(|| {
6602 robot_visible_pump_present_target(
6603 primary_visible,
6604 self.settings.headless,
6605 1,
6606 self.presented_frame_generation,
6607 )
6608 })
6609 .flatten();
6610 robot_present_or_ack(
6611 controller,
6612 &window,
6613 &mut self.last_frame_start_time,
6614 &mut self.primary_redraw_pending,
6615 present_target,
6616 );
6617 }
6618 RobotCommand::Exit => {
6619 let _ = controller.tx.send(RobotResponse::Ok);
6620 self.exiting = true;
6621 event_loop.exit();
6622 return;
6623 }
6624 }
6625 }
6626 if robot_visual_dirty {
6627 self.robot_visible_surface_dirty = true;
6628 if primary_surface_redraw_drives_app(primary_visible, self.settings.headless) {
6629 request_redraw_once(&window, &mut self.primary_redraw_pending);
6630 }
6631 }
6632
6633 if let Some(target_generation) = controller.waiting_for_pump_present_generation {
6634 if self.presented_frame_generation >= target_generation {
6635 if let Some(mut sequence) = controller.scroll_sequence.take() {
6636 if sequence.remaining == 0 {
6637 controller.finish_pump_present_wait();
6638 self.robot_visible_surface_dirty = app.needs_redraw();
6639 let _ = controller.tx.send(RobotResponse::Ok);
6640 } else if app.pointer_scrolled(sequence.delta_x, sequence.delta_y) {
6641 sequence.remaining = sequence.remaining.saturating_sub(1);
6642 controller.scroll_sequence = Some(sequence);
6643 controller.begin_pump_present_wait(
6644 self.presented_frame_generation.saturating_add(1),
6645 );
6646 self.robot_visible_surface_dirty = true;
6647 self.last_frame_start_time = None;
6648 request_redraw_once(&window, &mut self.primary_redraw_pending);
6649 } else {
6650 controller.finish_pump_present_wait();
6651 self.robot_visible_surface_dirty = app.needs_redraw();
6652 let _ = controller.tx.send(RobotResponse::Ok);
6653 }
6654 } else {
6655 controller.finish_pump_present_wait();
6656 self.robot_visible_surface_dirty = app.needs_redraw();
6657 let _ = controller.tx.send(RobotResponse::Ok);
6658 }
6659 } else if controller.pump_present_wait_timed_out(now) {
6660 controller.finish_pump_present_wait();
6661 controller.scroll_sequence = None;
6662 self.robot_visible_surface_dirty = app.needs_redraw();
6663 let _ = controller.tx.send(RobotResponse::Error(format!(
6664 "present wait: the window surface refused {} consecutive frames over {:?} \
6665 and never reached generation {target_generation} (currently {}); the \
6666 window is occluded, off-screen, or on a display that is not compositing",
6667 self.unpresentable_frames_since_present,
6668 ROBOT_PRESENT_WAIT_TIMEOUT,
6669 self.presented_frame_generation,
6670 )));
6671 } else {
6672 request_redraw_once(&window, &mut self.primary_redraw_pending);
6673 }
6674 }
6675
6676 if controller.waiting_for_idle {
6677 let frame_schedule = app.frame_schedule();
6678 let needs_update = frame_schedule.needs_update;
6679 let needs_frame = frame_schedule.needs_frame;
6680 let has_active_animations = app.has_active_animations();
6681 let has_transient_frame_callbacks = app.has_transient_frame_callbacks();
6682 let visible_redraw_pending = self.robot_visible_surface_dirty || app.needs_redraw();
6683 if controller.waiting_for_present_generation.is_none()
6684 && self.primary_redraw_pending
6685 && visible_redraw_pending
6686 {
6687 controller.waiting_for_present_generation = robot_visible_present_target(
6688 primary_visible,
6689 self.settings.headless,
6690 true,
6691 self.presented_frame_generation,
6692 );
6693 }
6694 let present_still_required = visible_redraw_pending;
6695 let waiting_for_present = present_still_required
6696 && controller
6697 .waiting_for_present_generation
6698 .is_some_and(|target| self.presented_frame_generation < target);
6699 let pending = RobotIdlePending {
6700 needs_frame,
6701 needs_update,
6702 transient_frame_callbacks: has_transient_frame_callbacks,
6703 waiting_for_present,
6704 awaits_pipelines: app.renderer().awaits_pipelines(),
6705 };
6706 let animation_loop_only = robot_wait_for_idle_animation_loop_only(
6707 has_active_animations,
6708 has_transient_frame_callbacks,
6709 waiting_for_present,
6710 controller.idle_iterations,
6711 controller.idle_structure_clean_frames,
6712 );
6713 if pending.frame_only() || animation_loop_only {
6714 controller.finish_idle_wait();
6715 self.robot_visible_surface_dirty = false;
6716 let _ = controller.tx.send(RobotResponse::Ok);
6717 } else if pending.quiet() {
6718 let mut finish_idle = true;
6719 if needs_update {
6720 let update_result = update_app_with_native_window_registry(app, ®istry);
6721 controller.record_idle_update_result(update_result);
6722 self.robot_visible_surface_dirty =
6723 app.needs_redraw() || update_result.visual_changed;
6724 let follow_up_schedule = app.frame_schedule();
6725 if follow_up_schedule.needs_frame || update_result.structure_changed {
6726 finish_idle = false;
6727 if primary_surface_redraw_drives_app(
6728 primary_visible,
6729 self.settings.headless,
6730 ) {
6731 request_redraw_once(&window, &mut self.primary_redraw_pending);
6732 }
6733 controller.idle_iterations += 1;
6734 }
6735 }
6736 if finish_idle {
6737 controller.finish_idle_wait();
6738 self.robot_visible_surface_dirty = false;
6739 let _ = controller.tx.send(RobotResponse::Ok);
6740 }
6741 } else {
6742 if needs_frame
6743 && primary_surface_redraw_drives_app(
6744 primary_visible,
6745 self.settings.headless,
6746 )
6747 {
6748 request_redraw_once(&window, &mut self.primary_redraw_pending);
6749 } else {
6750 let update_result = update_app_with_native_window_registry(app, ®istry);
6751 controller.record_idle_update_result(update_result);
6752 self.robot_visible_surface_dirty =
6753 app.needs_redraw() || update_result.visual_changed;
6754 }
6755 controller.idle_iterations += 1;
6756
6757 if controller.idle_iterations % 50 == 0 {
6758 log::debug!(
6759 "wait_for_idle iteration {}: needs_update={}, needs_redraw={}, has_animations={}, waiting_for_present={}, presented_frame_generation={}, target_present_generation={:?}",
6760 controller.idle_iterations,
6761 needs_update,
6762 app.needs_redraw(),
6763 has_active_animations,
6764 waiting_for_present,
6765 self.presented_frame_generation,
6766 controller.waiting_for_present_generation
6767 );
6768 }
6769
6770 let present_is_sole_blocker = waiting_for_present
6771 && !needs_update
6772 && !has_active_animations
6773 && !has_transient_frame_callbacks;
6774 let timeout_check_at = Instant::now();
6775 controller
6776 .observe_idle_present_block(present_is_sole_blocker, timeout_check_at);
6777 if let Some(timeout) = controller.idle_wait_timeout(timeout_check_at) {
6778 let iterations = controller.idle_iterations;
6779 let target_generation = controller.waiting_for_present_generation;
6780 controller.finish_idle_wait();
6781 let message = match timeout {
6782 IdleWaitTimeout::AppNotConverging => format!(
6783 "wait_for_idle: timed out after {ROBOT_IDLE_TIMEOUT:?} ({iterations} iterations); needs_update={needs_update}, needs_redraw={}, has_animations={has_active_animations}, waiting_for_present={waiting_for_present}",
6784 app.needs_redraw(),
6785 ),
6786 IdleWaitTimeout::SurfaceNotPresenting => format!(
6787 "wait_for_idle: the window surface refused {} consecutive frames over {:?} and never reached generation {target_generation:?} (currently {}); composition had already settled, so this is the compositor and not the application -- the window is occluded, off-screen, or on a display that is not compositing; needs_update={needs_update}, needs_redraw={}, has_animations={has_active_animations}, waiting_for_present={waiting_for_present}",
6788 self.unpresentable_frames_since_present,
6789 ROBOT_IDLE_PRESENT_TIMEOUT,
6790 self.presented_frame_generation,
6791 app.needs_redraw(),
6792 ),
6793 IdleWaitTimeout::HostStarved => format!(
6794 "wait_for_idle: host starvation -- only {iterations} iterations ran in {:?} (need {ROBOT_IDLE_MIN_ITERATIONS} to trust the elapsed budget); the process was not scheduled enough to tell whether the application is converging, not an application defect; needs_update={needs_update}, needs_redraw={}, has_animations={has_active_animations}, waiting_for_present={waiting_for_present}",
6795 ROBOT_IDLE_STARVATION_CEILING,
6796 app.needs_redraw(),
6797 ),
6798 };
6799 let _ = controller.tx.send(RobotResponse::Error(message));
6800 }
6801 }
6802 }
6803 }
6804
6805 let frame_interval = frame_cap_interval(
6806 app.frame_pacing_mode(),
6807 self.vsync_interval,
6808 self.last_redraw_empty,
6809 );
6810 let frame_schedule = app.frame_schedule();
6811 let has_active_animations = frames_enabled && app.has_active_animations();
6812 let needs_update = frame_schedule.needs_update;
6813 let needs_redraw =
6814 primary_frames_enabled && (frame_schedule.needs_frame || app.frame_owed());
6815 if needs_redraw {
6816 log::trace!(
6817 target: "cranpose::input",
6818 "about_to_wait needs_redraw={needs_redraw}"
6819 );
6820 }
6821 let next_frame_time = last_frame_start_time
6822 .and_then(|started_at| frame_interval.map(|interval| started_at + interval));
6823 let waiting_for_frame_cap =
6824 needs_redraw && next_frame_time.is_some_and(|deadline| deadline > now);
6825 let primary_next_event_time = primary_frames_enabled
6826 .then_some(frame_schedule.next_deadline)
6827 .flatten();
6828 let sync_native_windows = self.update_primary_before_wait(
6829 primary_frames_enabled,
6830 needs_update,
6831 needs_redraw,
6832 waiting_for_frame_cap,
6833 frame_interval,
6834 );
6835 self.sync_primary_size();
6836 self.sync_primary_visibility();
6837 if sync_native_windows {
6838 self.sync_native_windows(event_loop);
6839 }
6840
6841 #[cfg(feature = "webview")]
6842 crate::wry_webview::sync(
6843 &mut self.webviews,
6844 &self.platform_env.native_views,
6845 self.app.as_mut(),
6846 self.window.as_ref(),
6847 &self.webview_wake,
6848 );
6849
6850 #[cfg(all(feature = "webview", target_os = "linux"))]
6851 let webview_deadline = crate::wry_webview::pump(&self.webviews);
6852 #[cfg(not(all(feature = "webview", target_os = "linux")))]
6853 let webview_deadline = None;
6854
6855 let Some(app) = self.app.as_mut() else { return };
6856 let mut native_drag_deadline: Option<Instant> = None;
6857 let native_position_poll_deadline = self
6858 .native_windows
6859 .values()
6860 .any(|native| {
6861 native_window_position_poll_needed(
6862 native.options.visible,
6863 native.active_drag.is_some(),
6864 native.pending_outer_positions.has_pending(),
6865 )
6866 })
6867 .then_some(self.next_native_window_position_poll_at);
6868 let mut native_frame_cap_deadline: Option<Instant> = None;
6869 let mut native_next_event_time: Option<Instant> = None;
6870 let pacing_mode = app.frame_pacing_mode();
6871 for native in self.native_windows.values_mut() {
6872 if !native.presentable() {
6873 continue;
6874 }
6875 let Some(surface) = native_surface(app, native) else {
6876 continue;
6877 };
6878
6879 let frame_schedule = surface.frame_schedule();
6880 let needs_redraw = native_surface_needs_frame(
6881 frame_schedule.needs_frame,
6882 surface.frame_owed(),
6883 surface.needs_redraw(),
6884 native.surface_dirty,
6885 );
6886 let next_frame_time = native.last_frame_start_time.and_then(|started_at| {
6887 native
6888 .frame_interval(pacing_mode)
6889 .map(|interval| started_at + interval)
6890 });
6891 let waiting_for_frame_cap =
6892 needs_redraw && next_frame_time.is_some_and(|deadline| deadline > now);
6893
6894 if needs_redraw && !waiting_for_frame_cap {
6895 native.window.request_redraw();
6896 }
6897 if let Some(next_poll_at) = native.active_drag.and_then(|active_drag| {
6898 native_window_drag_poll_deadline(
6899 active_drag.next_poll_at,
6900 NATIVE_WINDOW_GLOBAL_POINTER_POLLED,
6901 )
6902 }) {
6903 native_drag_deadline = Some(
6904 native_drag_deadline.map_or(next_poll_at, |current| current.min(next_poll_at)),
6905 );
6906 }
6907 if waiting_for_frame_cap && let Some(deadline) = next_frame_time {
6908 native_frame_cap_deadline = Some(
6909 native_frame_cap_deadline.map_or(deadline, |current| current.min(deadline)),
6910 );
6911 }
6912 if let Some(next_time) = frame_schedule.next_deadline {
6913 native_next_event_time = Some(
6914 native_next_event_time.map_or(next_time, |current| current.min(next_time)),
6915 );
6916 }
6917 }
6918
6919 #[cfg(feature = "robot")]
6920 let robot_needs_poll = self
6921 .robot_controller
6922 .as_mut()
6923 .is_some_and(RobotController::awaiting_progress);
6924
6925 #[cfg(not(feature = "robot"))]
6926 let robot_needs_poll = false;
6927
6928 let control_flow = event_loop_control_flow(
6929 now,
6930 LoopControlInputs {
6931 robot_needs_poll,
6932 free_running: primary_frames_enabled
6933 && free_running_frame(
6934 frame_interval,
6935 needs_redraw,
6936 self.primary_redraw_pending,
6937 ),
6938 primary_pointer_polled,
6939 drag_poll_deadline: native_drag_deadline,
6940 position_poll_deadline: native_position_poll_deadline,
6941 frame_cap_deadline: [
6942 next_frame_time.filter(|_| waiting_for_frame_cap),
6943 native_frame_cap_deadline,
6944 ]
6945 .into_iter()
6946 .flatten()
6947 .min(),
6948 has_active_animations,
6949 next_event_time: [
6950 webview_deadline,
6951 primary_next_event_time,
6952 native_next_event_time,
6953 self.accessibility.as_ref().and_then(
6954 crate::desktop_accessibility::DesktopAccessibilityBridge::wake_deadline,
6955 ),
6956 ]
6957 .into_iter()
6958 .flatten()
6959 .min(),
6960 },
6961 );
6962
6963 #[cfg(feature = "robot")]
6964 let control_flow = bound_park_for_robot(control_flow, self.robot_controller.is_some(), now);
6965
6966 record_pacing_event(match control_flow {
6967 ControlFlow::Poll => |diag: &mut PacingDiag| &mut diag.control_flow_poll,
6968 ControlFlow::WaitUntil(_) => |diag: &mut PacingDiag| &mut diag.control_flow_wait_until,
6969 ControlFlow::Wait => |diag: &mut PacingDiag| &mut diag.control_flow_wait,
6970 });
6971 event_loop.set_control_flow(control_flow);
6972 }
6973}
6974
6975#[cfg(feature = "robot")]
6976const ROBOT_PARKED_COMMAND_POLL_INTERVAL: Duration = Duration::from_millis(8);
6977
6978#[cfg(feature = "robot")]
6979fn bound_park_for_robot(
6980 control_flow: ControlFlow,
6981 robot_attached: bool,
6982 now: Instant,
6983) -> ControlFlow {
6984 if !robot_attached {
6985 return control_flow;
6986 }
6987 let bound = now + ROBOT_PARKED_COMMAND_POLL_INTERVAL;
6988 match control_flow {
6989 ControlFlow::Poll => ControlFlow::Poll,
6990 ControlFlow::Wait => ControlFlow::WaitUntil(bound),
6991 ControlFlow::WaitUntil(deadline) => ControlFlow::WaitUntil(deadline.min(bound)),
6992 }
6993}
6994
6995fn build_event_loop() -> Result<EventLoop, winit::error::EventLoopError> {
6996 #[cfg(all(target_os = "linux", feature = "desktop-x11"))]
6997 if std::env::var_os("DISPLAY").is_some_and(|display| !display.is_empty()) {
6998 use winit::platform::x11::EventLoopBuilderExtX11;
6999
7000 match EventLoop::builder().with_x11().build() {
7001 Ok(event_loop) => return Ok(event_loop),
7002 Err(error) => log::warn!("cranpose: the X display is unavailable: {error}"),
7003 }
7004 }
7005 EventLoop::builder().build()
7006}
7007
7008#[allow(unused_mut)]
7020pub fn try_run(
7021 mut settings: AppSettings,
7022 content: impl FnMut() + 'static,
7023) -> Result<(), LaunchError> {
7024 crate::application_id::register(settings.application_id.as_deref());
7025
7026 let event_loop = build_event_loop().map_err(LaunchError::EventLoopCreate)?;
7027 let event_proxy = event_loop.create_proxy();
7028 let launch_error = Rc::new(RefCell::new(None));
7029
7030 #[cfg(feature = "robot")]
7031 let robot_controller = if let Some(driver) = settings.test_driver.take() {
7032 let wake_proxy = event_proxy.clone();
7033 let (controller, robot) = RobotController::new(move || wake_proxy.wake_up());
7034 let panic_tx = robot.command_sender();
7035 std::thread::spawn(move || {
7036 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
7037 driver(robot);
7038 }));
7039 if let Err(payload) = result {
7040 let _ = panic_tx.send(RobotCommand::DriverPanicked(panic_payload_message(payload)));
7041 }
7042 });
7043 Some(controller)
7044 } else {
7045 None
7046 };
7047
7048 let surface_wake = event_proxy.clone();
7049 crate::desktop_host_surface::install(move || surface_wake.wake_up());
7050
7051 #[cfg(feature = "media-backend")]
7052 cranpose_media::install();
7053
7054 #[cfg(all(feature = "camera-desktop", target_os = "macos"))]
7055 crate::apple_camera::register();
7056
7057 crate::desktop_power::register();
7058
7059 crate::desktop_bundled_assets::register();
7060
7061 cranpose_services::set_platform_app_updater(Arc::new(
7062 cranpose_services::GitHubAppUpdater::new(),
7063 ));
7064
7065 crate::desktop_incoming::publish_launch_documents();
7066
7067 #[cfg(unix)]
7068 crate::process_info::install();
7069
7070 settings.start_process();
7071 let mut app = App::new(settings, content, Rc::clone(&launch_error), event_proxy);
7072
7073 #[cfg(feature = "robot")]
7074 if let Some(controller) = robot_controller {
7075 app.set_robot_controller(controller);
7076 }
7077
7078 let run_result = event_loop.run_app(app);
7079 if let Some(error) = launch_error.borrow_mut().take() {
7080 return Err(error);
7081 }
7082
7083 run_result.map_err(LaunchError::EventLoopRun)
7084}
7085
7086pub fn run(settings: AppSettings, content: impl FnMut() + 'static) -> ! {
7090 try_run(settings, content).unwrap_or_else(|error| {
7091 crate::app_launcher::exit_after_launch_error("desktop launch failed", error)
7092 });
7093 std::process::exit(0)
7094}
7095
7096#[cfg(feature = "robot")]
7098const ROBOT_PIPELINE_WAIT: Duration = Duration::from_secs(30);
7099
7100#[cfg(feature = "robot")]
7104fn capture_final_frame(
7105 app: &mut AppShell<WgpuRenderer>,
7106 width: u32,
7107 height: u32,
7108 scale: f32,
7109) -> Result<cranpose_render_wgpu::CapturedFrame, String> {
7110 let deadline = Instant::now() + ROBOT_PIPELINE_WAIT;
7111 loop {
7112 let captured = app
7113 .renderer()
7114 .capture_frame_with_scale(width, height, scale)
7115 .map_err(|err| format!("Failed to capture GPU screenshot: {err:?}"))?;
7116 let drew_placeholders = app
7117 .renderer()
7118 .last_frame_stats()
7119 .is_some_and(|stats| stats.placeholder_draws > 0);
7120 if !drew_placeholders || Instant::now() >= deadline {
7121 return Ok(captured);
7122 }
7123 while app.renderer().awaits_pipelines() && Instant::now() < deadline {
7124 std::thread::sleep(Duration::from_millis(2));
7125 }
7126 }
7127}
7128
7129#[cfg(feature = "robot")]
7130fn capture_screenshot(app: &mut AppShell<WgpuRenderer>) -> Result<RobotScreenshot, String> {
7131 let logical_size = app.viewport_size();
7132 let (width, height, capture_scale) =
7133 resolve_robot_screenshot_params(app.buffer_size(), Some(logical_size));
7134
7135 let captured = capture_final_frame(app, width, height, capture_scale)?;
7136
7137 let (logical_width, logical_height) = logical_size;
7138
7139 Ok(RobotScreenshot {
7140 width: captured.width,
7141 height: captured.height,
7142 logical_width,
7143 logical_height,
7144 pixels: captured.pixels,
7145 })
7146}
7147
7148#[cfg(feature = "robot")]
7149fn capture_screenshot_with_scale(
7150 app: &mut AppShell<WgpuRenderer>,
7151 scale: f32,
7152) -> Result<RobotScreenshot, String> {
7153 let (logical_width, logical_height) = app.viewport_size();
7154 let width = (logical_width * scale).ceil().max(1.0) as u32;
7155 let height = (logical_height * scale).ceil().max(1.0) as u32;
7156
7157 let captured = capture_final_frame(app, width, height, scale)?;
7158
7159 Ok(RobotScreenshot {
7160 width: captured.width,
7161 height: captured.height,
7162 logical_width,
7163 logical_height,
7164 pixels: captured.pixels,
7165 })
7166}
7167
7168#[cfg(feature = "robot")]
7169fn resolve_robot_screenshot_params(
7170 buffer_size: (u32, u32),
7171 fallback_logical_size: Option<(f32, f32)>,
7172) -> (u32, u32, f32) {
7173 resolve_robot_screenshot_params_with_scale(
7174 buffer_size,
7175 fallback_logical_size,
7176 robot_capture_scale_from_env(),
7177 )
7178}
7179
7180#[cfg(feature = "robot")]
7181fn robot_capture_scale_from_env() -> f32 {
7182 parse_robot_capture_scale(
7183 std::env::var("CRANPOSE_ROBOT_CAPTURE_SCALE")
7184 .ok()
7185 .as_deref(),
7186 )
7187}
7188
7189#[cfg(feature = "robot")]
7190fn parse_robot_capture_scale(value: Option<&str>) -> f32 {
7191 value
7192 .and_then(|value| value.parse::<f32>().ok())
7193 .filter(|scale| scale.is_finite() && *scale >= 0.5 && *scale <= 4.0)
7194 .unwrap_or(1.0)
7195}
7196
7197#[cfg(feature = "robot")]
7198fn resolve_robot_screenshot_params_with_scale(
7199 buffer_size: (u32, u32),
7200 fallback_logical_size: Option<(f32, f32)>,
7201 capture_scale: f32,
7202) -> (u32, u32, f32) {
7203 if let Some((logical_width, logical_height)) = fallback_logical_size {
7204 let width = (logical_width * capture_scale).ceil().max(1.0) as u32;
7205 let height = (logical_height * capture_scale).ceil().max(1.0) as u32;
7206 return (width, height, capture_scale);
7207 }
7208
7209 let (buffer_width, buffer_height) = buffer_size;
7210 (buffer_width.max(1), buffer_height.max(1), 1.0)
7211}
7212
7213#[cfg(test)]
7214#[path = "tests/desktop_tests.rs"]
7215mod tests;