Skip to main content

cranpose/
desktop.rs

1//! Desktop runtime for Compose applications.
2//!
3//! This module provides the desktop event loop implementation using winit.
4
5use 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, &registry);
847            } else {
848                native_window::with_native_window_registry(&registry, || {
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                    &registry,
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    /// Tells the cursors where the pointer went: a window it left gives back
1035    /// a cursor it was holding, and one it moved over may need it shown again.
1036    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    /// Hands a cursor the window was holding back to the window, now that the
1047    /// pointer is somewhere else.
1048    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    /// Serves what the app asked of the host since the loop last looked: the
1447    /// window size it requested, and the tasks woken since they last ran,
1448    /// which run now rather than at the next frame.
1449    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(&registry, || 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, &registry, "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
3521/// How long the next frame waits after the last one started: the pacing
3522/// mode's frame interval, and when unpaced, a vsync interval after a redraw
3523/// that presented nothing. Frame callbacks that change nothing on screen then
3524/// tick at the display's rate instead of the event loop's; time-based
3525/// animations finish in the same real time either way.
3526fn 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/// Whose the press is, as the application itself says.
4245#[derive(Clone, Copy, Debug, Eq, PartialEq)]
4246enum PressBelongsHere {
4247    /// The node that took the press now draws in this window, so the press
4248    /// came here with it and the window takes it wherever it is on screen.
4249    ItsNodeMovedHere,
4250    /// No node of this window took the press. It may still belong here — a
4251    /// gesture can be carried by a node that stays where it is, as a tab's
4252    /// drag-and-drop source stays in the strip the tab was pulled out of —
4253    /// so the pointer and the window's rectangle are all there is to go on.
4254    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    // A new display density changes the size a custom cursor is handed over at.
4805    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        // The first composition reads the window's theme. Without it the
5469        // theme is detected by running a system command (`defaults` on
5470        // macOS), and the theme set after it recomposes what read it.
5471        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(&registry, || {
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, &registry);
5884                wrap_primary_window_for_frame(
5885                    app,
5886                    surface,
5887                    surface_config,
5888                    window,
5889                    &registry,
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                            // No texture: the window is minimized, occluded or
5944                            // timed out. The next attempt waits a frame
5945                            // interval, as after an empty redraw, or the loop
5946                            // retries at once and spins a core.
5947                            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                            &registry,
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                            &registry,
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                            &registry,
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, &registry);
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, &registry);
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, &registry);
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, &registry);
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, &registry);
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(&registry, || {
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(&registry, || {
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(&registry, || {
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, &registry);
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, &registry);
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, &registry);
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(&registry, || {
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, &registry);
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, &registry);
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, &registry);
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/// Runs a desktop Compose application with wgpu rendering.
7009///
7010/// Called by `AppLauncher::run_desktop()`. This is the framework-level
7011/// entrypoint that manages the desktop event loop and rendering.
7012///
7013/// On Linux a build with the X11 backend runs on the X display named by
7014/// `DISPLAY` whenever there is one, which in a Wayland session is XWayland:
7015/// native windows need the global positions X11 reports and Wayland does not.
7016/// With no reachable X display the application runs on Wayland.
7017///
7018/// **Note:** Applications should use `AppLauncher` instead of calling this directly.
7019#[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
7086/// Runs a desktop application and exits the process on success.
7087///
7088/// Use [`try_run`] when the caller needs to handle launch failures explicitly.
7089pub 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/// How long a robot screenshot waits for the pipelines its effects need.
7097#[cfg(feature = "robot")]
7098const ROBOT_PIPELINE_WAIT: Duration = Duration::from_secs(30);
7099
7100/// Captures the scene at `scale`, again until the capture draws no
7101/// placeholder: a robot screenshot is the final picture, drawn once the
7102/// pipelines its effects wait for have landed.
7103#[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;