Skip to main content

tessera_ui/
renderer.rs

1//! The core rendering system for the Tessera UI framework. This module provides
2//! the main [`Renderer`] struct that manages the application lifecycle, event
3//! handling, and rendering pipeline for cross-platform UI applications.
4
5pub mod compute;
6pub mod core;
7pub mod drawer;
8
9#[cfg(target_os = "macos")]
10mod macos_titlebar;
11
12use std::{
13    cell::Cell,
14    sync::{
15        Arc,
16        atomic::{AtomicBool, Ordering},
17    },
18};
19
20pub use core::{RenderCore, RenderResources};
21
22use accesskit::{self, TreeUpdate};
23use accesskit_winit::{Adapter as AccessKitAdapter, Event as AccessKitEvent};
24use parking_lot::RwLock;
25use tracing::{debug, error, instrument, warn};
26use winit::{
27    application::ApplicationHandler,
28    error::EventLoopError,
29    event::WindowEvent,
30    event_loop::{ActiveEventLoop, ControlFlow, EventLoop},
31    window::{ResizeDirection, Window, WindowId},
32};
33
34use crate::{
35    ImeRequest, ImeState, PxPosition, ScrollEventContent,
36    build_tree::build_component_tree,
37    component_tree::{
38        LayoutFrameDiagnostics, WindowDragRequest, WindowRequests, clear_layout_snapshots,
39    },
40    context::reset_context_read_dependencies,
41    cursor::{
42        CursorEventContent, CursorState, GestureState, MOUSE_POINTER_ID, PointerChange,
43        PressKeyEventType,
44    },
45    dp::set_scale_factor,
46    focus::{FocusDirection, flush_pending_focus_callbacks},
47    keyboard_state::KeyboardState,
48    pipeline_context::PipelineContext,
49    plugin::{DesktopPlatformContext, DesktopWindowAction, PluginContext, PluginHost},
50    px::PxSize,
51    render_graph::RenderGraph,
52    render_module::RenderModule,
53    runtime::{
54        TesseraRuntime, begin_frame_build_invalidations, begin_frame_clock,
55        clear_persistent_focus_handles, clear_redraw_waker, has_pending_build_invalidations,
56        has_pending_frame_nanos_receivers, has_pending_layout_invalidations,
57        has_pending_persistent_focus_prune, has_ready_build_invalidations, install_redraw_waker,
58        prune_persistent_focus_handles, reset_build_invalidations, reset_component_replay_tracking,
59        reset_focus_read_dependencies, reset_frame_clock, reset_layout_dirty_tracking,
60        reset_render_slot_read_dependencies, reset_state_read_dependencies,
61        take_layout_dirty_nodes, tick_frame_nanos_receivers,
62    },
63    thread_utils,
64    time::Instant,
65};
66
67pub use crate::render_scene::{Command, DrawRegion, PaddingRect, SampleRegion};
68
69use self::core::RenderTimingBreakdown;
70
71pub use compute::{
72    ComputablePipeline, ComputeBatchItem, ComputePipelineRegistry, ErasedComputeBatchItem,
73};
74pub use drawer::{DrawCommand, DrawablePipeline, PipelineRegistry};
75
76#[cfg(feature = "debug-dirty-overlay")]
77use crate::PxRect;
78#[cfg(feature = "debug-dirty-overlay")]
79use crate::build_tree::{BuildTreeMode, BuildTreeResult};
80
81#[cfg(feature = "profiling")]
82use crate::profiler::{
83    FrameMeta, Phase as ProfilerPhase, RedrawReason, RuntimeEventKind, RuntimeMeta,
84    ScopeGuard as ProfilerScopeGuard, WakeMeta, WakeSource, begin_frame as profiler_begin_frame,
85    end_frame as profiler_end_frame, submit_frame_meta, submit_runtime_meta, submit_wake_meta,
86};
87#[cfg(feature = "profiling")]
88use crate::runtime::frame_delta;
89#[cfg(feature = "profiling")]
90use std::collections::BTreeSet;
91#[cfg(feature = "profiling")]
92use std::path::PathBuf;
93
94#[cfg(target_family = "wasm")]
95use std::{cell::RefCell, rc::Rc};
96#[cfg(target_family = "wasm")]
97use wasm_bindgen_futures::spawn_local;
98#[cfg(target_family = "wasm")]
99use web_sys::HtmlCanvasElement;
100#[cfg(target_family = "wasm")]
101use web_sys::wasm_bindgen::JsCast;
102#[cfg(target_family = "wasm")]
103use winit::platform::web::EventLoopExtWebSys;
104
105#[cfg(target_os = "android")]
106use winit::platform::android::{
107    ActiveEventLoopExtAndroid, EventLoopBuilderExtAndroid, activity::AndroidApp,
108};
109
110#[derive(Debug)]
111enum RendererUserEvent {
112    AccessKit(AccessKitEvent),
113    RuntimeRedrawWake,
114    #[cfg(target_family = "wasm")]
115    WebInitReady(u64),
116}
117
118impl From<AccessKitEvent> for RendererUserEvent {
119    fn from(event: AccessKitEvent) -> Self {
120        Self::AccessKit(event)
121    }
122}
123
124type RenderComputationOutput = (
125    RenderGraph,
126    WindowRequests,
127    std::time::Duration,
128    LayoutFrameDiagnostics,
129    std::time::Duration,
130    Option<FocusDirection>,
131    bool,
132    bool,
133);
134
135#[derive(Clone, Copy, Debug, Default)]
136struct RuntimePendingWork {
137    invalidation_pending: bool,
138    layout_pending: bool,
139    frame_receiver_pending: bool,
140}
141
142pub(crate) const MAX_FRAME_SETTLING_RETRIES: usize = 8;
143
144pub(crate) fn frame_settling_can_progress(pending_keyboard_retry: bool) -> bool {
145    pending_keyboard_retry || has_ready_build_invalidations() || has_pending_layout_invalidations()
146}
147
148impl RuntimePendingWork {
149    fn capture(pending_keyboard_retry: bool) -> Self {
150        Self {
151            invalidation_pending: has_pending_build_invalidations()
152                || crate::dispatcher::has_pending()
153                || crate::clipboard::has_pending_requests()
154                || pending_keyboard_retry
155                || has_pending_persistent_focus_prune()
156                || TesseraRuntime::with(|runtime| {
157                    runtime.component_tree.has_pending_focus_reveal()
158                        || runtime
159                            .component_tree
160                            .focus_owner()
161                            .has_pending_navigation()
162                }),
163            layout_pending: has_pending_layout_invalidations(),
164            frame_receiver_pending: has_pending_frame_nanos_receivers(),
165        }
166    }
167
168    fn requires_redraw(self) -> bool {
169        self.invalidation_pending || self.layout_pending || self.frame_receiver_pending
170    }
171
172    #[cfg(feature = "profiling")]
173    fn redraw_reasons(self) -> Vec<RedrawReason> {
174        let mut reasons = Vec::new();
175        if self.invalidation_pending || self.layout_pending {
176            reasons.push(RedrawReason::RuntimeInvalidation);
177        }
178        if self.frame_receiver_pending {
179            reasons.push(RedrawReason::RuntimeFrameAwaiter);
180        }
181        reasons
182    }
183}
184
185#[cfg(feature = "profiling")]
186pub(crate) fn resolve_profiler_output_path(config: &TesseraConfig) -> PathBuf {
187    if let Ok(path) = std::env::var("TESSERA_PROFILING_OUTPUT")
188        && !path.trim().is_empty()
189    {
190        return PathBuf::from(path);
191    }
192    if let Some(path) = option_env!("TESSERA_PROFILING_OUTPUT")
193        && !path.trim().is_empty()
194    {
195        return PathBuf::from(path);
196    }
197    config.profiler_output_path.clone()
198}
199
200/// Window creation options for desktop platforms.
201#[derive(Debug, Clone)]
202pub struct WindowConfig {
203    /// Application identifier used to associate desktop windows with their launcher.
204    pub application_id: Option<String>,
205    /// Icon displayed by desktop window managers.
206    pub icon: Option<winit::window::Icon>,
207    /// Whether to show the system window decorations (title bar and borders).
208    pub decorations: bool,
209    /// Integrates the title bar with app content. Keep decorations enabled for
210    /// native macOS controls and disable them to draw controls in the app bar on
211    /// other desktops. Use [`Self::with_app_titlebar`] to select these settings.
212    pub integrated_titlebar: bool,
213    /// Whether the window supports transparency.
214    pub transparent: bool,
215    /// Whether the window is resizable.
216    pub resizable: bool,
217}
218
219impl Default for WindowConfig {
220    fn default() -> Self {
221        Self {
222            application_id: None,
223            icon: None,
224            decorations: true,
225            integrated_titlebar: false,
226            transparent: true,
227            resizable: true,
228        }
229    }
230}
231
232impl WindowConfig {
233    /// Integrates the app bar with the desktop window. macOS keeps its native
234    /// controls. Other desktops can draw window controls in the app bar.
235    pub fn with_app_titlebar(mut self) -> Self {
236        if desktop_window_controls_available() {
237            self.decorations = cfg!(target_os = "macos");
238            self.integrated_titlebar = true;
239        }
240        self
241    }
242
243    fn app_titlebar(&self) -> AppTitleBar {
244        if !self.integrated_titlebar || !desktop_window_controls_available() {
245            AppTitleBar::None
246        } else if cfg!(target_os = "macos") && self.decorations {
247            AppTitleBar::Native
248        } else if !self.decorations {
249            AppTitleBar::Custom
250        } else {
251            AppTitleBar::None
252        }
253    }
254}
255
256/// The title bar integration available to the current app's content.
257#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
258pub enum AppTitleBar {
259    /// The app bar has no window controls or drag region.
260    #[default]
261    None,
262    /// The app bar can drag the window while the OS draws the controls.
263    Native,
264    /// The app bar supplies both the drag region and the controls.
265    Custom,
266}
267
268impl AppTitleBar {
269    /// Reads the current window's title bar integration.
270    pub fn current() -> Self {
271        crate::local_runtime_resource::<Cell<Self>>().get()
272    }
273}
274
275const fn desktop_window_controls_available() -> bool {
276    !cfg!(any(
277        target_family = "wasm",
278        target_os = "android",
279        target_os = "ios"
280    ))
281}
282
283/// Web host configuration for browser platforms.
284#[derive(Debug, Clone, Default)]
285pub struct WebConfig {
286    /// An optional canvas element id to mount into before falling back to the
287    /// default body append behavior.
288    pub canvas_id: Option<String>,
289}
290
291impl WebConfig {
292    /// Returns a config that looks up the canvas with the provided id.
293    pub fn with_canvas_id(mut self, canvas_id: impl Into<String>) -> Self {
294        self.canvas_id = Some(canvas_id.into());
295        self
296    }
297}
298
299/// Configuration for the UI runtime, window, and renderer.
300#[derive(Debug, Clone)]
301pub struct TesseraConfig {
302    /// MSAA sample count; defaults to `1` (disabled). The GPU must support the chosen count.
303    pub sample_count: u32,
304    /// Positive D3D12 non-sampler binding capacity; defaults to 65,536. Larger budgets use more memory.
305    pub max_non_sampler_bindings: u32,
306    /// The title of the application window.
307    /// Defaults to "Tessera" if not specified.
308    pub window_title: String,
309    /// Window configuration (desktop only).
310    pub window: WindowConfig,
311    /// Web host configuration for browser platforms.
312    pub web: WebConfig,
313    /// Path to write profiler output when `profiling` is enabled.
314    #[cfg(feature = "profiling")]
315    pub profiler_output_path: PathBuf,
316}
317
318impl Default for TesseraConfig {
319    fn default() -> Self {
320        Self {
321            sample_count: 1,
322            max_non_sampler_bindings: DEFAULT_MAX_NON_SAMPLER_BINDINGS,
323            window_title: "Tessera".to_string(),
324            window: WindowConfig::default(),
325            web: WebConfig::default(),
326            #[cfg(feature = "profiling")]
327            profiler_output_path: PathBuf::from("tessera-profiler.jsonl"),
328        }
329    }
330}
331
332pub(crate) const DEFAULT_MAX_NON_SAMPLER_BINDINGS: u32 = 64 * 1024;
333
334/// Owns a UI window, rendering resources, and event processing.
335///
336/// Use [`crate::EntryPoint`] for shared desktop, Android, and web startup.
337/// The renderer and its component builds run on the UI thread.
338pub struct Renderer<F> {
339    session: crate::RuntimeSession,
340    app: Option<RenderCore>,
341    entry_point: F,
342    cursor_state: CursorState,
343    keyboard_state: KeyboardState,
344    ime_state: ImeState,
345    ime_bridge_state: RendererImeBridgeState,
346    modules: Vec<Box<dyn RenderModule>>,
347    plugins: PluginHost,
348    config: TesseraConfig,
349    accessibility_adapter: Option<AccessKitAdapter>,
350    accessibility_active: bool,
351    event_loop_proxy: Option<winit::event_loop::EventLoopProxy<RendererUserEvent>>,
352    #[cfg(target_os = "android")]
353    android_back: Option<crate::android::system_back::BackBridge>,
354    pending_desktop_window_action: Arc<RwLock<Option<DesktopWindowAction>>>,
355    #[cfg(target_os = "macos")]
356    macos_titlebar: macos_titlebar::TitleBarInput,
357    frame_index: u64,
358    redraw_request_pending: Arc<AtomicBool>,
359    pending_close_requested: bool,
360    resize_in_progress: bool,
361    pending_window_size: Option<winit::dpi::PhysicalSize<u32>>,
362    #[cfg(target_family = "wasm")]
363    pending_web_inits: Rc<RefCell<Vec<(u64, RenderCore)>>>,
364    #[cfg(target_family = "wasm")]
365    web_init_epoch: u64,
366    #[cfg(target_family = "wasm")]
367    web_init_in_progress: bool,
368    #[cfg(feature = "profiling")]
369    pending_redraw_reasons: BTreeSet<RedrawReason>,
370}
371
372impl<F, Output> Renderer<F>
373where
374    F: Fn() -> Output,
375{
376    #[cfg(target_family = "wasm")]
377    fn resolve_web_canvas(config: &TesseraConfig) -> Option<HtmlCanvasElement> {
378        let canvas_id = config.web.canvas_id.as_deref()?;
379        let window = web_sys::window()?;
380        let document = window.document()?;
381        let element = document.get_element_by_id(canvas_id)?;
382        element.dyn_into::<HtmlCanvasElement>().ok()
383    }
384
385    /// Runs the desktop application with default configuration.
386    ///
387    /// Returns an error if the event loop cannot start or fails.
388    #[cfg(all(not(target_os = "android"), not(target_family = "wasm")))]
389    #[tracing::instrument(level = "info", skip(entry_point, modules))]
390    pub fn run(entry_point: F, modules: Vec<Box<dyn RenderModule>>) -> Result<(), EventLoopError> {
391        Self::run_with_config(entry_point, modules, Default::default())
392    }
393
394    /// Runs the desktop application with the supplied configuration.
395    ///
396    /// Returns an error if the event loop cannot start or fails.
397    #[tracing::instrument(level = "info", skip(entry_point, modules))]
398    #[cfg(all(not(target_os = "android"), not(target_family = "wasm")))]
399    pub fn run_with_config(
400        entry_point: F,
401        modules: Vec<Box<dyn RenderModule>>,
402        config: TesseraConfig,
403    ) -> Result<(), EventLoopError> {
404        let session = crate::RuntimeSession::for_root();
405        session.activate();
406        let event_loop = EventLoop::<RendererUserEvent>::with_user_event().build()?;
407        let event_loop_proxy = event_loop.create_proxy();
408        let app = None;
409        let cursor_state = CursorState::default();
410        let keyboard_state = KeyboardState::default();
411        let ime_state = ImeState::default();
412        let ime_bridge_state = RendererImeBridgeState::default();
413        #[cfg(feature = "profiling")]
414        crate::profiler::set_output_path(resolve_profiler_output_path(&config));
415        let mut renderer = Self {
416            session,
417            app,
418            entry_point,
419            cursor_state,
420            keyboard_state,
421            modules,
422            plugins: PluginHost::new(),
423            ime_state,
424            ime_bridge_state,
425            config,
426            accessibility_adapter: None,
427            accessibility_active: false,
428            event_loop_proxy: Some(event_loop_proxy),
429            pending_desktop_window_action: Arc::new(RwLock::new(None)),
430            #[cfg(target_os = "macos")]
431            macos_titlebar: macos_titlebar::TitleBarInput::default(),
432            frame_index: 0,
433            redraw_request_pending: Arc::new(AtomicBool::new(false)),
434            pending_close_requested: false,
435            resize_in_progress: false,
436            pending_window_size: None,
437            #[cfg(target_family = "wasm")]
438            pending_web_inits: Rc::new(RefCell::new(Vec::new())),
439            #[cfg(target_family = "wasm")]
440            web_init_epoch: 0,
441            #[cfg(target_family = "wasm")]
442            web_init_in_progress: false,
443            #[cfg(feature = "profiling")]
444            pending_redraw_reasons: BTreeSet::new(),
445        };
446        thread_utils::set_thread_name("TesseraMain");
447        event_loop.run_app(&mut renderer)
448    }
449
450    /// Runs the Tessera application with default configuration on web
451    /// platforms.
452    #[cfg(target_family = "wasm")]
453    #[tracing::instrument(level = "info", skip(entry_point, modules))]
454    pub fn run(entry_point: F, modules: Vec<Box<dyn RenderModule>>) -> Result<(), EventLoopError>
455    where
456        F: 'static,
457    {
458        Self::run_web_with_config(entry_point, modules, Default::default())
459    }
460
461    /// Runs the Tessera application with custom configuration on web
462    /// platforms.
463    #[cfg(target_family = "wasm")]
464    #[tracing::instrument(level = "info", skip(entry_point, modules))]
465    pub fn run_web_with_config(
466        entry_point: F,
467        modules: Vec<Box<dyn RenderModule>>,
468        config: TesseraConfig,
469    ) -> Result<(), EventLoopError>
470    where
471        F: 'static,
472    {
473        let session = crate::RuntimeSession::for_root();
474        session.activate();
475        let event_loop = EventLoop::<RendererUserEvent>::with_user_event().build()?;
476        let event_loop_proxy = event_loop.create_proxy();
477        let app = None;
478        let cursor_state = CursorState::default();
479        let keyboard_state = KeyboardState::default();
480        let ime_state = ImeState::default();
481        let ime_bridge_state = RendererImeBridgeState::default();
482        #[cfg(feature = "profiling")]
483        crate::profiler::set_output_path(resolve_profiler_output_path(&config));
484        let renderer = Self {
485            session,
486            app,
487            entry_point,
488            cursor_state,
489            keyboard_state,
490            modules,
491            plugins: PluginHost::new(),
492            ime_state,
493            ime_bridge_state,
494            config,
495            accessibility_adapter: None,
496            accessibility_active: false,
497            event_loop_proxy: Some(event_loop_proxy),
498            pending_desktop_window_action: Arc::new(RwLock::new(None)),
499            frame_index: 0,
500            redraw_request_pending: Arc::new(AtomicBool::new(false)),
501            pending_close_requested: false,
502            resize_in_progress: false,
503            pending_window_size: None,
504            pending_web_inits: Rc::new(RefCell::new(Vec::new())),
505            web_init_epoch: 0,
506            web_init_in_progress: false,
507            #[cfg(feature = "profiling")]
508            pending_redraw_reasons: BTreeSet::new(),
509        };
510        thread_utils::set_thread_name("TesseraMain");
511        event_loop.spawn_app(renderer);
512        Ok(())
513    }
514
515    /// Runs the Android application with default configuration and its activity.
516    ///
517    /// Returns an error if the event loop cannot start or fails.
518    #[cfg(target_os = "android")]
519    #[tracing::instrument(level = "info", skip(entry_point, modules, android_app))]
520    pub fn run(
521        entry_point: F,
522        modules: Vec<Box<dyn RenderModule>>,
523        android_app: AndroidApp,
524    ) -> Result<(), EventLoopError> {
525        Self::run_with_config(entry_point, modules, android_app, Default::default())
526    }
527
528    /// Runs the Android application with its activity and supplied configuration.
529    ///
530    /// Returns an error if the event loop cannot start or fails.
531    #[cfg(target_os = "android")]
532    #[tracing::instrument(level = "info", skip(entry_point, modules, android_app))]
533    pub fn run_with_config(
534        entry_point: F,
535        modules: Vec<Box<dyn RenderModule>>,
536        android_app: AndroidApp,
537        config: TesseraConfig,
538    ) -> Result<(), EventLoopError> {
539        let session = crate::RuntimeSession::for_root();
540        session.activate();
541        let event_loop = EventLoop::<RendererUserEvent>::with_user_event()
542            .with_android_app(android_app.clone())
543            .build()
544            .unwrap();
545        let event_loop_proxy = event_loop.create_proxy();
546        let android_back =
547            crate::android::system_back::BackBridge::new(&android_app, session.dispatcher());
548        let app = None;
549        let cursor_state = CursorState::default();
550        let keyboard_state = KeyboardState::default();
551        let ime_state = ImeState::default();
552        let ime_bridge_state = RendererImeBridgeState::default();
553        #[cfg(feature = "profiling")]
554        crate::profiler::set_output_path(resolve_profiler_output_path(&config));
555        let mut renderer = Self {
556            session,
557            app,
558            entry_point,
559            cursor_state,
560            keyboard_state,
561            modules,
562            plugins: PluginHost::new(),
563            ime_state,
564            ime_bridge_state,
565            config,
566            accessibility_adapter: None,
567            accessibility_active: false,
568            event_loop_proxy: Some(event_loop_proxy),
569            android_back,
570            pending_desktop_window_action: Arc::new(RwLock::new(None)),
571            frame_index: 0,
572            redraw_request_pending: Arc::new(AtomicBool::new(false)),
573            pending_close_requested: false,
574            resize_in_progress: false,
575            pending_window_size: None,
576            #[cfg(feature = "profiling")]
577            pending_redraw_reasons: BTreeSet::new(),
578        };
579        thread_utils::set_thread_name("TesseraMain");
580        event_loop.run_app(&mut renderer)
581    }
582}
583
584// Helper struct to group render-frame arguments and reduce parameter count.
585// Kept private to this module.
586struct RenderFrameArgs<'a> {
587    pub cursor_state: &'a mut CursorState,
588    pub keyboard_state: &'a mut KeyboardState,
589    pub ime_state: &'a mut ImeState,
590    pub ime_bridge_state: &'a mut RendererImeBridgeState,
591    pub app: &'a mut RenderCore,
592}
593
594struct RenderFrameContext<'a, F> {
595    entry_point: &'a F,
596    args: &'a mut RenderFrameArgs<'a>,
597    accessibility_enabled: bool,
598    decorations: bool,
599    window_label: &'a str,
600    frame_idx: u64,
601    #[cfg(feature = "profiling")]
602    redraw_reasons: Vec<RedrawReason>,
603}
604
605struct RenderFrameOutcome {
606    accessibility_update: Option<TreeUpdate>,
607    request_window_drag: Option<WindowDragRequest>,
608    runtime_pending_work: RuntimePendingWork,
609    #[cfg(feature = "profiling")]
610    frame_meta: FrameMeta,
611    #[cfg(feature = "debug-dirty-overlay")]
612    overlay_clear_pending: bool,
613}
614
615#[derive(Clone, Debug, Default, PartialEq)]
616struct RendererImeBridgeState {
617    current_request: Option<ImeRequest>,
618}
619
620#[derive(Clone, Debug, Default, PartialEq)]
621struct RendererImeBridgeUpdate {
622    allowed: Option<bool>,
623    cursor_area: Option<(PxPosition, PxSize)>,
624    text_state: Option<winit::event::ImeTextState>,
625    options: Option<winit::window::ImeOptions>,
626    warn_missing_position: bool,
627    show_keyboard: bool,
628    change_cause: winit::event::ImeChangeCause,
629}
630
631impl RendererImeBridgeState {
632    fn sync_window(&mut self, window: &Window, request: Option<crate::ImeRequest>) {
633        let ime_was_allowed = self.current_request.is_some();
634        let ime_bridge_update = self.update_request(request);
635        if ime_was_allowed && ime_bridge_update.allowed == Some(true) {
636            window.set_ime_allowed(false);
637        }
638        if let Some(options) = ime_bridge_update.options {
639            window.set_ime_options(options);
640        }
641        if let Some(state) = ime_bridge_update.text_state {
642            let _ = window.set_ime_text_state(state, ime_bridge_update.change_cause);
643        }
644        if let Some(allowed) = ime_bridge_update.allowed {
645            window.set_ime_allowed(allowed);
646        }
647        if ime_bridge_update.show_keyboard {
648            window.set_ime_allowed(true);
649        }
650        if let Some((position, size)) = ime_bridge_update.cursor_area {
651            window.set_ime_cursor_area::<PxPosition, PxSize>(position, size);
652        } else if ime_bridge_update.warn_missing_position {
653            warn!("IME request missing position; skipping IME cursor area update");
654        }
655    }
656
657    #[cfg(test)]
658    fn request(&self) -> Option<&ImeRequest> {
659        self.current_request.as_ref()
660    }
661
662    fn reset(&mut self) {
663        *self = Self::default();
664    }
665
666    fn receive_event(&mut self, event: &winit::event::Ime) -> Option<crate::focus::FocusHandleId> {
667        let request = self.current_request.as_mut()?;
668        let client = request.client_id?;
669        if let winit::event::Ime::TextState(state) = event {
670            // Echoing native edits can restart the input connection and end composition.
671            request.text_state.clone_from(state);
672        }
673        Some(client)
674    }
675
676    fn update_request(&mut self, mut request: Option<ImeRequest>) -> RendererImeBridgeUpdate {
677        let change_cause = request
678            .as_mut()
679            .map_or(winit::event::ImeChangeCause::Other, |request| {
680                std::mem::take(&mut request.change_cause)
681            });
682
683        let show_keyboard = request
684            .as_mut()
685            .is_some_and(|request| std::mem::take(&mut request.show_keyboard));
686        let previous = self.current_request.as_ref();
687        let context_changed = previous.map(|request| (request.client_id, request.options))
688            != request
689                .as_ref()
690                .map(|request| (request.client_id, request.options));
691        let allowed = context_changed.then_some(request.is_some());
692        let cursor_area = request.as_ref().and_then(|next| {
693            let old_area =
694                previous.and_then(|old| old.position.map(|position| (position, old.size)));
695            let area = next.position.map(|position| (position, next.size));
696            (context_changed || old_area != area)
697                .then_some(area)
698                .flatten()
699        });
700        let text_state = request
701            .as_ref()
702            .filter(|next| {
703                context_changed || previous.is_none_or(|old| old.text_state != next.text_state)
704            })
705            .map(|next| next.text_state.clone());
706        let options = request
707            .as_ref()
708            .filter(|_| context_changed)
709            .map(|next| next.options);
710        let warn_missing_position = self.current_request != request
711            && request
712                .as_ref()
713                .is_some_and(|request| request.position.is_none());
714        self.current_request = request;
715        RendererImeBridgeUpdate {
716            change_cause,
717            allowed,
718            cursor_area,
719            text_state,
720            options,
721            warn_missing_position,
722            show_keyboard: show_keyboard && allowed != Some(true),
723        }
724    }
725}
726
727impl<F, Output> Renderer<F>
728where
729    F: Fn() -> Output,
730{
731    const RESIZE_EDGE_THRESHOLD: f64 = 8.0;
732
733    #[cfg(target_os = "windows")]
734    fn update_native_window_shape(&self, window: &Window) {
735        let _session = self.session.enter();
736        use winit::platform::windows::{CornerPreference, WindowExtWindows};
737
738        if self.config.window.decorations {
739            window.set_corner_preference(CornerPreference::Default);
740            return;
741        }
742
743        let preference = if window.is_maximized() || window.fullscreen().is_some() {
744            CornerPreference::DoNotRound
745        } else {
746            CornerPreference::Round
747        };
748        window.set_corner_preference(preference);
749    }
750
751    #[cfg(target_os = "macos")]
752    fn update_native_window_shape(&self, window: &Window) {
753        let _session = self.session.enter();
754        use objc2::rc::Retained;
755        use objc2_app_kit::NSView;
756        use winit::raw_window_handle::{HasWindowHandle, RawWindowHandle};
757
758        let raw_window_handle = match window.window_handle() {
759            Ok(handle) => handle.as_raw(),
760            Err(err) => {
761                warn!("Failed to fetch native window handle: {}", err);
762                return;
763            }
764        };
765        let RawWindowHandle::AppKit(appkit) = raw_window_handle else {
766            return;
767        };
768
769        // SAFETY: The pointer comes from winit's window handle and is valid for
770        // the lifetime of this call on the main thread.
771        let create_retained = Retained::<NSView>::retain;
772        let Some(ns_view) = (unsafe { create_retained(appkit.ns_view.as_ptr().cast()) }) else {
773            warn!("Failed to retain NSView from raw window handle");
774            return;
775        };
776
777        let radius_px = if self.config.window.decorations
778            || window.is_maximized()
779            || window.fullscreen().is_some()
780        {
781            0.0
782        } else {
783            8.0 * window.scale_factor()
784        };
785
786        ns_view.setWantsLayer(true);
787        let layer = if let Some(layer) = ns_view.layer() {
788            layer
789        } else {
790            let layer = ns_view.makeBackingLayer();
791            ns_view.setLayer(Some(&layer));
792            layer
793        };
794        layer.setCornerRadius(radius_px as _);
795        layer.setMasksToBounds(radius_px > 0.0);
796    }
797
798    #[cfg(not(any(target_os = "windows", target_os = "macos")))]
799    fn update_native_window_shape(&self, _window: &Window) {
800        let _session = self.session.enter();
801    }
802
803    fn should_set_cursor_pos(
804        cursor_position: Option<crate::PxPosition>,
805        window_width: f64,
806        window_height: f64,
807        edge_threshold: f64,
808    ) -> bool {
809        if let Some(pos) = cursor_position {
810            let x = pos.x as f64;
811            let y = pos.y as f64;
812            x > edge_threshold
813                && x < window_width - edge_threshold
814                && y > edge_threshold
815                && y < window_height - edge_threshold
816        } else {
817            false
818        }
819    }
820
821    fn cursor_resize_direction(
822        cursor_position: Option<crate::PxPosition>,
823        window_size: winit::dpi::PhysicalSize<u32>,
824        edge_threshold: f64,
825    ) -> Option<ResizeDirection> {
826        let position = cursor_position?;
827        let x = position.x as f64;
828        let y = position.y as f64;
829        let width = window_size.width as f64;
830        let height = window_size.height as f64;
831
832        let near_left = x <= edge_threshold;
833        let near_right = x >= width - edge_threshold;
834        let near_top = y <= edge_threshold;
835        let near_bottom = y >= height - edge_threshold;
836
837        match (near_left, near_right, near_top, near_bottom) {
838            (true, _, true, _) => Some(ResizeDirection::NorthWest),
839            (_, true, true, _) => Some(ResizeDirection::NorthEast),
840            (true, _, _, true) => Some(ResizeDirection::SouthWest),
841            (_, true, _, true) => Some(ResizeDirection::SouthEast),
842            (true, _, _, _) => Some(ResizeDirection::West),
843            (_, true, _, _) => Some(ResizeDirection::East),
844            (_, _, true, _) => Some(ResizeDirection::North),
845            (_, _, _, true) => Some(ResizeDirection::South),
846            _ => None,
847        }
848    }
849
850    fn cursor_icon_for_resize(direction: ResizeDirection) -> winit::window::CursorIcon {
851        match direction {
852            ResizeDirection::East | ResizeDirection::West => winit::window::CursorIcon::EwResize,
853            ResizeDirection::North | ResizeDirection::South => winit::window::CursorIcon::NsResize,
854            ResizeDirection::NorthEast | ResizeDirection::SouthWest => {
855                winit::window::CursorIcon::NeswResize
856            }
857            ResizeDirection::NorthWest | ResizeDirection::SouthEast => {
858                winit::window::CursorIcon::NwseResize
859            }
860        }
861    }
862
863    fn log_frame_stats(
864        build_tree_cost: std::time::Duration,
865        draw_cost: std::time::Duration,
866        render_cost: std::time::Duration,
867        render_breakdown: Option<RenderTimingBreakdown>,
868    ) {
869        let total = build_tree_cost + draw_cost + render_cost;
870        let fps = 1.0 / total.as_secs_f32();
871        if fps < 30.0 {
872            if let Some(breakdown) = render_breakdown {
873                warn!(
874                    "Jank detected! Frame statistics:
875Build tree cost: {:?}
876Draw commands cost: {:?}
877Render cost: {:?}
878Total frame cost: {:?}
879Fps: {:.2}
880Render breakdown:
881Acquire: {:?}
882Build passes: {:?}
883Encode: {:?}
884Submit: {:?}
885Present: {:?}
886Render total (core): {:?}
887",
888                    build_tree_cost,
889                    draw_cost,
890                    render_cost,
891                    total,
892                    1.0 / total.as_secs_f32(),
893                    breakdown.acquire,
894                    breakdown.build_passes,
895                    breakdown.encode,
896                    breakdown.submit,
897                    breakdown.present,
898                    breakdown.total,
899                );
900            } else {
901                warn!(
902                    "Jank detected! Frame statistics:
903Build tree cost: {:?}
904Draw commands cost: {:?}
905Render cost: {:?}
906Total frame cost: {:?}
907Fps: {:.2}
908",
909                    build_tree_cost,
910                    draw_cost,
911                    render_cost,
912                    total,
913                    1.0 / total.as_secs_f32()
914                );
915            }
916        }
917    }
918
919    #[instrument(level = "debug", skip(args))]
920    fn compute_draw_commands<'a>(
921        args: &mut RenderFrameArgs<'a>,
922        screen_size: PxSize,
923        frame_idx: u64,
924        retry_focus_move: Option<FocusDirection>,
925        retry_focus_reveal: bool,
926        is_retry: bool,
927    ) -> RenderComputationOutput {
928        let draw_timer = Instant::now();
929        debug!("Computing draw commands...");
930        let cursor_position = args.cursor_state.position();
931        let pointer_changes = if is_retry {
932            Vec::new()
933        } else {
934            args.cursor_state.take_events()
935        };
936        let keyboard_events = if is_retry {
937            Vec::new()
938        } else {
939            args.keyboard_state.take_events()
940        };
941        let ime_events = if is_retry {
942            Vec::new()
943        } else {
944            args.ime_state.take_events()
945        };
946
947        let layout_dirty_nodes = take_layout_dirty_nodes();
948
949        let (
950            graph,
951            window_requests,
952            layout_diagnostics,
953            record_cost,
954            pending_focus_move_retry,
955            pending_focus_reveal_retry,
956            pending_keyboard_retry,
957        ) = TesseraRuntime::with_mut(|rt| {
958            let component_tree = &mut rt.component_tree;
959            component_tree.compute(
960                crate::component_tree::ComputeParams {
961                    screen_size,
962                    cursor_position,
963                    pointer_changes,
964                    keyboard_events,
965                    ime_events,
966                    retry_focus_move,
967                    retry_focus_reveal,
968                    modifiers: args.keyboard_state.modifiers(),
969                    layout_dirty_nodes,
970                },
971                crate::component_tree::ComputeMode::Full,
972            )
973        });
974        flush_pending_focus_callbacks();
975        crate::component_tree::flush_pending_globally_positioned_callbacks();
976        let pending_keyboard_retry = pending_keyboard_retry
977            || TesseraRuntime::with(|runtime| runtime.component_tree.has_pending_focus_reveal());
978
979        let draw_cost = draw_timer.elapsed();
980        debug!("Draw commands computed in {draw_cost:?}");
981        (
982            graph,
983            window_requests,
984            draw_cost,
985            layout_diagnostics,
986            record_cost,
987            pending_focus_move_retry,
988            pending_focus_reveal_retry,
989            pending_keyboard_retry,
990        )
991    }
992
993    #[cfg(feature = "debug-dirty-overlay")]
994    fn layout_node_overlay_rect(
995        metadatas: &crate::component_tree::ComponentNodeMetaDatas,
996        node_id: crate::NodeId,
997    ) -> Option<PxRect> {
998        let metadata = metadatas.get(&node_id)?;
999        let abs_position = metadata.abs_position?;
1000        let computed_data = metadata.computed_data?;
1001        if computed_data.width.0 <= 0 || computed_data.height.0 <= 0 {
1002            return None;
1003        }
1004        let node_rect = PxRect::from_position_size(
1005            abs_position,
1006            PxSize::new(computed_data.width, computed_data.height),
1007        );
1008        Some(
1009            metadata
1010                .event_clip_rect()
1011                .and_then(|clip_rect| clip_rect.intersection(&node_rect))
1012                .unwrap_or(node_rect),
1013        )
1014    }
1015
1016    #[cfg(feature = "debug-dirty-overlay")]
1017    fn collect_dirty_overlay_rects(
1018        screen_size: PxSize,
1019        build_tree_result: &BuildTreeResult,
1020    ) -> Vec<PxRect> {
1021        use crate::component_tree::{NodeRole, direct_layout_children};
1022
1023        if matches!(build_tree_result.mode(), BuildTreeMode::SkipNoInvalidation) {
1024            return Vec::new();
1025        }
1026        if matches!(build_tree_result.mode(), BuildTreeMode::RootRecompose) {
1027            if build_tree_result.had_invalidations() {
1028                return vec![PxRect::from_position_size(PxPosition::ZERO, screen_size)];
1029            }
1030            return Vec::new();
1031        }
1032        TesseraRuntime::with(|rt| {
1033            let tree = rt.component_tree.tree();
1034            let metadatas = rt.component_tree.metadatas();
1035            let mut rects = Vec::new();
1036            for instance_key in build_tree_result.dirty_replay_roots() {
1037                let Some(node_id) = rt
1038                    .component_tree
1039                    .find_node_id_by_instance_key(*instance_key)
1040                else {
1041                    continue;
1042                };
1043                let layout_nodes = match tree.get(node_id) {
1044                    Some(node_ref) if node_ref.get().role == NodeRole::Layout => vec![node_id],
1045                    Some(_) => direct_layout_children(node_id, tree).to_vec(),
1046                    None => continue,
1047                };
1048                for layout_node_id in layout_nodes {
1049                    if let Some(rect) = Self::layout_node_overlay_rect(metadatas, layout_node_id) {
1050                        rects.push(rect);
1051                    }
1052                }
1053            }
1054            rects
1055        })
1056    }
1057
1058    /// Perform the actual GPU rendering for the provided commands and return
1059    /// the render duration.
1060    #[cfg(not(feature = "debug-dirty-overlay"))]
1061    #[instrument(level = "debug", skip(args, graph))]
1062    fn perform_render<'a>(
1063        args: &mut RenderFrameArgs<'a>,
1064        graph: RenderGraph,
1065    ) -> std::time::Duration {
1066        #[cfg(feature = "profiling")]
1067        let _profiler_guard =
1068            ProfilerScopeGuard::new(ProfilerPhase::RenderFrame, None, None, Some("render_frame"));
1069        let render_timer = Instant::now();
1070
1071        // skip actual rendering if window is minimized
1072        if TesseraRuntime::with(|rt| rt.window_minimized) {
1073            return render_timer.elapsed();
1074        }
1075
1076        debug!("Rendering draw commands...");
1077        args.app.render(graph);
1078        let render_cost = render_timer.elapsed();
1079        debug!("Rendered to surface in {render_cost:?}");
1080        render_cost
1081    }
1082
1083    #[cfg(feature = "debug-dirty-overlay")]
1084    #[instrument(level = "debug", skip(args, graph, dirty_overlay_rects))]
1085    fn perform_render<'a>(
1086        args: &mut RenderFrameArgs<'a>,
1087        graph: RenderGraph,
1088        dirty_overlay_rects: &[PxRect],
1089    ) -> std::time::Duration {
1090        #[cfg(feature = "profiling")]
1091        let _profiler_guard =
1092            ProfilerScopeGuard::new(ProfilerPhase::RenderFrame, None, None, Some("render_frame"));
1093        let render_timer = Instant::now();
1094
1095        // skip actual rendering if window is minimized
1096        if TesseraRuntime::with(|rt| rt.window_minimized) {
1097            return render_timer.elapsed();
1098        }
1099
1100        debug!("Rendering draw commands...");
1101        args.app.render(graph, dirty_overlay_rects);
1102        let render_cost = render_timer.elapsed();
1103        debug!("Rendered to surface in {render_cost:?}");
1104        render_cost
1105    }
1106
1107    #[instrument(level = "debug", skip(context))]
1108    fn execute_render_frame(context: RenderFrameContext<'_, F>) -> RenderFrameOutcome {
1109        let RenderFrameContext {
1110            entry_point,
1111            args,
1112            accessibility_enabled,
1113            decorations,
1114            window_label,
1115            frame_idx,
1116            #[cfg(feature = "profiling")]
1117            redraw_reasons,
1118        } = context;
1119        #[cfg(feature = "profiling")]
1120        profiler_begin_frame(frame_idx);
1121        begin_frame_clock(Instant::now());
1122        begin_frame_build_invalidations();
1123        // Tick frame-nanos receivers before build so their state writes are
1124        // consumed by the current recomposition pass.
1125        tick_frame_nanos_receivers();
1126        // notify the windowing system before rendering
1127        // this will help winit to properly schedule and make assumptions about
1128        // its internal state
1129        args.app.window().pre_present_notify();
1130        // and tell runtime the new size
1131        TesseraRuntime::with_mut(|rt| {
1132            rt.component_tree.begin_frame_diagnostics();
1133            rt.window_size = args.app.size().into();
1134        });
1135        // Build the component tree and measure time
1136        let mut build_tree_result = build_component_tree(entry_point);
1137        debug!("Component tree build mode: {:?}", build_tree_result.mode());
1138
1139        // Compute draw commands
1140        let screen_size: PxSize = args.app.size().into();
1141        let (
1142            mut new_graph,
1143            mut window_requests,
1144            mut draw_cost,
1145            mut layout_diagnostics,
1146            mut record_cost,
1147            mut pending_focus_move_retry,
1148            mut pending_focus_reveal_retry,
1149            mut pending_keyboard_retry,
1150        ) = Self::compute_draw_commands(args, screen_size, frame_idx, None, false, false);
1151        let mut settling_retries = 0usize;
1152        while pending_focus_move_retry.is_some()
1153            || pending_focus_reveal_retry
1154            || frame_settling_can_progress(pending_keyboard_retry)
1155        {
1156            let focus_retry = pending_focus_move_retry.is_some()
1157                || pending_focus_reveal_retry
1158                || pending_keyboard_retry;
1159            if settling_retries >= MAX_FRAME_SETTLING_RETRIES
1160                || (focus_retry && !frame_settling_can_progress(pending_keyboard_retry))
1161            {
1162                break;
1163            }
1164
1165            settling_retries += 1;
1166            if focus_retry || has_ready_build_invalidations() {
1167                build_tree_result.absorb_retry(build_component_tree(entry_point));
1168            }
1169
1170            let (
1171                retry_graph,
1172                retry_window_requests,
1173                retry_draw_cost,
1174                retry_layout_diagnostics,
1175                retry_record_cost,
1176                next_focus_move_retry,
1177                next_focus_reveal_retry,
1178                next_keyboard_retry,
1179            ) = Self::compute_draw_commands(
1180                args,
1181                screen_size,
1182                frame_idx,
1183                pending_focus_move_retry,
1184                pending_focus_reveal_retry,
1185                true,
1186            );
1187            new_graph = retry_graph;
1188            window_requests.settle(retry_window_requests);
1189            draw_cost += retry_draw_cost;
1190            layout_diagnostics.accumulate_pass(retry_layout_diagnostics);
1191            record_cost += retry_record_cost;
1192            pending_focus_move_retry = next_focus_move_retry;
1193            pending_focus_reveal_retry = next_focus_reveal_retry;
1194            pending_keyboard_retry = next_keyboard_retry;
1195        }
1196        if pending_focus_reveal_retry
1197            && (has_pending_build_invalidations() || has_pending_layout_invalidations())
1198        {
1199            TesseraRuntime::with_mut(|runtime| {
1200                runtime
1201                    .component_tree
1202                    .focus_owner_mut()
1203                    .retain_reveal_retry();
1204            });
1205        }
1206        prune_persistent_focus_handles();
1207        #[cfg(not(feature = "profiling"))]
1208        let _ = (layout_diagnostics, record_cost);
1209        #[cfg(feature = "debug-dirty-overlay")]
1210        let dirty_overlay_rects =
1211            Self::collect_dirty_overlay_rects(screen_size, &build_tree_result);
1212        #[cfg(feature = "debug-dirty-overlay")]
1213        let overlay_clear_pending = !dirty_overlay_rects.is_empty();
1214        let render_cost = Self::perform_render(
1215            args,
1216            new_graph,
1217            #[cfg(feature = "debug-dirty-overlay")]
1218            &dirty_overlay_rects,
1219        );
1220        // Log frame statistics
1221        let render_breakdown = args.app.last_render_breakdown();
1222        Self::log_frame_stats(
1223            build_tree_result.duration(),
1224            draw_cost,
1225            render_cost,
1226            render_breakdown,
1227        );
1228
1229        #[cfg(feature = "profiling")]
1230        let frame_meta = {
1231            let render_duration_ns = Some(render_cost.as_nanos());
1232            let render_acquire_ns = render_breakdown.map(|breakdown| breakdown.acquire.as_nanos());
1233            let render_build_passes_ns =
1234                render_breakdown.map(|breakdown| breakdown.build_passes.as_nanos());
1235            let render_encode_ns = render_breakdown.map(|breakdown| breakdown.encode.as_nanos());
1236            let render_submit_ns = render_breakdown.map(|breakdown| breakdown.submit.as_nanos());
1237            let render_present_ns = render_breakdown.map(|breakdown| breakdown.present.as_nanos());
1238            #[cfg(not(target_arch = "wasm32"))]
1239            let render_gpu_ns = render_breakdown
1240                .and_then(|_| args.app.read_gpu_time())
1241                .map(|time| time.as_nanos());
1242            #[cfg(target_arch = "wasm32")]
1243            let render_gpu_ns = None;
1244            let inter_frame_wait_ns = (frame_idx > 0).then(|| frame_delta().as_nanos());
1245            let nodes = TesseraRuntime::with_mut(|rt| rt.component_tree.profiler_nodes());
1246            FrameMeta {
1247                frame_idx,
1248                build_mode: build_tree_result.profiler_build_mode(),
1249                redraw_reasons,
1250                inter_frame_wait_ns,
1251                partial_replay_nodes: build_tree_result.partial_replay_nodes(),
1252                total_nodes_before_build: build_tree_result.total_nodes_before_build(),
1253                render_time_ns: render_duration_ns,
1254                render_prepare_ns: None,
1255                render_acquire_ns,
1256                render_build_passes_ns,
1257                render_encode_ns,
1258                render_submit_ns,
1259                render_present_ns,
1260                render_gpu_ns,
1261                build_tree_time_ns: Some(build_tree_result.duration().as_nanos()),
1262                draw_time_ns: Some(draw_cost.as_nanos()),
1263                record_time_ns: Some(record_cost.as_nanos()),
1264                frame_total_ns: None,
1265                layout_diagnostics: Some(layout_diagnostics),
1266                nodes,
1267            }
1268        };
1269
1270        // Prepare accessibility tree update before clearing the component tree
1271        // if needed
1272        let accessibility_update = if accessibility_enabled {
1273            Self::build_accessibility_update(window_label, false)
1274        } else {
1275            None
1276        };
1277
1278        // Handle the window requests (cursor / IME)
1279        // Only set cursor when not at window edges to let window manager handle
1280        // resize cursors
1281        let cursor_position = args.cursor_state.position();
1282        let window_size = args.app.size();
1283        let resize_direction = (desktop_window_controls_available() && !decorations).then(|| {
1284            Self::cursor_resize_direction(cursor_position, window_size, Self::RESIZE_EDGE_THRESHOLD)
1285        });
1286
1287        if let Some(direction) = resize_direction.flatten() {
1288            let icon = Self::cursor_icon_for_resize(direction);
1289            args.app
1290                .window()
1291                .set_cursor(winit::window::Cursor::Icon(icon));
1292        } else {
1293            let should_set_cursor = Self::should_set_cursor_pos(
1294                cursor_position,
1295                window_size.width as f64,
1296                window_size.height as f64,
1297                Self::RESIZE_EDGE_THRESHOLD,
1298            );
1299
1300            if should_set_cursor {
1301                args.app
1302                    .window()
1303                    .set_cursor(winit::window::Cursor::Icon(window_requests.cursor_icon));
1304            }
1305        }
1306
1307        let request_window_drag = window_requests
1308            .request_window_drag
1309            .filter(|_| desktop_window_controls_available());
1310
1311        args.ime_bridge_state
1312            .sync_window(args.app.window(), window_requests.ime_request);
1313
1314        // End of frame cleanup
1315        args.cursor_state.frame_cleanup();
1316
1317        let mut runtime_pending_work = RuntimePendingWork::capture(pending_keyboard_retry);
1318        runtime_pending_work.invalidation_pending |=
1319            args.app.resources().graphics.take_deferred_upload();
1320
1321        RenderFrameOutcome {
1322            accessibility_update,
1323            request_window_drag,
1324            runtime_pending_work,
1325            #[cfg(feature = "profiling")]
1326            frame_meta,
1327            #[cfg(feature = "debug-dirty-overlay")]
1328            overlay_clear_pending,
1329        }
1330    }
1331}
1332
1333impl<F, Output> Renderer<F>
1334where
1335    F: Fn() -> Output,
1336{
1337    fn desktop_platform_context(&self) -> Option<DesktopPlatformContext> {
1338        let _session = self.session.enter();
1339        let app = self.app.as_ref()?;
1340        let redraw_pending = self.redraw_request_pending.clone();
1341        let window = app.window_arc();
1342        let wake_handler = Arc::new(move || {
1343            Self::try_request_redraw(window.as_ref(), redraw_pending.as_ref());
1344        });
1345        Some(DesktopPlatformContext::new(
1346            app.window_arc(),
1347            self.pending_desktop_window_action.clone(),
1348            wake_handler,
1349        ))
1350    }
1351
1352    fn take_pending_desktop_window_action(&self) -> Option<DesktopWindowAction> {
1353        let _session = self.session.enter();
1354        self.pending_desktop_window_action.write().take()
1355    }
1356
1357    #[cfg(target_os = "android")]
1358    fn plugin_context(&self, event_loop: &ActiveEventLoop) -> Option<PluginContext> {
1359        let _session = self.session.enter();
1360        let desktop = self.desktop_platform_context()?;
1361        Some(PluginContext::new(
1362            desktop,
1363            event_loop.android_app().clone(),
1364        ))
1365    }
1366
1367    #[cfg(not(target_os = "android"))]
1368    fn plugin_context(&self, _event_loop: &ActiveEventLoop) -> Option<PluginContext> {
1369        let _session = self.session.enter();
1370        let desktop = self.desktop_platform_context()?;
1371        Some(PluginContext::new(desktop))
1372    }
1373
1374    fn try_request_redraw(window: &Window, redraw_pending: &AtomicBool) {
1375        if redraw_pending
1376            .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
1377            .is_ok()
1378        {
1379            window.request_redraw();
1380        }
1381    }
1382
1383    fn request_redraw_now(&self) {
1384        let _session = self.session.enter();
1385        if let Some(app) = self.app.as_ref() {
1386            Self::try_request_redraw(app.window(), self.redraw_request_pending.as_ref());
1387        }
1388    }
1389
1390    fn install_runtime_redraw_waker(&self) {
1391        let _session = self.session.enter();
1392        let Some(proxy) = self.event_loop_proxy.clone() else {
1393            clear_redraw_waker();
1394            return;
1395        };
1396        install_redraw_waker(Arc::new(move || {
1397            let _ = proxy.send_event(RendererUserEvent::RuntimeRedrawWake);
1398        }));
1399    }
1400
1401    #[cfg(target_family = "wasm")]
1402    fn begin_web_initialization(&mut self, window: Arc<Window>) {
1403        let _session = self.session.enter();
1404        let Some(proxy) = self.event_loop_proxy.clone() else {
1405            error!("Missing event loop proxy for web renderer initialization");
1406            return;
1407        };
1408
1409        self.web_init_in_progress = true;
1410        let epoch = self.web_init_epoch;
1411        let pending_web_inits = self.pending_web_inits.clone();
1412        let sample_count = self.config.sample_count;
1413        let transparent = self.config.window.transparent;
1414        let max_bindings = self.config.max_non_sampler_bindings;
1415        spawn_local(async move {
1416            let render_core =
1417                RenderCore::new(window, sample_count, transparent, max_bindings).await;
1418            pending_web_inits.borrow_mut().push((epoch, render_core));
1419            let _ = proxy.send_event(RendererUserEvent::WebInitReady(epoch));
1420        });
1421    }
1422
1423    #[cfg(target_family = "wasm")]
1424    fn finish_web_initialization(&mut self, event_loop: &ActiveEventLoop, epoch: u64) -> bool {
1425        let _session = self.session.enter();
1426        let pending_render_core = {
1427            let mut pending_web_inits = self.pending_web_inits.borrow_mut();
1428            pending_web_inits
1429                .iter()
1430                .position(|(ready_epoch, _)| *ready_epoch == epoch)
1431                .map(|position| pending_web_inits.remove(position).1)
1432        };
1433        let Some(mut render_core) = pending_render_core else {
1434            return false;
1435        };
1436
1437        if epoch != self.web_init_epoch {
1438            return false;
1439        }
1440
1441        let mut context = PipelineContext::new(&mut render_core);
1442        for module in &self.modules {
1443            module.register_pipelines(&mut context);
1444        }
1445
1446        self.app = Some(render_core);
1447        self.web_init_in_progress = false;
1448        self.apply_pending_window_size();
1449        self.install_runtime_redraw_waker();
1450        #[cfg(feature = "profiling")]
1451        self.request_redraw_with_reasons(WakeSource::Lifecycle, vec![RedrawReason::Startup]);
1452        #[cfg(not(feature = "profiling"))]
1453        self.request_redraw_now();
1454
1455        if let Some(context) = self.plugin_context(event_loop) {
1456            self.plugins.resumed(&context);
1457        }
1458        true
1459    }
1460
1461    #[cfg(feature = "profiling")]
1462    fn request_redraw_with_reasons(&mut self, source: WakeSource, mut reasons: Vec<RedrawReason>) {
1463        let _session = self.session.enter();
1464        reasons.sort_unstable();
1465        reasons.dedup();
1466        if reasons.is_empty() {
1467            return;
1468        }
1469
1470        self.pending_redraw_reasons.extend(reasons.iter().copied());
1471
1472        submit_wake_meta(WakeMeta {
1473            frame_idx: self.frame_index,
1474            source,
1475            reasons: reasons.clone(),
1476        });
1477
1478        self.request_redraw_now();
1479    }
1480
1481    #[cfg(feature = "profiling")]
1482    fn take_pending_redraw_reasons(&mut self) -> Vec<RedrawReason> {
1483        let _session = self.session.enter();
1484        std::mem::take(&mut self.pending_redraw_reasons)
1485            .into_iter()
1486            .collect()
1487    }
1488
1489    // These keep behavior identical but reduce per-function complexity.
1490    fn handle_close_requested(&mut self, event_loop: &ActiveEventLoop) {
1491        let _session = self.session.enter();
1492        if let Some(context) = self.plugin_context(event_loop) {
1493            self.plugins.shutdown(&context);
1494        }
1495        if let Some(ref app) = self.app
1496            && let Err(e) = app.save_pipeline_cache()
1497        {
1498            warn!("Failed to save pipeline cache: {}", e);
1499        }
1500        event_loop.exit();
1501    }
1502
1503    fn apply_window_drag(&mut self, window: &Window, _request: WindowDragRequest) {
1504        let _session = self.session.enter();
1505        if !desktop_window_controls_available() {
1506            return;
1507        }
1508        #[cfg(target_os = "macos")]
1509        if let Some(press) = _request.mouse_press {
1510            self.macos_titlebar.handle_press(press);
1511            self.update_native_window_shape(window);
1512            return;
1513        }
1514        if let Err(err) = window.drag_window() {
1515            warn!("Failed to start window drag: {}", err);
1516        }
1517        self.update_native_window_shape(window);
1518    }
1519
1520    fn apply_desktop_window_action(&mut self, window: &Window, action: DesktopWindowAction) {
1521        let _session = self.session.enter();
1522        match action {
1523            DesktopWindowAction::Minimize => {
1524                window.set_minimized(true);
1525            }
1526            DesktopWindowAction::Maximize => {
1527                window.set_maximized(true);
1528            }
1529            DesktopWindowAction::ToggleMaximize => {
1530                window.set_maximized(!window.is_maximized());
1531            }
1532            DesktopWindowAction::Close => {
1533                self.pending_close_requested = true;
1534            }
1535        }
1536        self.update_native_window_shape(window);
1537    }
1538
1539    /// Applies the most recent window size observed before the render core was
1540    /// installed so the surface matches the real canvas size on the first
1541    /// frame.
1542    fn apply_pending_window_size(&mut self) {
1543        let _session = self.session.enter();
1544        let live_size = self
1545            .app
1546            .as_ref()
1547            .map(|app| window_drawable_size(app.window()));
1548        let size = resolve_startup_window_size(self.pending_window_size.take(), live_size);
1549        let Some(size) = size else {
1550            return;
1551        };
1552        self.handle_resized(size);
1553    }
1554
1555    fn handle_resized(&mut self, size: winit::dpi::PhysicalSize<u32>) {
1556        let _session = self.session.enter();
1557        // Obtain the app inside the method to avoid holding a mutable borrow
1558        // across other borrows of `self`.
1559        let app = match self.app.as_mut() {
1560            Some(app) => app,
1561            None => {
1562                // The render core may still be initializing (asynchronously on
1563                // the web). Keep the size so it is applied once the core is
1564                // installed instead of leaving the surface stale until the next
1565                // resize.
1566                self.pending_window_size = Some(size);
1567                return;
1568            }
1569        };
1570        let window = app.window_arc();
1571        #[cfg(target_family = "wasm")]
1572        let size = if size.width > 0 && size.height > 0 {
1573            web_canvas_physical_size(&window).unwrap_or(size)
1574        } else {
1575            size
1576        };
1577
1578        if size.width == 0 || size.height == 0 {
1579            TesseraRuntime::with_mut(|rt| {
1580                if !rt.window_minimized {
1581                    rt.window_minimized = true;
1582                }
1583            });
1584        } else {
1585            TesseraRuntime::with_mut(|rt| {
1586                if rt.window_minimized {
1587                    rt.window_minimized = false;
1588                }
1589            });
1590            app.resize(size);
1591        }
1592        self.update_native_window_shape(&window);
1593    }
1594
1595    fn handle_cursor_moved(&mut self, position: winit::dpi::PhysicalPosition<f64>) {
1596        let _session = self.session.enter();
1597        if self.resize_in_progress {
1598            return;
1599        }
1600        let px_position = PxPosition::from_f64_arr2([position.x, position.y]);
1601        // Update cursor position
1602        self.cursor_state.update_position(px_position);
1603        self.cursor_state.push_event(PointerChange {
1604            timestamp: Instant::now(),
1605            pointer_id: MOUSE_POINTER_ID,
1606            content: CursorEventContent::Moved(px_position),
1607            gesture_state: GestureState::TapCandidate,
1608            sampled_position: Default::default(),
1609            consumed: false,
1610            tap_claimed: false,
1611            scroll_discrete: None,
1612        });
1613        debug!("Cursor moved to: {}, {}", position.x, position.y);
1614    }
1615
1616    fn handle_cursor_left(&mut self) {
1617        let _session = self.session.enter();
1618        // Clear cursor position when it leaves the window
1619        // This also set the position to None
1620        self.cursor_state.clear();
1621        debug!("Cursor left the window");
1622    }
1623
1624    fn push_accessibility_update(&mut self, tree_update: TreeUpdate) {
1625        let _session = self.session.enter();
1626        if let Some(adapter) = self.accessibility_adapter.as_mut() {
1627            adapter.update_if_active(|| tree_update);
1628        }
1629    }
1630
1631    fn send_accessibility_update(&mut self) {
1632        let _session = self.session.enter();
1633        if let Some(tree_update) = Self::build_accessibility_update(&self.config.window_title, true)
1634        {
1635            self.push_accessibility_update(tree_update);
1636        }
1637    }
1638
1639    fn build_accessibility_update(window_label: &str, full: bool) -> Option<TreeUpdate> {
1640        TesseraRuntime::with_mut(|runtime| {
1641            runtime
1642                .component_tree
1643                .accessibility_update(Some(window_label), full)
1644        })
1645    }
1646
1647    fn handle_mouse_input(
1648        &mut self,
1649        state: winit::event::ElementState,
1650        button: winit::event::MouseButton,
1651    ) {
1652        let _session = self.session.enter();
1653        let Some(event_content) = CursorEventContent::from_press_event(state, button) else {
1654            return; // Ignore unsupported buttons
1655        };
1656
1657        if self.resize_in_progress {
1658            if matches!(
1659                event_content,
1660                CursorEventContent::Released(PressKeyEventType::Left)
1661            ) {
1662                self.resize_in_progress = false;
1663            }
1664            return;
1665        }
1666
1667        if matches!(
1668            event_content,
1669            CursorEventContent::Pressed(PressKeyEventType::Left)
1670        ) && desktop_window_controls_available()
1671            && !self.config.window.decorations
1672            && let Some(app) = self.app.as_ref()
1673        {
1674            let window_size = app.size();
1675            let direction = Self::cursor_resize_direction(
1676                self.cursor_state.position(),
1677                window_size,
1678                Self::RESIZE_EDGE_THRESHOLD,
1679            );
1680            if let Some(direction) = direction {
1681                if let Err(err) = app.window().drag_resize_window(direction) {
1682                    warn!("Failed to start window resize: {}", err);
1683                } else {
1684                    self.resize_in_progress = true;
1685                    self.cursor_state.clear();
1686                    debug!("Started native border resize; suppressing cursor input");
1687                }
1688                return;
1689            }
1690        }
1691        let event = PointerChange {
1692            timestamp: Instant::now(),
1693            pointer_id: MOUSE_POINTER_ID,
1694            content: event_content,
1695            gesture_state: GestureState::TapCandidate,
1696            sampled_position: Default::default(),
1697            consumed: false,
1698            tap_claimed: false,
1699            scroll_discrete: None,
1700        };
1701        #[cfg(target_os = "macos")]
1702        if matches!(
1703            event.content,
1704            CursorEventContent::Pressed(PressKeyEventType::Left)
1705        ) {
1706            self.macos_titlebar.record_press(event.timestamp);
1707        }
1708        if state == winit::event::ElementState::Pressed {
1709            TesseraRuntime::with_mut(|runtime| {
1710                runtime
1711                    .component_tree
1712                    .focus_owner_mut()
1713                    .record_pointer_press(event.timestamp);
1714            });
1715        }
1716        self.cursor_state.push_event(event);
1717        debug!("Mouse input: {state:?} button {button:?}");
1718    }
1719
1720    fn handle_mouse_wheel(&mut self, delta: winit::event::MouseScrollDelta) {
1721        let _session = self.session.enter();
1722        if self.resize_in_progress {
1723            return;
1724        }
1725        let scroll = ScrollEventContent::from_wheel(delta);
1726        let discrete = crate::scroll::is_discrete_platform_wheel(&scroll);
1727        if !scroll.delta_x.is_finite()
1728            || !scroll.delta_y.is_finite()
1729            || (scroll.delta_x == 0.0 && scroll.delta_y == 0.0)
1730        {
1731            return;
1732        }
1733        let mut event = PointerChange::new(
1734            Instant::now(),
1735            MOUSE_POINTER_ID,
1736            CursorEventContent::Scroll(scroll),
1737            GestureState::Dragged,
1738        );
1739        event.scroll_discrete = Some(discrete);
1740        self.cursor_state.push_event(event);
1741        debug!("Mouse scroll: {delta:?}");
1742    }
1743
1744    fn handle_touch(&mut self, touch_event: winit::event::Touch) {
1745        let _session = self.session.enter();
1746        if self.resize_in_progress {
1747            return;
1748        }
1749        let pos = PxPosition::from_f64_arr2([touch_event.location.x, touch_event.location.y]);
1750        debug!(
1751            "Touch event: id {}, phase {:?}, position {:?}",
1752            touch_event.id, touch_event.phase, pos
1753        );
1754        match touch_event.phase {
1755            winit::event::TouchPhase::Started => {
1756                TesseraRuntime::with_mut(|runtime| {
1757                    runtime
1758                        .component_tree
1759                        .focus_owner_mut()
1760                        .record_pointer_press(Instant::now());
1761                });
1762                // Use new touch start handling method
1763                self.cursor_state.handle_touch_start(touch_event.id, pos);
1764            }
1765            winit::event::TouchPhase::Moved => {
1766                // Use new touch move handling method, may generate scroll event
1767                if let Some(scroll_event) = self.cursor_state.handle_touch_move(touch_event.id, pos)
1768                {
1769                    // Scroll event is already added to event queue in
1770                    // handle_touch_move
1771                    self.cursor_state.push_event(scroll_event);
1772                }
1773            }
1774            winit::event::TouchPhase::Ended => {
1775                self.cursor_state.handle_touch_end(touch_event.id);
1776            }
1777            winit::event::TouchPhase::Cancelled => {
1778                self.cursor_state.handle_touch_cancel(touch_event.id);
1779            }
1780        }
1781    }
1782
1783    fn handle_keyboard_input(&mut self, event: winit::event::KeyEvent) {
1784        let _session = self.session.enter();
1785        debug!("Keyboard input: {event:?}");
1786        TesseraRuntime::with_mut(|runtime| {
1787            runtime
1788                .component_tree
1789                .focus_owner_mut()
1790                .record_keyboard_input(event.state, Instant::now());
1791        });
1792        self.keyboard_state.push_event(event.into());
1793    }
1794
1795    fn flush_focused_input(&mut self) {
1796        if self.app.is_none() {
1797            return;
1798        }
1799        let modifiers = self.keyboard_state.modifiers();
1800        let keyboard = self.keyboard_state.take_events();
1801        let ime = self.ime_state.take_events();
1802        let requests = TesseraRuntime::with_mut(|runtime| {
1803            runtime.component_tree.flush_focused_input(
1804                &mut self.cursor_state,
1805                modifiers,
1806                keyboard,
1807                ime,
1808            )
1809        });
1810        flush_pending_focus_callbacks();
1811        if let Some(request) = requests.request_window_drag {
1812            let window = self.app.as_ref().unwrap().window_arc();
1813            self.apply_window_drag(&window, request);
1814        }
1815        let request = requests.ime_request.filter(|request| {
1816            TesseraRuntime::with(|runtime| {
1817                request.client_id == runtime.component_tree.focus_owner().active_handle_id()
1818            })
1819        });
1820        // The next native event must see edits made by this event, before the next frame.
1821        self.ime_bridge_state
1822            .sync_window(self.app.as_ref().unwrap().window(), request);
1823    }
1824
1825    fn handle_redraw_requested(&mut self, event_loop: &ActiveEventLoop) {
1826        let _session = self.session.enter();
1827        self.redraw_request_pending.store(false, Ordering::Release);
1828        let mut app = match self.app.take() {
1829            Some(app) => app,
1830            None => return,
1831        };
1832
1833        #[cfg(feature = "profiling")]
1834        let frame_timer = Instant::now();
1835        crate::window_insets::platform::refresh(app.window(), event_loop);
1836        app.resize_if_needed();
1837        let accessibility_enabled =
1838            self.accessibility_adapter.is_some() && self.accessibility_active;
1839        let frame_idx = self.frame_index;
1840        #[cfg(feature = "profiling")]
1841        let redraw_reasons = self.take_pending_redraw_reasons();
1842        let window_label = &self.config.window_title;
1843        let RenderFrameOutcome {
1844            accessibility_update,
1845            request_window_drag,
1846            runtime_pending_work,
1847            #[cfg(feature = "profiling")]
1848            mut frame_meta,
1849            #[cfg(feature = "debug-dirty-overlay")]
1850            overlay_clear_pending,
1851        } = {
1852            let mut args = RenderFrameArgs {
1853                cursor_state: &mut self.cursor_state,
1854                keyboard_state: &mut self.keyboard_state,
1855                ime_state: &mut self.ime_state,
1856                ime_bridge_state: &mut self.ime_bridge_state,
1857                app: &mut app,
1858            };
1859            Self::execute_render_frame(RenderFrameContext {
1860                entry_point: &self.entry_point,
1861                args: &mut args,
1862                accessibility_enabled,
1863                decorations: self.config.window.decorations,
1864                window_label,
1865                frame_idx,
1866                #[cfg(feature = "profiling")]
1867                redraw_reasons,
1868            })
1869        };
1870
1871        if let Some(request) = request_window_drag {
1872            self.apply_window_drag(app.window(), request);
1873        }
1874        #[cfg(target_os = "macos")]
1875        self.macos_titlebar.clear();
1876        if let Some(action) = self.take_pending_desktop_window_action() {
1877            self.apply_desktop_window_action(app.window(), action);
1878        }
1879
1880        self.frame_index = self.frame_index.wrapping_add(1);
1881
1882        if let Some(tree_update) = accessibility_update {
1883            self.push_accessibility_update(tree_update);
1884        }
1885
1886        #[cfg(feature = "profiling")]
1887        {
1888            frame_meta.frame_total_ns = Some(frame_timer.elapsed().as_nanos());
1889            submit_frame_meta(frame_meta);
1890            profiler_end_frame();
1891        }
1892
1893        self.app = Some(app);
1894        #[cfg(target_os = "android")]
1895        if let Some(back) = self.android_back.as_mut() {
1896            back.set_enabled(crate::system_back::enabled());
1897        }
1898
1899        if runtime_pending_work.requires_redraw() {
1900            #[cfg(feature = "profiling")]
1901            self.request_redraw_with_reasons(
1902                WakeSource::Runtime,
1903                runtime_pending_work.redraw_reasons(),
1904            );
1905            #[cfg(not(feature = "profiling"))]
1906            self.request_redraw_now();
1907        }
1908        #[cfg(feature = "debug-dirty-overlay")]
1909        if overlay_clear_pending {
1910            self.request_redraw_now();
1911        }
1912    }
1913}
1914
1915#[cfg(target_family = "wasm")]
1916fn web_canvas_physical_size(window: &Window) -> Option<winit::dpi::PhysicalSize<u32>> {
1917    use winit::platform::web::WindowExtWebSys;
1918
1919    let canvas = window.canvas()?;
1920    let style = web_sys::window()?.get_computed_style(&canvas).ok()??;
1921    let pixels = |property: &str| -> Option<f64> {
1922        style
1923            .get_property_value(property)
1924            .ok()?
1925            .strip_suffix("px")?
1926            .parse()
1927            .ok()
1928    };
1929    let mut width = pixels("width")?;
1930    let mut height = pixels("height")?;
1931    if style.get_property_value("box-sizing").ok()? == "border-box" {
1932        width -= pixels("padding-left")?
1933            + pixels("padding-right")?
1934            + pixels("border-left-width")?
1935            + pixels("border-right-width")?;
1936        height -= pixels("padding-top")?
1937            + pixels("padding-bottom")?
1938            + pixels("border-top-width")?
1939            + pixels("border-bottom-width")?;
1940    }
1941    // Device-pixel content boxes can disagree with emulated DPI. Use the same
1942    // density as pointer coordinates so the canvas and input stay aligned.
1943    canvas_physical_size([width, height], window.scale_factor())
1944}
1945
1946#[cfg(any(target_family = "wasm", test))]
1947fn canvas_physical_size(logical: [f64; 2], density: f64) -> Option<winit::dpi::PhysicalSize<u32>> {
1948    if !density.is_finite() || density <= 0.0 {
1949        return None;
1950    }
1951    let physical = logical.map(|size| size * density);
1952    if physical
1953        .iter()
1954        .any(|size| !size.is_finite() || *size <= 0.0 || *size > u32::MAX as f64)
1955    {
1956        return None;
1957    }
1958    Some(winit::dpi::PhysicalSize::new(
1959        physical[0].round().max(1.0) as u32,
1960        physical[1].round().max(1.0) as u32,
1961    ))
1962}
1963
1964fn window_drawable_size(window: &Window) -> winit::dpi::PhysicalSize<u32> {
1965    // UIKit draws across the entire window, while Winit's inner size excludes the safe area.
1966    #[cfg(target_os = "ios")]
1967    {
1968        window.outer_size()
1969    }
1970    #[cfg(not(target_os = "ios"))]
1971    {
1972        window.inner_size()
1973    }
1974}
1975
1976// Resize events may arrive before the asynchronous web render core is ready.
1977fn resolve_startup_window_size(
1978    pending: Option<winit::dpi::PhysicalSize<u32>>,
1979    live: Option<winit::dpi::PhysicalSize<u32>>,
1980) -> Option<winit::dpi::PhysicalSize<u32>> {
1981    fn usable(size: &winit::dpi::PhysicalSize<u32>) -> bool {
1982        size.width > 0 && size.height > 0
1983    }
1984
1985    match pending {
1986        Some(size) if usable(&size) => Some(size),
1987        _ => live.filter(usable),
1988    }
1989}
1990
1991/// Handle application lifecycle and window events.
1992impl<F, Output> ApplicationHandler<RendererUserEvent> for Renderer<F>
1993where
1994    F: Fn() -> Output,
1995{
1996    #[tracing::instrument(level = "debug", skip(self, event_loop))]
1997    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
1998        let _session = self.session.enter();
1999        event_loop.set_control_flow(ControlFlow::Wait);
2000        let app_titlebar = self.config.window.app_titlebar();
2001        crate::local_runtime_resource::<Cell<AppTitleBar>>().set(app_titlebar);
2002        #[cfg(feature = "profiling")]
2003        submit_runtime_meta(RuntimeMeta {
2004            kind: RuntimeEventKind::Resumed,
2005        });
2006        #[cfg(target_family = "wasm")]
2007        self.finish_web_initialization(event_loop, self.web_init_epoch);
2008        // Just return if the app is already created
2009        if self.app.is_some() {
2010            self.install_runtime_redraw_waker();
2011            #[cfg(feature = "profiling")]
2012            self.request_redraw_with_reasons(WakeSource::Lifecycle, vec![RedrawReason::Startup]);
2013            #[cfg(not(feature = "profiling"))]
2014            self.request_redraw_now();
2015            return;
2016        }
2017        #[cfg(target_family = "wasm")]
2018        if self.web_init_in_progress {
2019            return;
2020        }
2021
2022        // Create a new window (initially hidden for AccessKit initialization)
2023        let window_attributes = Window::default_attributes()
2024            .with_title(&self.config.window_title)
2025            .with_window_icon(self.config.window.icon.clone())
2026            .with_decorations(self.config.window.decorations)
2027            .with_resizable(self.config.window.resizable)
2028            .with_transparent(self.config.window.transparent)
2029            .with_visible(false); // Hide initially for AccessKit
2030        #[cfg(target_os = "windows")]
2031        let window_attributes = {
2032            use winit::platform::windows::WindowAttributesExtWindows;
2033            if let Some(identifier) = &self.config.window.application_id {
2034                let identifier = windows::core::HSTRING::from(identifier);
2035                if let Err(error) = unsafe {
2036                    windows::Win32::UI::Shell::SetCurrentProcessExplicitAppUserModelID(&identifier)
2037                } {
2038                    tracing::warn!(%error, "Failed to set application identity");
2039                }
2040            }
2041            window_attributes.with_taskbar_icon(self.config.window.icon.clone())
2042        };
2043        #[cfg(any(
2044            target_os = "linux",
2045            target_os = "freebsd",
2046            target_os = "dragonfly",
2047            target_os = "netbsd",
2048            target_os = "openbsd"
2049        ))]
2050        let window_attributes = if let Some(identifier) = &self.config.window.application_id {
2051            let attributes = winit::platform::wayland::WindowAttributesExtWayland::with_name(
2052                window_attributes,
2053                identifier,
2054                identifier,
2055            );
2056            winit::platform::x11::WindowAttributesExtX11::with_name(
2057                attributes, identifier, identifier,
2058            )
2059        } else {
2060            window_attributes
2061        };
2062        #[cfg(target_os = "macos")]
2063        let window_attributes = {
2064            use winit::platform::macos::WindowAttributesExtMacOS;
2065
2066            if app_titlebar == AppTitleBar::Native {
2067                window_attributes
2068                    .with_titlebar_transparent(true)
2069                    .with_title_hidden(true)
2070                    .with_fullsize_content_view(true)
2071            } else {
2072                window_attributes
2073            }
2074        };
2075        #[cfg(target_family = "wasm")]
2076        let window_attributes = {
2077            use winit::platform::web::WindowAttributesExtWebSys;
2078
2079            if let Some(canvas) = Self::resolve_web_canvas(&self.config) {
2080                window_attributes
2081                    .with_canvas(Some(canvas))
2082                    .with_append(false)
2083            } else {
2084                window_attributes.with_append(true)
2085            }
2086        };
2087        let window = match event_loop.create_window(window_attributes) {
2088            Ok(window) => Arc::new(window),
2089            Err(err) => {
2090                error!("Failed to create window: {err}");
2091                return;
2092            }
2093        };
2094
2095        // Initialize AccessKit adapter BEFORE showing the window
2096        if let Some(proxy) = self.event_loop_proxy.clone() {
2097            self.accessibility_active = false;
2098            self.accessibility_adapter = Some(AccessKitAdapter::with_event_loop_proxy(
2099                event_loop, &window, proxy,
2100            ));
2101        }
2102
2103        // Now show the window after AccessKit is initialized
2104        window.set_visible(true);
2105        self.update_native_window_shape(&window);
2106
2107        #[cfg(target_family = "wasm")]
2108        {
2109            self.begin_web_initialization(window);
2110            return;
2111        }
2112
2113        #[cfg(not(target_family = "wasm"))]
2114        {
2115            let mut render_core = pollster::block_on(RenderCore::new(
2116                window.clone(),
2117                self.config.sample_count,
2118                self.config.window.transparent,
2119                self.config.max_non_sampler_bindings,
2120            ));
2121
2122            // Register pipelines
2123            let mut context = PipelineContext::new(&mut render_core);
2124            for module in &self.modules {
2125                module.register_pipelines(&mut context);
2126            }
2127
2128            self.app = Some(render_core);
2129            self.apply_pending_window_size();
2130            self.install_runtime_redraw_waker();
2131            #[cfg(feature = "profiling")]
2132            self.request_redraw_with_reasons(WakeSource::Lifecycle, vec![RedrawReason::Startup]);
2133            #[cfg(not(feature = "profiling"))]
2134            self.request_redraw_now();
2135
2136            if let Some(context) = self.plugin_context(event_loop) {
2137                self.plugins.resumed(&context);
2138            }
2139        }
2140    }
2141
2142    fn suspended(&mut self, event_loop: &ActiveEventLoop) {
2143        let _session = self.session.enter();
2144        debug!("Suspending renderer; tearing down WGPU resources.");
2145        #[cfg(target_os = "android")]
2146        if let Some(back) = self.android_back.as_mut() {
2147            back.set_enabled(false);
2148            crate::system_back::dispatch(crate::system_back::SystemBackEvent::Cancelled);
2149        }
2150        #[cfg(feature = "profiling")]
2151        submit_runtime_meta(RuntimeMeta {
2152            kind: RuntimeEventKind::Suspended,
2153        });
2154
2155        if let Some(context) = self.plugin_context(event_loop) {
2156            self.plugins.suspended(&context);
2157        }
2158
2159        self.app = None;
2160
2161        // Clean up AccessKit adapter
2162        self.accessibility_adapter = None;
2163        self.accessibility_active = false;
2164
2165        self.cursor_state = CursorState::default();
2166        self.keyboard_state = KeyboardState::default();
2167        self.ime_state = ImeState::default();
2168        self.ime_bridge_state.reset();
2169        self.resize_in_progress = false;
2170        self.redraw_request_pending.store(false, Ordering::Release);
2171        #[cfg(target_family = "wasm")]
2172        {
2173            self.web_init_epoch = self.web_init_epoch.wrapping_add(1);
2174            self.web_init_in_progress = false;
2175            self.pending_web_inits.borrow_mut().clear();
2176        }
2177
2178        TesseraRuntime::with_mut(|runtime| {
2179            runtime.component_tree.reset();
2180            runtime.cursor_icon_request = None;
2181            runtime.window_minimized = false;
2182            runtime.window_size = [0, 0];
2183        });
2184        clear_layout_snapshots();
2185        reset_layout_dirty_tracking();
2186        reset_component_replay_tracking();
2187        reset_focus_read_dependencies();
2188        reset_render_slot_read_dependencies();
2189        reset_state_read_dependencies();
2190        reset_context_read_dependencies();
2191        reset_build_invalidations();
2192        reset_frame_clock();
2193        clear_redraw_waker();
2194        clear_persistent_focus_handles();
2195        crate::runtime::reset_component_slots();
2196        #[cfg(feature = "profiling")]
2197        self.pending_redraw_reasons.clear();
2198    }
2199
2200    fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
2201        let _session = self.session.enter();
2202        if cfg!(target_os = "ios") && self.redraw_request_pending.load(Ordering::Acquire) {
2203            // UIKit can coalesce view redraws while the Metal sublayer presents.
2204            self.handle_redraw_requested(event_loop);
2205        } else if let Some(app) = self.app.as_ref() {
2206            crate::window_insets::platform::refresh(app.window(), event_loop);
2207        }
2208        #[cfg(target_os = "ios")]
2209        {
2210            let flow = if self.redraw_request_pending.load(Ordering::Acquire) {
2211                ControlFlow::Poll
2212            } else {
2213                ControlFlow::Wait
2214            };
2215            event_loop.set_control_flow(flow);
2216        }
2217    }
2218
2219    #[tracing::instrument(level = "debug", skip(self, event_loop))]
2220    fn window_event(
2221        &mut self,
2222        event_loop: &ActiveEventLoop,
2223        _window_id: WindowId,
2224        event: WindowEvent,
2225    ) {
2226        let _session = self.session.enter();
2227        if self.pending_close_requested {
2228            self.pending_close_requested = false;
2229            self.handle_close_requested(event_loop);
2230            return;
2231        }
2232
2233        // Forward event to AccessKit adapter
2234        if let (Some(adapter), Some(app)) = (&mut self.accessibility_adapter, &self.app) {
2235            adapter.process_event(app.window(), &event);
2236        }
2237
2238        // Handle window events
2239        let mut request_redraw = false;
2240        #[cfg(feature = "profiling")]
2241        let mut redraw_reasons = Vec::new();
2242        match event {
2243            WindowEvent::CloseRequested => {
2244                self.handle_close_requested(event_loop);
2245            }
2246            WindowEvent::Resized(size) => {
2247                self.handle_resized(size);
2248                request_redraw = true;
2249                #[cfg(feature = "profiling")]
2250                redraw_reasons.push(RedrawReason::WindowResized);
2251            }
2252            WindowEvent::CursorMoved {
2253                device_id: _,
2254                position,
2255            } => {
2256                self.handle_cursor_moved(position);
2257                request_redraw = true;
2258                #[cfg(feature = "profiling")]
2259                redraw_reasons.push(RedrawReason::CursorMoved);
2260            }
2261            WindowEvent::CursorLeft { device_id: _ } => {
2262                self.handle_cursor_left();
2263                request_redraw = true;
2264                #[cfg(feature = "profiling")]
2265                redraw_reasons.push(RedrawReason::CursorLeft);
2266            }
2267            WindowEvent::MouseInput {
2268                device_id: _,
2269                state,
2270                button,
2271            } => {
2272                self.handle_mouse_input(state, button);
2273                request_redraw = true;
2274                #[cfg(feature = "profiling")]
2275                redraw_reasons.push(RedrawReason::MouseInput);
2276            }
2277            WindowEvent::MouseWheel {
2278                device_id: _,
2279                delta,
2280                phase: _,
2281            } => {
2282                self.handle_mouse_wheel(delta);
2283                request_redraw = true;
2284                #[cfg(feature = "profiling")]
2285                redraw_reasons.push(RedrawReason::MouseWheel);
2286            }
2287            WindowEvent::Touch(touch_event) => {
2288                self.handle_touch(touch_event);
2289                request_redraw = true;
2290                #[cfg(feature = "profiling")]
2291                redraw_reasons.push(RedrawReason::TouchInput);
2292            }
2293            WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
2294                set_scale_factor(scale_factor);
2295                #[cfg(target_family = "wasm")]
2296                if let Some(size) = self
2297                    .app
2298                    .as_ref()
2299                    .map(|app| window_drawable_size(app.window()))
2300                {
2301                    self.handle_resized(size);
2302                }
2303                if let Some(app) = self.app.as_ref() {
2304                    self.update_native_window_shape(app.window());
2305                }
2306                request_redraw = true;
2307                #[cfg(feature = "profiling")]
2308                redraw_reasons.push(RedrawReason::ScaleFactorChanged);
2309            }
2310            WindowEvent::KeyboardInput { event, .. } => {
2311                self.handle_keyboard_input(event);
2312                self.flush_focused_input();
2313                request_redraw = true;
2314                #[cfg(feature = "profiling")]
2315                redraw_reasons.push(RedrawReason::KeyboardInput);
2316            }
2317            WindowEvent::ModifiersChanged(modifiers) => {
2318                debug!("Modifiers changed: {modifiers:?}");
2319                self.keyboard_state.update_modifiers(modifiers.state());
2320                request_redraw = true;
2321                #[cfg(feature = "profiling")]
2322                redraw_reasons.push(RedrawReason::ModifiersChanged);
2323            }
2324            WindowEvent::Ime(ime_event) => {
2325                debug!("IME event: {ime_event:?}");
2326                if let Some(client) = self.ime_bridge_state.receive_event(&ime_event) {
2327                    self.ime_state.push_event_for(Some(client), ime_event);
2328                    self.flush_focused_input();
2329                }
2330                request_redraw = true;
2331                #[cfg(feature = "profiling")]
2332                redraw_reasons.push(RedrawReason::ImeEvent);
2333            }
2334            WindowEvent::Focused(focused) => {
2335                TesseraRuntime::with_mut(|runtime| {
2336                    runtime
2337                        .component_tree
2338                        .focus_owner_mut()
2339                        .set_owner_focused(focused);
2340                });
2341                flush_pending_focus_callbacks();
2342                if self.resize_in_progress {
2343                    self.resize_in_progress = false;
2344                    self.cursor_state.clear();
2345                }
2346                request_redraw = true;
2347                #[cfg(feature = "profiling")]
2348                redraw_reasons.push(RedrawReason::FocusChanged);
2349            }
2350            WindowEvent::RedrawRequested => {
2351                self.handle_redraw_requested(event_loop);
2352            }
2353            _ => (),
2354        }
2355
2356        if request_redraw {
2357            #[cfg(feature = "profiling")]
2358            self.request_redraw_with_reasons(WakeSource::WindowEvent, redraw_reasons);
2359            #[cfg(not(feature = "profiling"))]
2360            self.request_redraw_now();
2361        }
2362    }
2363
2364    fn user_event(&mut self, event_loop: &ActiveEventLoop, event: RendererUserEvent) {
2365        let _session = self.session.enter();
2366        use accesskit_winit::WindowEvent as AccessKitWindowEvent;
2367        #[cfg(not(target_family = "wasm"))]
2368        let _ = event_loop;
2369
2370        match event {
2371            RendererUserEvent::RuntimeRedrawWake => {
2372                // A frame can consume invalidations before their queued wake arrives.
2373                let pending = RuntimePendingWork::capture(false);
2374                if !pending.requires_redraw() {
2375                    return;
2376                }
2377                #[cfg(feature = "profiling")]
2378                self.request_redraw_with_reasons(WakeSource::Runtime, pending.redraw_reasons());
2379                #[cfg(not(feature = "profiling"))]
2380                self.request_redraw_now();
2381            }
2382            #[cfg(target_family = "wasm")]
2383            RendererUserEvent::WebInitReady(epoch) => {
2384                self.finish_web_initialization(event_loop, epoch);
2385            }
2386            RendererUserEvent::AccessKit(event) => {
2387                if self.accessibility_adapter.is_none() {
2388                    return;
2389                }
2390                match event.window_event {
2391                    AccessKitWindowEvent::InitialTreeRequested => {
2392                        self.accessibility_active = true;
2393                        self.send_accessibility_update();
2394                    }
2395                    AccessKitWindowEvent::ActionRequested(action_request) => {
2396                        // Dispatch action to the appropriate component handler
2397                        let handled = TesseraRuntime::with_mut(|runtime| {
2398                            let (tree, metadatas, focus_owner) =
2399                                runtime.component_tree.accessibility_dispatch_context();
2400                            crate::accessibility::dispatch_action(
2401                                tree,
2402                                metadatas,
2403                                focus_owner,
2404                                action_request,
2405                            )
2406                        });
2407                        flush_pending_focus_callbacks();
2408
2409                        if !handled {
2410                            debug!("Action was not handled by any component");
2411                        }
2412                    }
2413                    AccessKitWindowEvent::AccessibilityDeactivated => {
2414                        self.accessibility_active = false;
2415                        TesseraRuntime::with_mut(|runtime| {
2416                            runtime.component_tree.stop_accessibility_updates();
2417                        });
2418                        debug!("AccessKit deactivated");
2419                    }
2420                }
2421            }
2422        }
2423    }
2424}
2425
2426#[cfg(test)]
2427mod tests {
2428
2429    use winit::dpi::PhysicalSize;
2430
2431    use crate::{
2432        FocusRequester, Modifier, PxRect, State,
2433        component_tree::dispatch_pending_focus_reveal_request,
2434        focus::flush_pending_focus_callbacks,
2435        layout::{LayoutResult, MeasureScope, layout},
2436        modifier::{FocusModifierExt as _, LayoutModifierNode},
2437        remember,
2438        runtime::{TesseraRuntime, prune_persistent_focus_handles},
2439        testing::{layout_test, pump_layout_test_session, start_layout_test_session},
2440    };
2441
2442    use super::{
2443        RendererImeBridgeState, RendererImeBridgeUpdate, RuntimePendingWork, canvas_physical_size,
2444        resolve_startup_window_size,
2445    };
2446
2447    #[derive(Clone, Default, PartialEq)]
2448    struct FrameEndProbe {
2449        focus: crate::testing::TestProbe<Option<(State<bool>, FocusRequester, FocusRequester)>>,
2450        events: crate::testing::TestProbe<usize>,
2451        reveals: crate::testing::TestProbe<Vec<PxRect>>,
2452    }
2453
2454    #[derive(Clone, PartialEq)]
2455    struct FocusLeafLayout;
2456
2457    impl LayoutModifierNode for FocusLeafLayout {
2458        fn measure(
2459            &self,
2460            _scope: &mut MeasureScope<'_>,
2461        ) -> Result<LayoutResult, crate::MeasurementError> {
2462            Ok(LayoutResult::new(crate::ComputedData {
2463                width: crate::Px(64),
2464                height: crate::Px(24),
2465            }))
2466        }
2467    }
2468
2469    #[crate::tessera(crate)]
2470    fn frame_end_removable_target(requester: FocusRequester) {
2471        layout()
2472            .policy(FocusLeafLayout)
2473            .modifier(Modifier::new().focusable().focus_requester(requester));
2474    }
2475
2476    #[crate::tessera(crate)]
2477    fn frame_end_focus_fixture(probe: FrameEndProbe) {
2478        let visible = remember(|| true);
2479        let removed = remember(FocusRequester::new).get();
2480        let fallback = remember(FocusRequester::new).get();
2481        probe.focus.set(Some((visible, removed, fallback)));
2482        let reveals = probe.reveals.clone();
2483        let events = probe.events.clone();
2484        layout()
2485            .modifier(
2486                Modifier::new()
2487                    .focus_restorer(Some(fallback))
2488                    .focus_reveal_handler(move |request: crate::focus::FocusRevealRequest| {
2489                        reveals.with_mut(|rects| rects.push(request.target_rect));
2490                        false
2491                    }),
2492            )
2493            .child(move || {
2494                let events = events.clone();
2495                if visible.get() {
2496                    frame_end_removable_target(removed);
2497                }
2498                layout().policy(FocusLeafLayout).modifier(
2499                    Modifier::new()
2500                        .focusable()
2501                        .focus_requester(fallback)
2502                        .on_focus_changed(move |state: crate::focus::FocusState| {
2503                            if state.is_focused() {
2504                                events.with_mut(|events| *events += 1);
2505                            }
2506                        }),
2507                );
2508            });
2509    }
2510
2511    #[derive(Clone, Default, PartialEq)]
2512    struct VisibilityProbe {
2513        focus: crate::testing::TestProbe<Option<(FocusRequester, FocusRequester)>>,
2514        reads: crate::testing::TestProbe<Vec<bool>>,
2515    }
2516
2517    #[crate::tessera(crate)]
2518    fn focus_visibility_observer(
2519        requester: FocusRequester,
2520        reads: crate::testing::TestProbe<Vec<bool>>,
2521    ) {
2522        reads.with_mut(|reads| reads.push(requester.is_focus_visible()));
2523        layout().policy(FocusLeafLayout);
2524    }
2525
2526    #[crate::tessera(crate)]
2527    fn focus_visibility_fixture(probe: VisibilityProbe) {
2528        let first = remember(FocusRequester::new).get();
2529        let second = remember(FocusRequester::new).get();
2530        probe.focus.set(Some((first, second)));
2531        layout().child(move || {
2532            frame_end_removable_target(first);
2533            frame_end_removable_target(second);
2534            focus_visibility_observer(first, probe.reads.clone());
2535        });
2536    }
2537
2538    #[test]
2539    fn focus_visibility_replays_subscribers_without_changing_same_target_ownership() {
2540        let _runtime_session = crate::RuntimeSession::new();
2541        let _runtime_scope = _runtime_session.enter();
2542        use std::time::Duration;
2543
2544        use winit::event::ElementState;
2545
2546        use crate::{
2547            CursorEventContent, GestureState, MOUSE_POINTER_ID, PointerChange, PxPosition,
2548            cursor::PressKeyEventType, testing::pump_layout_test_session_with_input, time::Instant,
2549        };
2550
2551        let probe = VisibilityProbe::default();
2552        let mut session =
2553            start_layout_test_session(layout_test(|| focus_visibility_fixture(probe.clone())));
2554        pump_layout_test_session(&mut session, 0);
2555        let (first, second) = probe.focus.get().unwrap();
2556        first.request_focus();
2557        first.capture_focus();
2558        pump_layout_test_session(&mut session, 1);
2559        assert!(!first.is_focus_visible());
2560        let origin = Instant::now();
2561        let keyboard = origin + Duration::from_millis(10);
2562        let press = origin + Duration::from_millis(20);
2563        let newer_keyboard = origin + Duration::from_millis(30);
2564        let key = |timestamp| {
2565            TesseraRuntime::with_mut(|runtime| {
2566                runtime
2567                    .component_tree
2568                    .focus_owner_mut()
2569                    .record_keyboard_input(ElementState::Pressed, timestamp);
2570            });
2571        };
2572        key(keyboard);
2573        pump_layout_test_session(&mut session, 2);
2574        assert!(first.is_captured());
2575        assert_eq!(probe.reads.with(|reads| reads.last().copied()), Some(true));
2576        let pointer = |timestamp| PointerChange {
2577            timestamp,
2578            pointer_id: MOUSE_POINTER_ID,
2579            content: CursorEventContent::Pressed(PressKeyEventType::Left),
2580            gesture_state: GestureState::TapCandidate,
2581            sampled_position: Default::default(),
2582            consumed: false,
2583            tap_claimed: false,
2584            scroll_discrete: None,
2585        };
2586        pump_layout_test_session_with_input(
2587            &mut session,
2588            3,
2589            vec![pointer(press)],
2590            Some(PxPosition::ZERO),
2591        );
2592        pump_layout_test_session(&mut session, 4);
2593        assert!(first.is_captured());
2594        assert_eq!(probe.reads.with(|reads| reads.last().copied()), Some(false));
2595        key(keyboard);
2596        first.request_focus();
2597        pump_layout_test_session(&mut session, 5);
2598        assert!(!first.is_focus_visible());
2599        key(newer_keyboard);
2600        pump_layout_test_session_with_input(
2601            &mut session,
2602            6,
2603            vec![pointer(press)],
2604            Some(PxPosition::ZERO),
2605        );
2606        pump_layout_test_session(&mut session, 7);
2607        assert!(first.is_captured());
2608        assert_eq!(probe.reads.with(|reads| reads.last().copied()), Some(true));
2609        let mut hover = pointer(origin + Duration::from_millis(40));
2610        hover.content = CursorEventContent::Moved(PxPosition::ZERO);
2611        let mut wheel = pointer(origin + Duration::from_millis(50));
2612        wheel.content = CursorEventContent::Scroll(crate::cursor::ScrollEventContent {
2613            delta_x: 0.0,
2614            delta_y: -24.0,
2615            unit: crate::cursor::ScrollDeltaUnit::Pixel,
2616            source: crate::cursor::ScrollEventSource::Wheel,
2617        });
2618        pump_layout_test_session_with_input(
2619            &mut session,
2620            8,
2621            vec![hover, wheel],
2622            Some(PxPosition::ZERO),
2623        );
2624        assert!(first.is_captured());
2625        assert!(first.is_focus_visible());
2626        first.free_focus();
2627        second.request_focus();
2628        assert!(second.is_focus_visible());
2629        assert!(!first.is_focus_visible());
2630        pump_layout_test_session_with_input(
2631            &mut session,
2632            9,
2633            vec![pointer(origin + Duration::from_millis(60))],
2634            Some(PxPosition::ZERO),
2635        );
2636        first.request_focus();
2637        assert!(first.is_focused());
2638        assert!(!first.is_focus_visible());
2639    }
2640
2641    fn dispatch_frame_end_reveal() {
2642        TesseraRuntime::with_mut(|runtime| {
2643            let (tree, metadatas, focus_owner) =
2644                runtime.component_tree.accessibility_dispatch_context();
2645            dispatch_pending_focus_reveal_request(tree, metadatas, focus_owner, false);
2646        });
2647    }
2648
2649    #[test]
2650    fn pruned_focus_restoration_keeps_a_frame_scheduled_until_its_reveal_is_dispatched() {
2651        let _runtime_session = crate::RuntimeSession::new();
2652        let _runtime_scope = _runtime_session.enter();
2653        let probe = FrameEndProbe::default();
2654        let mut session =
2655            start_layout_test_session(layout_test(|| frame_end_focus_fixture(probe.clone())));
2656        pump_layout_test_session(&mut session, 0);
2657        let (visible, removed, fallback) = probe.focus.get().unwrap();
2658        removed.request_focus();
2659        assert!(removed.is_focused());
2660        dispatch_frame_end_reveal();
2661        pump_layout_test_session(&mut session, 1);
2662        assert!(!RuntimePendingWork::capture(false).requires_redraw());
2663
2664        visible.set(false);
2665        pump_layout_test_session(&mut session, 2);
2666        assert!(!fallback.is_focused());
2667        prune_persistent_focus_handles();
2668        assert!(!fallback.is_focused());
2669        assert!(RuntimePendingWork::capture(false).requires_redraw());
2670        pump_layout_test_session(&mut session, 3);
2671        prune_persistent_focus_handles();
2672        flush_pending_focus_callbacks();
2673        assert!(fallback.is_focused());
2674        assert_eq!(probe.events.get(), 1);
2675
2676        pump_layout_test_session(&mut session, 4);
2677        assert!(!crate::runtime::has_pending_build_invalidations());
2678        assert!(!crate::runtime::has_pending_layout_invalidations());
2679        assert!(!crate::runtime::has_pending_frame_nanos_receivers());
2680        assert!(!crate::runtime::has_pending_persistent_focus_prune());
2681        assert!(!crate::clipboard::has_pending_requests());
2682        assert!(!TesseraRuntime::with(|runtime| {
2683            runtime
2684                .component_tree
2685                .focus_owner()
2686                .has_pending_navigation()
2687        }));
2688        assert!(RuntimePendingWork::capture(false).requires_redraw());
2689        assert_eq!(probe.reveals.with(Vec::len), 1);
2690
2691        dispatch_frame_end_reveal();
2692        assert_eq!(
2693            probe.reveals.with(|rects| rects.clone()),
2694            vec![
2695                PxRect::new(crate::Px(0), crate::Px(0), crate::Px(64), crate::Px(24)),
2696                PxRect::new(crate::Px(0), crate::Px(0), crate::Px(64), crate::Px(24)),
2697            ]
2698        );
2699        assert!(fallback.is_focused());
2700        assert!(!RuntimePendingWork::capture(false).requires_redraw());
2701    }
2702
2703    #[derive(Clone, Default, PartialEq)]
2704    struct ColdProbe {
2705        child: crate::testing::TestProbe<Option<PxRect>>,
2706        writes: crate::testing::TestProbe<usize>,
2707        wakes: crate::testing::TestProbe<usize>,
2708        counts: crate::testing::TestProbe<(usize, usize)>,
2709    }
2710
2711    #[crate::tessera(crate)]
2712    fn cold_geometry_child(probe: crate::testing::TestProbe<Option<PxRect>>) {
2713        layout()
2714            .policy(FocusLeafLayout)
2715            .modifier(Modifier::new().on_globally_positioned(move |rect| probe.set(Some(rect))));
2716    }
2717
2718    #[crate::tessera(crate)]
2719    fn cold_geometry_fixture(probe: ColdProbe) {
2720        let observed = remember(|| None::<PxRect>);
2721        let output = probe.clone();
2722        layout()
2723            .modifier(Modifier::new().on_globally_positioned(move |rect| {
2724                if observed.get() != Some(rect) {
2725                    let before = output.wakes.get();
2726                    observed.set(Some(rect));
2727                    output.counts.set((before, output.wakes.get()));
2728                    output.writes.set(output.writes.get() + 1);
2729                }
2730            }))
2731            .child(move || {
2732                layout().policy(FocusLeafLayout);
2733                if observed.get().is_some() {
2734                    cold_geometry_child(probe.child.clone());
2735                }
2736            });
2737    }
2738
2739    #[test]
2740    fn first_geometry_publication_settles_its_conditional_child_before_the_frame_finishes() {
2741        let _runtime_session = crate::RuntimeSession::new();
2742        let _runtime_scope = _runtime_session.enter();
2743        struct WakerGuard;
2744        impl Drop for WakerGuard {
2745            fn drop(&mut self) {
2746                crate::runtime::clear_redraw_waker();
2747            }
2748        }
2749
2750        let probe = ColdProbe::default();
2751        let mut session =
2752            start_layout_test_session(layout_test(|| cold_geometry_fixture(probe.clone())));
2753        let _guard = WakerGuard;
2754        crate::runtime::install_redraw_waker(std::sync::Arc::new({
2755            let wakes = probe.wakes.clone();
2756            move || wakes.with_mut(|wakes| *wakes += 1)
2757        }));
2758        pump_layout_test_session(&mut session, 0);
2759        assert_eq!(probe.writes.get(), 1);
2760        let (before, after) = probe.counts.get();
2761        assert!(
2762            after > before,
2763            "the observer's state write must wake the runtime"
2764        );
2765        assert_eq!(
2766            probe.child.get(),
2767            Some(PxRect::new(
2768                crate::Px(0),
2769                crate::Px(0),
2770                crate::Px(64),
2771                crate::Px(24)
2772            )),
2773            "the geometry update must place the newly inserted child in the same frame"
2774        );
2775        assert_eq!(probe.writes.get(), 1);
2776        assert!(!RuntimePendingWork::capture(false).requires_redraw());
2777        let wakes = probe.wakes.get();
2778        pump_layout_test_session(&mut session, 16_666_667);
2779        assert_eq!(probe.writes.get(), 1);
2780        assert_eq!(
2781            probe.wakes.get(),
2782            wakes,
2783            "an unchanged idle layout must not wake again"
2784        );
2785        assert!(!RuntimePendingWork::capture(false).requires_redraw());
2786    }
2787
2788    #[test]
2789    fn web_canvas_size_tracks_css_density_and_fractional_dimensions() {
2790        let _runtime_session = crate::RuntimeSession::new();
2791        let _runtime_scope = _runtime_session.enter();
2792        assert_eq!(
2793            canvas_physical_size([390.0, 780.0], 1.0000000298023224),
2794            Some(PhysicalSize::new(390, 780))
2795        );
2796        assert_eq!(
2797            canvas_physical_size([877.333374, 871.333374], 1.5),
2798            Some(PhysicalSize::new(1316, 1307))
2799        );
2800        assert_eq!(
2801            canvas_physical_size([390.0, 780.0], 2.0),
2802            Some(PhysicalSize::new(780, 1560))
2803        );
2804        assert_eq!(canvas_physical_size([0.0, 780.0], 1.0), None);
2805        assert_eq!(canvas_physical_size([390.0, 780.0], f64::NAN), None);
2806    }
2807
2808    #[test]
2809    fn startup_window_size_prefers_recorded_size_over_live_size() {
2810        let _runtime_session = crate::RuntimeSession::new();
2811        let _runtime_scope = _runtime_session.enter();
2812        let recorded = PhysicalSize::new(800, 600);
2813        let live = PhysicalSize::new(1024, 768);
2814        assert_eq!(
2815            resolve_startup_window_size(Some(recorded), Some(live)),
2816            Some(recorded)
2817        );
2818    }
2819
2820    #[test]
2821    fn startup_window_size_falls_back_to_live_size_for_empty_recordings() {
2822        let _runtime_session = crate::RuntimeSession::new();
2823        let _runtime_scope = _runtime_session.enter();
2824        let live = PhysicalSize::new(1024, 768);
2825        assert_eq!(
2826            resolve_startup_window_size(Some(PhysicalSize::new(0, 0)), Some(live)),
2827            Some(live)
2828        );
2829        assert_eq!(
2830            resolve_startup_window_size(Some(PhysicalSize::new(0, 600)), Some(live)),
2831            Some(live)
2832        );
2833    }
2834
2835    #[test]
2836    fn startup_window_size_uses_live_size_without_recorded_events() {
2837        let _runtime_session = crate::RuntimeSession::new();
2838        let _runtime_scope = _runtime_session.enter();
2839        let live = PhysicalSize::new(1024, 768);
2840        assert_eq!(resolve_startup_window_size(None, Some(live)), Some(live));
2841    }
2842
2843    #[test]
2844    fn startup_window_size_is_none_when_every_reading_is_empty() {
2845        let _runtime_session = crate::RuntimeSession::new();
2846        let _runtime_scope = _runtime_session.enter();
2847        assert_eq!(
2848            resolve_startup_window_size(Some(PhysicalSize::new(0, 0)), None),
2849            None
2850        );
2851        assert_eq!(
2852            resolve_startup_window_size(None, Some(PhysicalSize::new(0, 0))),
2853            None
2854        );
2855    }
2856
2857    use crate::{ImeRequest, Px, PxPosition, px::PxSize};
2858
2859    fn positioned_request(
2860        position: PxPosition,
2861        size: PxSize,
2862        selection: std::ops::Range<usize>,
2863        composition: Option<std::ops::Range<usize>>,
2864    ) -> ImeRequest {
2865        let mut request = ImeRequest::new(size)
2866            .with_local_position(position)
2867            .with_text_state(winit::event::ImeTextState {
2868                text: "0123456789".into(),
2869                selection,
2870                composition,
2871            });
2872        request.position = Some(position);
2873        request
2874    }
2875
2876    #[test]
2877    fn ime_bridge_enables_and_updates_cursor_area_for_first_request() {
2878        let _runtime_session = crate::RuntimeSession::new();
2879        let _runtime_scope = _runtime_session.enter();
2880        let mut bridge = RendererImeBridgeState::default();
2881        let request = positioned_request(
2882            PxPosition::new(Px(12), Px(18)),
2883            PxSize::new(Px(7), Px(13)),
2884            2..6,
2885            Some(3..5),
2886        );
2887
2888        let update = bridge.update_request(Some(request.clone()));
2889
2890        assert_eq!(
2891            update,
2892            RendererImeBridgeUpdate {
2893                allowed: Some(true),
2894                cursor_area: Some((PxPosition::new(Px(12), Px(18)), PxSize::new(Px(7), Px(13)))),
2895                text_state: Some(request.text_state.clone()),
2896                options: Some(request.options),
2897                ..Default::default()
2898            }
2899        );
2900        assert_eq!(bridge.request(), Some(&request));
2901    }
2902
2903    #[test]
2904    fn ime_bridge_reuses_identical_request_without_window_updates() {
2905        let _runtime_session = crate::RuntimeSession::new();
2906        let _runtime_scope = _runtime_session.enter();
2907        let request = positioned_request(
2908            PxPosition::new(Px(12), Px(18)),
2909            PxSize::new(Px(7), Px(13)),
2910            2..6,
2911            Some(3..5),
2912        );
2913        let mut bridge = RendererImeBridgeState::default();
2914        bridge.update_request(Some(request.clone()));
2915
2916        let update = bridge.update_request(Some(request.clone()));
2917
2918        assert_eq!(update, RendererImeBridgeUpdate::default());
2919        assert_eq!(bridge.request(), Some(&request));
2920        let mut show = request.clone();
2921        show.show_keyboard = true;
2922        show.change_cause = winit::event::ImeChangeCause::InputMethod;
2923        let mut requests = crate::component_tree::WindowRequests {
2924            ime_request: Some(show),
2925            ..Default::default()
2926        };
2927        requests.settle(crate::component_tree::WindowRequests {
2928            ime_request: Some(request.clone()),
2929            ..Default::default()
2930        });
2931        let update = bridge.update_request(requests.ime_request);
2932        assert_eq!(
2933            update,
2934            RendererImeBridgeUpdate {
2935                show_keyboard: true,
2936                change_cause: winit::event::ImeChangeCause::InputMethod,
2937                ..Default::default()
2938            }
2939        );
2940        assert_eq!(
2941            bridge.update_request(Some(request)),
2942            RendererImeBridgeUpdate::default()
2943        );
2944    }
2945
2946    #[test]
2947    fn ime_bridge_preserves_selection_and_composition_changes_without_cursor_reapply() {
2948        let _runtime_session = crate::RuntimeSession::new();
2949        let _runtime_scope = _runtime_session.enter();
2950        let size = PxSize::new(Px(7), Px(13));
2951        let mut bridge = RendererImeBridgeState::default();
2952        bridge.update_request(Some(positioned_request(
2953            PxPosition::new(Px(12), Px(18)),
2954            size,
2955            2..6,
2956            Some(3..5),
2957        )));
2958        let next_request =
2959            positioned_request(PxPosition::new(Px(12), Px(18)), size, 4..8, Some(5..7));
2960
2961        let update = bridge.update_request(Some(next_request.clone()));
2962
2963        assert_eq!(update.allowed, None);
2964        assert_eq!(update.cursor_area, None);
2965        assert_eq!(update.text_state, Some(next_request.text_state.clone()));
2966        assert_eq!(bridge.request(), Some(&next_request));
2967        let mut moved = next_request.clone();
2968        moved.position = Some(PxPosition::new(Px(20), Px(30)));
2969        let update = bridge.update_request(Some(moved));
2970        assert_eq!(update.allowed, None);
2971        assert_eq!(update.text_state, None);
2972        assert_eq!(update.options, None);
2973        assert_eq!(
2974            update.cursor_area,
2975            Some((PxPosition::new(Px(20), Px(30)), size))
2976        );
2977        let mut another_client = next_request.clone();
2978        another_client.client_id = Some(42);
2979        let update = bridge.update_request(Some(another_client.clone()));
2980        assert_eq!(update.allowed, Some(true));
2981        assert_eq!(
2982            update.cursor_area,
2983            Some((PxPosition::new(Px(12), Px(18)), size))
2984        );
2985        another_client.text_state.text = "native edit".into();
2986        another_client.text_state.selection = 11..11;
2987        another_client.text_state.composition = Some(7..11);
2988        assert_eq!(
2989            bridge.receive_event(&winit::event::Ime::TextState(
2990                another_client.text_state.clone()
2991            )),
2992            Some(42)
2993        );
2994        assert_eq!(
2995            bridge.update_request(Some(another_client.clone())),
2996            RendererImeBridgeUpdate::default()
2997        );
2998        another_client.text_state.text = "transformed edit".into();
2999        let update = bridge.update_request(Some(another_client.clone()));
3000        assert_eq!(update.text_state, Some(another_client.text_state));
3001        assert_eq!(update.allowed, None);
3002    }
3003
3004    #[test]
3005    fn ime_bridge_disables_and_clears_snapshot_when_request_is_removed() {
3006        let _runtime_session = crate::RuntimeSession::new();
3007        let _runtime_scope = _runtime_session.enter();
3008        let request = positioned_request(
3009            PxPosition::new(Px(12), Px(18)),
3010            PxSize::new(Px(7), Px(13)),
3011            2..6,
3012            Some(3..5),
3013        );
3014        let mut bridge = RendererImeBridgeState::default();
3015        bridge.update_request(Some(request));
3016
3017        let update = bridge.update_request(None);
3018
3019        assert_eq!(
3020            update,
3021            RendererImeBridgeUpdate {
3022                allowed: Some(false),
3023                cursor_area: None,
3024                text_state: None,
3025                options: None,
3026                ..Default::default()
3027            }
3028        );
3029        assert_eq!(bridge.request(), None);
3030    }
3031}