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 composite;
6pub mod compute;
7pub mod core;
8pub mod drawer;
9pub mod external;
10
11use std::sync::{
12    Arc,
13    atomic::{AtomicBool, Ordering},
14};
15
16pub use core::{RenderCore, RenderResources};
17
18use accesskit::{self, TreeUpdate};
19use accesskit_winit::{Adapter as AccessKitAdapter, Event as AccessKitEvent};
20use parking_lot::RwLock;
21use tracing::{debug, error, instrument, warn};
22use winit::{
23    application::ApplicationHandler,
24    error::EventLoopError,
25    event::WindowEvent,
26    event_loop::{ActiveEventLoop, ControlFlow, EventLoop},
27    window::{ResizeDirection, Window, WindowId},
28};
29
30use crate::{
31    ImeRequest, ImeState, PxPosition,
32    build_tree::build_component_tree,
33    component_tree::{
34        LayoutFrameDiagnostics, WindowAction, WindowRequests, clear_layout_snapshots,
35    },
36    context::{reset_component_context_tracking, reset_context_read_dependencies},
37    cursor::{
38        CursorEvent, CursorEventContent, CursorState, GestureState, MOUSE_POINTER_ID,
39        PressKeyEventType,
40    },
41    dp::SCALE_FACTOR,
42    focus::{FocusDirection, flush_pending_focus_callbacks},
43    keyboard_state::KeyboardState,
44    pipeline_context::PipelineContext,
45    plugin::{PluginContext, PluginHost},
46    px::PxSize,
47    render_graph::{RenderGraph, RenderGraphExecution},
48    render_module::RenderModule,
49    runtime::{
50        TesseraRuntime, begin_frame_clock, clear_persistent_focus_handles, clear_redraw_waker,
51        has_pending_build_invalidations, has_pending_frame_nanos_receivers, install_redraw_waker,
52        reset_build_invalidations, reset_component_replay_tracking, reset_focus_read_dependencies,
53        reset_frame_clock, reset_layout_dirty_tracking, reset_render_slot_read_dependencies,
54        reset_state_read_dependencies, retain_persistent_focus_handles, take_layout_dirty_nodes,
55        tick_frame_nanos_receivers,
56    },
57    thread_utils,
58    time::Instant,
59};
60
61pub use crate::render_scene::{Command, DrawRegion, PaddingRect, SampleRegion};
62
63use self::core::RenderTimingBreakdown;
64
65pub use compute::{
66    ComputablePipeline, ComputeBatchItem, ComputePipelineRegistry, ErasedComputeBatchItem,
67};
68pub use drawer::{DrawCommand, DrawablePipeline, PipelineRegistry};
69pub use external::{ExternalTextureHandle, ExternalTextureRegistry};
70
71#[cfg(feature = "debug-dirty-overlay")]
72use crate::PxRect;
73#[cfg(feature = "debug-dirty-overlay")]
74use crate::build_tree::{BuildTreeMode, BuildTreeResult};
75
76#[cfg(feature = "profiling")]
77use crate::profiler::{
78    FrameMeta, Phase as ProfilerPhase, RedrawReason, RuntimeEventKind, RuntimeMeta,
79    ScopeGuard as ProfilerScopeGuard, WakeMeta, WakeSource, begin_frame as profiler_begin_frame,
80    end_frame as profiler_end_frame, submit_frame_meta, submit_runtime_meta, submit_wake_meta,
81};
82#[cfg(feature = "profiling")]
83use crate::runtime::frame_delta;
84#[cfg(feature = "profiling")]
85use std::collections::BTreeSet;
86#[cfg(feature = "profiling")]
87use std::path::PathBuf;
88
89#[cfg(target_family = "wasm")]
90use std::{cell::RefCell, rc::Rc};
91#[cfg(target_family = "wasm")]
92use wasm_bindgen_futures::spawn_local;
93#[cfg(target_family = "wasm")]
94use web_sys::HtmlCanvasElement;
95#[cfg(target_family = "wasm")]
96use web_sys::wasm_bindgen::JsCast;
97#[cfg(target_family = "wasm")]
98use winit::platform::web::EventLoopExtWebSys;
99
100#[cfg(target_os = "android")]
101use winit::platform::android::{
102    ActiveEventLoopExtAndroid, EventLoopBuilderExtAndroid, activity::AndroidApp,
103};
104
105#[derive(Debug)]
106enum RendererUserEvent {
107    AccessKit(AccessKitEvent),
108    RuntimeRedrawWake,
109    #[cfg(target_family = "wasm")]
110    WebInitReady(u64),
111}
112
113impl From<AccessKitEvent> for RendererUserEvent {
114    fn from(event: AccessKitEvent) -> Self {
115        Self::AccessKit(event)
116    }
117}
118
119type RenderComputationOutput = (
120    RenderGraph,
121    WindowRequests,
122    std::time::Duration,
123    LayoutFrameDiagnostics,
124    std::time::Duration,
125    Option<FocusDirection>,
126    bool,
127);
128
129#[derive(Clone, Copy, Debug, Default)]
130struct RuntimePendingWork {
131    invalidation_pending: bool,
132    frame_receiver_pending: bool,
133}
134
135impl RuntimePendingWork {
136    fn requires_redraw(self) -> bool {
137        self.invalidation_pending || self.frame_receiver_pending
138    }
139
140    #[cfg(feature = "profiling")]
141    fn redraw_reasons(self) -> Vec<RedrawReason> {
142        let mut reasons = Vec::new();
143        if self.invalidation_pending {
144            reasons.push(RedrawReason::RuntimeInvalidation);
145        }
146        if self.frame_receiver_pending {
147            reasons.push(RedrawReason::RuntimeFrameAwaiter);
148        }
149        reasons
150    }
151}
152
153#[cfg(feature = "profiling")]
154fn resolve_profiler_output_path(config: &TesseraConfig) -> PathBuf {
155    if let Ok(path) = std::env::var("TESSERA_PROFILING_OUTPUT")
156        && !path.trim().is_empty()
157    {
158        return PathBuf::from(path);
159    }
160    if let Some(path) = option_env!("TESSERA_PROFILING_OUTPUT")
161        && !path.trim().is_empty()
162    {
163        return PathBuf::from(path);
164    }
165    config.profiler_output_path.clone()
166}
167
168/// Window creation options for desktop platforms.
169#[derive(Debug, Clone)]
170pub struct WindowConfig {
171    /// Whether to show the system window decorations (title bar and borders).
172    pub decorations: bool,
173    /// Whether the window supports transparency.
174    pub transparent: bool,
175    /// Whether the window is resizable.
176    pub resizable: bool,
177}
178
179impl Default for WindowConfig {
180    fn default() -> Self {
181        Self {
182            decorations: true,
183            transparent: true,
184            resizable: true,
185        }
186    }
187}
188
189/// Web host configuration for browser platforms.
190#[derive(Debug, Clone, Default)]
191pub struct WebConfig {
192    /// An optional canvas element id to mount into before falling back to the
193    /// default body append behavior.
194    pub canvas_id: Option<String>,
195}
196
197impl WebConfig {
198    /// Returns a config that looks up the canvas with the provided id.
199    pub fn with_canvas_id(mut self, canvas_id: impl Into<String>) -> Self {
200        self.canvas_id = Some(canvas_id.into());
201        self
202    }
203}
204
205/// Configuration for the Tessera runtime and renderer.
206///
207/// This struct allows you to customize various aspects of the renderer's
208/// behavior, including anti-aliasing settings and other rendering parameters.
209///
210/// # Examples
211///
212/// ```
213/// use tessera_ui::renderer::TesseraConfig;
214///
215/// // Default configuration (4x MSAA)
216/// let config = TesseraConfig::default();
217///
218/// // Custom configuration with 8x MSAA
219/// let config = TesseraConfig {
220///     sample_count: 8,
221///     ..Default::default()
222/// };
223///
224/// // Disable MSAA for better performance
225/// let config = TesseraConfig {
226///     sample_count: 1,
227///     ..Default::default()
228/// };
229/// ```
230#[derive(Debug, Clone)]
231pub struct TesseraConfig {
232    /// The number of samples to use for Multi-Sample Anti-Aliasing (MSAA).
233    ///
234    /// MSAA helps reduce aliasing artifacts (jagged edges) in rendered graphics
235    /// by sampling multiple points per pixel and averaging the results.
236    ///
237    /// ## Supported Values
238    /// - `1`: Disables MSAA (best performance, lower quality)
239    /// - `4`: 4x MSAA (balanced quality/performance)
240    /// - `8`: 8x MSAA (high quality, higher performance cost)
241    ///
242    /// ## Notes
243    /// - Higher sample counts provide better visual quality but consume more
244    ///   GPU resources
245    /// - The GPU must support the chosen sample count; unsupported values may
246    ///   cause errors
247    /// - Mobile devices may have limited support for higher sample counts
248    /// - Consider using lower values on resource-constrained devices
249    pub sample_count: u32,
250    /// The title of the application window.
251    /// Defaults to "Tessera" if not specified.
252    pub window_title: String,
253    /// Window configuration (desktop only).
254    pub window: WindowConfig,
255    /// Web host configuration for browser platforms.
256    pub web: WebConfig,
257    /// Path to write profiler output when `profiling` is enabled.
258    #[cfg(feature = "profiling")]
259    pub profiler_output_path: PathBuf,
260}
261
262impl Default for TesseraConfig {
263    /// Creates a default configuration without MSAA and "Tessera" as the window
264    /// title.
265    fn default() -> Self {
266        Self {
267            sample_count: 1,
268            window_title: "Tessera".to_string(),
269            window: WindowConfig::default(),
270            web: WebConfig::default(),
271            #[cfg(feature = "profiling")]
272            profiler_output_path: PathBuf::from("tessera-profiler.jsonl"),
273        }
274    }
275}
276
277/// # Renderer
278///
279/// The main renderer struct that manages the application lifecycle and
280/// rendering.
281///
282/// The `Renderer` is the core component of the Tessera UI framework,
283/// responsible for:
284///
285/// - Managing the application window and WGPU context
286/// - Handling input events (mouse, touch, keyboard, IME)
287/// - Coordinating the component tree building and rendering process
288/// - Managing rendering pipelines and resources
289///
290/// ## Type Parameters
291///
292/// - `F`: The entry point function type that defines your UI. Must implement
293///   `Fn()`.
294///
295/// ## Lifecycle
296///
297/// The renderer follows this lifecycle:
298/// 1. **Initialization**: Create window, initialize WGPU context, register
299///    pipelines
300/// 2. **Event Loop**: Handle window events, input events, and render requests
301/// 3. **Frame Rendering**: Build component tree → Compute draw commands →
302///    Render to surface
303/// 4. **Cleanup**: Automatic cleanup when the application exits
304///
305/// ## Thread Safety
306///
307/// The renderer runs on the main thread and coordinates with other threads for:
308/// - Component tree building (potentially parallelized)
309/// - Resource management
310/// - Event processing
311///
312/// ## Usage
313///
314/// ## Basic Usage
315///
316/// It's suggested to use `cargo-tessera` to create your project from templates
317/// which include all necessary setup. However, here's a minimal example of how
318/// to use the renderer through the [`Renderer::run`] method:
319///
320/// ```no_run
321/// use tessera_ui::{PipelineContext, RenderModule, Renderer};
322///
323/// #[derive(Debug)]
324/// struct DemoModule;
325///
326/// impl RenderModule for DemoModule {
327///     fn register_pipelines(&self, _context: &mut PipelineContext<'_>) {}
328/// }
329///
330/// // Define your UI entry point
331/// fn my_app() {
332///     // Your UI components go here
333/// }
334///
335/// // Run the application
336/// Renderer::run(my_app, vec![Box::new(DemoModule)]).unwrap();
337/// ```
338///
339/// ### Android Usage
340///
341/// On android, [`Renderer::run`] requires an additional `AndroidApp` parameter
342/// from app context or `android_main` function.
343///
344/// ## Configuration
345///
346/// You can customize the renderer behavior by passing [`TesseraConfig`] when
347/// using [`Renderer::run_with_config`]. instead of [`Renderer::run`].
348///
349/// ```no_run
350/// use tessera_ui::{PipelineContext, RenderModule, Renderer, renderer::TesseraConfig};
351///
352/// # fn foo() -> Result<(), Box<dyn std::error::Error>> {
353/// #[derive(Debug)]
354/// struct DemoModule;
355///
356/// impl RenderModule for DemoModule {
357///     fn register_pipelines(&self, _context: &mut PipelineContext<'_>) {}
358/// }
359///
360/// let config = TesseraConfig {
361///     sample_count: 8,                            // 8x MSAA
362///     window_title: "My Tessera App".to_string(), // Custom window title
363///     ..Default::default()
364/// };
365///
366/// Renderer::run_with_config(|| { /* my_app */ }, vec![Box::new(DemoModule)], config)?;
367/// # Ok(())
368/// # }
369/// ```
370///
371/// ## Performance Monitoring
372///
373/// The renderer includes built-in performance monitoring that logs frame
374/// statistics when performance drops below 60 FPS.
375pub struct Renderer<F: Fn()> {
376    /// The WGPU application context, initialized after window creation
377    app: Option<RenderCore>,
378    /// The entry point function that defines the root of your UI component tree
379    entry_point: F,
380    /// Tracks cursor/mouse position and button states
381    cursor_state: CursorState,
382    /// Tracks keyboard key states and events
383    keyboard_state: KeyboardState,
384    /// Tracks Input Method Editor (IME) state for international text input
385    ime_state: ImeState,
386    /// Tracks the renderer-side IME bridge snapshot and platform lifecycle.
387    ime_bridge_state: RendererImeBridgeState,
388    /// Render modules providing pipelines.
389    modules: Vec<Box<dyn RenderModule>>,
390    /// Lifecycle hooks for registered plugins.
391    plugins: PluginHost,
392    /// Configuration settings for the renderer
393    config: TesseraConfig,
394    /// AccessKit adapter for accessibility support
395    accessibility_adapter: Option<AccessKitAdapter>,
396    /// Event loop proxy for posting user events (accessibility/runtime wakeups)
397    event_loop_proxy: Option<winit::event_loop::EventLoopProxy<RendererUserEvent>>,
398    /// Incrementing frame index for profiling and debugging.
399    frame_index: u64,
400    /// Global redraw gate. While `true`, redraw requests are coalesced until
401    /// the pending `RedrawRequested` event is consumed.
402    redraw_request_pending: Arc<AtomicBool>,
403    /// Whether a window close was requested during the last frame.
404    pending_close_requested: bool,
405    /// Tracks whether a native border resize drag is currently in progress.
406    /// While active, cursor input is withheld from the component tree to avoid
407    /// accidental UI interaction during system resize.
408    resize_in_progress: bool,
409    #[cfg(target_family = "wasm")]
410    /// Render cores that completed asynchronous initialization on the web
411    /// event loop thread and are waiting to be installed into the renderer.
412    pending_web_inits: Rc<RefCell<Vec<(u64, RenderCore)>>>,
413    #[cfg(target_family = "wasm")]
414    /// Monotonic token used to discard stale async initialization completions
415    /// after suspend/resume cycles.
416    web_init_epoch: u64,
417    #[cfg(target_family = "wasm")]
418    /// Whether a web-side render core initialization task is currently
419    /// running.
420    web_init_in_progress: bool,
421    #[cfg(feature = "profiling")]
422    /// Aggregated redraw reasons that will be attached to the next rendered
423    /// frame.
424    pending_redraw_reasons: BTreeSet<RedrawReason>,
425    #[cfg(target_os = "android")]
426    /// Android-specific state tracking whether the soft keyboard is currently
427    /// open
428    android_ime_opened: bool,
429}
430
431impl<F: Fn()> Renderer<F> {
432    #[cfg(target_family = "wasm")]
433    fn resolve_web_canvas(config: &TesseraConfig) -> Option<HtmlCanvasElement> {
434        let canvas_id = config.web.canvas_id.as_deref()?;
435        let window = web_sys::window()?;
436        let document = window.document()?;
437        let element = document.get_element_by_id(canvas_id)?;
438        element.dyn_into::<HtmlCanvasElement>().ok()
439    }
440
441    /// Runs the Tessera application with default configuration on desktop
442    /// platforms.
443    ///
444    /// This is the most convenient way to start a Tessera application on
445    /// Windows, Linux, or macOS. It uses the default [`TesseraConfig`]
446    /// settings (4x MSAA).
447    ///
448    /// # Parameters
449    ///
450    /// - `entry_point`: A function that defines your UI. This function will be
451    ///   called every frame to build the component tree. It should contain your
452    ///   root UI components.
453    /// - `modules`: Render modules that register pipelines.
454    ///
455    /// # Returns
456    ///
457    /// Returns `Ok(())` when the application exits normally, or an
458    /// `EventLoopError` if the event loop fails to start or encounters a
459    /// critical error.
460    ///
461    /// # Examples
462    ///
463    /// ```no_run
464    /// use tessera_ui::{PipelineContext, RenderModule, Renderer};
465    ///
466    /// #[derive(Debug)]
467    /// struct DemoModule;
468    ///
469    /// impl RenderModule for DemoModule {
470    ///     fn register_pipelines(&self, _context: &mut PipelineContext<'_>) {}
471    /// }
472    ///
473    /// fn my_ui() {
474    ///     // Your UI components go here
475    /// }
476    ///
477    /// fn main() -> Result<(), Box<dyn std::error::Error>> {
478    ///     Renderer::run(my_ui, vec![Box::new(DemoModule)])?;
479    ///     Ok(())
480    /// }
481    /// ```
482    #[cfg(all(not(target_os = "android"), not(target_family = "wasm")))]
483    #[tracing::instrument(level = "info", skip(entry_point, modules))]
484    pub fn run(entry_point: F, modules: Vec<Box<dyn RenderModule>>) -> Result<(), EventLoopError> {
485        Self::run_with_config(entry_point, modules, Default::default())
486    }
487
488    /// Runs the Tessera application with custom configuration on desktop
489    /// platforms.
490    ///
491    /// This method allows you to customize the renderer behavior through
492    /// [`TesseraConfig`]. Use this when you need to adjust settings like
493    /// MSAA sample count or other rendering parameters.
494    ///
495    /// # Parameters
496    ///
497    /// - `entry_point`: A function that defines your UI
498    /// - `modules`: Render modules that register pipelines
499    /// - `config`: Custom configuration for the renderer
500    ///
501    /// # Returns
502    ///
503    /// Returns `Ok(())` when the application exits normally, or an
504    /// `EventLoopError` if the event loop fails to start.
505    ///
506    /// # Examples
507    ///
508    /// ```no_run
509    /// use tessera_ui::{PipelineContext, RenderModule, Renderer, renderer::TesseraConfig};
510    ///
511    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
512    /// #[derive(Debug)]
513    /// struct DemoModule;
514    ///
515    /// impl RenderModule for DemoModule {
516    ///     fn register_pipelines(&self, _context: &mut PipelineContext<'_>) {}
517    /// }
518    ///
519    /// let config = TesseraConfig {
520    ///     sample_count: 8, // 8x MSAA for higher quality
521    ///     ..Default::default()
522    /// };
523    ///
524    /// Renderer::run_with_config(|| { /* my_ui */ }, vec![Box::new(DemoModule)], config)?;
525    /// # Ok(())
526    /// # }
527    /// ```
528    #[tracing::instrument(level = "info", skip(entry_point, modules))]
529    #[cfg(all(not(target_os = "android"), not(target_family = "wasm")))]
530    pub fn run_with_config(
531        entry_point: F,
532        modules: Vec<Box<dyn RenderModule>>,
533        config: TesseraConfig,
534    ) -> Result<(), EventLoopError> {
535        let event_loop = EventLoop::<RendererUserEvent>::with_user_event().build()?;
536        let event_loop_proxy = event_loop.create_proxy();
537        let app = None;
538        let cursor_state = CursorState::default();
539        let keyboard_state = KeyboardState::default();
540        let ime_state = ImeState::default();
541        let ime_bridge_state = RendererImeBridgeState::default();
542        #[cfg(feature = "profiling")]
543        crate::profiler::set_output_path(resolve_profiler_output_path(&config));
544        let mut renderer = Self {
545            app,
546            entry_point,
547            cursor_state,
548            keyboard_state,
549            modules,
550            plugins: PluginHost::new(),
551            ime_state,
552            ime_bridge_state,
553            config,
554            accessibility_adapter: None,
555            event_loop_proxy: Some(event_loop_proxy),
556            frame_index: 0,
557            redraw_request_pending: Arc::new(AtomicBool::new(false)),
558            pending_close_requested: false,
559            resize_in_progress: false,
560            #[cfg(target_family = "wasm")]
561            pending_web_inits: Rc::new(RefCell::new(Vec::new())),
562            #[cfg(target_family = "wasm")]
563            web_init_epoch: 0,
564            #[cfg(target_family = "wasm")]
565            web_init_in_progress: false,
566            #[cfg(feature = "profiling")]
567            pending_redraw_reasons: BTreeSet::new(),
568        };
569        thread_utils::set_thread_name("TesseraMain");
570        event_loop.run_app(&mut renderer)
571    }
572
573    /// Runs the Tessera application with default configuration on web
574    /// platforms.
575    #[cfg(target_family = "wasm")]
576    #[tracing::instrument(level = "info", skip(entry_point, modules))]
577    pub fn run(entry_point: F, modules: Vec<Box<dyn RenderModule>>) -> Result<(), EventLoopError>
578    where
579        F: 'static,
580    {
581        Self::run_web_with_config(entry_point, modules, Default::default())
582    }
583
584    /// Runs the Tessera application with custom configuration on web
585    /// platforms.
586    #[cfg(target_family = "wasm")]
587    #[tracing::instrument(level = "info", skip(entry_point, modules))]
588    pub fn run_web_with_config(
589        entry_point: F,
590        modules: Vec<Box<dyn RenderModule>>,
591        config: TesseraConfig,
592    ) -> Result<(), EventLoopError>
593    where
594        F: 'static,
595    {
596        let event_loop = EventLoop::<RendererUserEvent>::with_user_event().build()?;
597        let event_loop_proxy = event_loop.create_proxy();
598        let app = None;
599        let cursor_state = CursorState::default();
600        let keyboard_state = KeyboardState::default();
601        let ime_state = ImeState::default();
602        let ime_bridge_state = RendererImeBridgeState::default();
603        #[cfg(feature = "profiling")]
604        crate::profiler::set_output_path(resolve_profiler_output_path(&config));
605        let renderer = Self {
606            app,
607            entry_point,
608            cursor_state,
609            keyboard_state,
610            modules,
611            plugins: PluginHost::new(),
612            ime_state,
613            ime_bridge_state,
614            config,
615            accessibility_adapter: None,
616            event_loop_proxy: Some(event_loop_proxy),
617            frame_index: 0,
618            redraw_request_pending: Arc::new(AtomicBool::new(false)),
619            pending_close_requested: false,
620            resize_in_progress: false,
621            pending_web_inits: Rc::new(RefCell::new(Vec::new())),
622            web_init_epoch: 0,
623            web_init_in_progress: false,
624            #[cfg(feature = "profiling")]
625            pending_redraw_reasons: BTreeSet::new(),
626        };
627        thread_utils::set_thread_name("TesseraMain");
628        event_loop.spawn_app(renderer);
629        Ok(())
630    }
631
632    /// Runs the Tessera application with default configuration on Android.
633    ///
634    /// This method is specifically for Android applications and requires an
635    /// `AndroidApp` instance that is typically provided by the
636    /// `android_main` function.
637    ///
638    /// # Parameters
639    ///
640    /// - `entry_point`: A function that defines your UI
641    /// - `modules`: Render modules that register pipelines
642    /// - `android_app`: The Android application context
643    ///
644    /// # Returns
645    ///
646    /// Returns `Ok(())` when the application exits normally, or an
647    /// `EventLoopError` if the event loop fails to start.
648    ///
649    /// # Examples
650    ///
651    /// ```no_run
652    /// use tessera_ui::Renderer;
653    /// use winit::platform::android::activity::AndroidApp;
654    ///
655    /// fn my_ui() {}
656    ///
657    /// #[derive(Debug)]
658    /// struct DemoModule;
659    ///
660    /// impl tessera_ui::RenderModule for DemoModule {
661    ///     fn register_pipelines(&self, _context: &mut tessera_ui::PipelineContext<'_>) {}
662    /// }
663    ///
664    /// #[unsafe(no_mangle)]
665    /// fn android_main(android_app: AndroidApp) {
666    ///     Renderer::run(my_ui, vec![Box::new(DemoModule)], android_app).unwrap();
667    /// }
668    /// ```
669    #[cfg(target_os = "android")]
670    #[tracing::instrument(level = "info", skip(entry_point, modules, android_app))]
671    pub fn run(
672        entry_point: F,
673        modules: Vec<Box<dyn RenderModule>>,
674        android_app: AndroidApp,
675    ) -> Result<(), EventLoopError> {
676        Self::run_with_config(entry_point, modules, android_app, Default::default())
677    }
678
679    /// Runs the Tessera application with custom configuration on Android.
680    ///
681    /// This method allows you to customize the renderer behavior on Android
682    /// through [`TesseraConfig`].
683    ///
684    /// # Parameters
685    ///
686    /// - `entry_point`: A function that defines your UI
687    /// - `modules`: Render modules that register pipelines
688    /// - `android_app`: The Android application context
689    /// - `config`: Custom configuration for the renderer
690    ///
691    /// # Returns
692    ///
693    /// Returns `Ok(())` when the application exits normally, or an
694    /// `EventLoopError` if the event loop fails to start.
695    ///
696    /// # Examples
697    ///
698    /// ```no_run
699    /// use tessera_ui::{Renderer, renderer::TesseraConfig};
700    /// use winit::platform::android::activity::AndroidApp;
701    ///
702    /// fn my_ui() {}
703    ///
704    /// #[derive(Debug)]
705    /// struct DemoModule;
706    ///
707    /// impl tessera_ui::RenderModule for DemoModule {
708    ///     fn register_pipelines(&self, _context: &mut tessera_ui::PipelineContext<'_>) {}
709    /// }
710    ///
711    /// #[unsafe(no_mangle)]
712    /// fn android_main(android_app: AndroidApp) {
713    ///     let config = TesseraConfig {
714    ///         sample_count: 2, // Lower MSAA for mobile performance
715    ///     };
716    ///
717    ///     Renderer::run_with_config(my_ui, vec![Box::new(DemoModule)], android_app, config).unwrap();
718    /// }
719    /// ```
720    #[cfg(target_os = "android")]
721    #[tracing::instrument(level = "info", skip(entry_point, modules, android_app))]
722    pub fn run_with_config(
723        entry_point: F,
724        modules: Vec<Box<dyn RenderModule>>,
725        android_app: AndroidApp,
726        config: TesseraConfig,
727    ) -> Result<(), EventLoopError> {
728        let event_loop = EventLoop::<RendererUserEvent>::with_user_event()
729            .with_android_app(android_app.clone())
730            .build()
731            .unwrap();
732        let event_loop_proxy = event_loop.create_proxy();
733        let app = None;
734        let cursor_state = CursorState::default();
735        let keyboard_state = KeyboardState::default();
736        let ime_state = ImeState::default();
737        let ime_bridge_state = RendererImeBridgeState::default();
738        #[cfg(feature = "profiling")]
739        crate::profiler::set_output_path(resolve_profiler_output_path(&config));
740        let mut renderer = Self {
741            app,
742            entry_point,
743            cursor_state,
744            keyboard_state,
745            modules,
746            plugins: PluginHost::new(),
747            ime_state,
748            ime_bridge_state,
749            android_ime_opened: false,
750            config,
751            accessibility_adapter: None,
752            event_loop_proxy: Some(event_loop_proxy),
753            frame_index: 0,
754            redraw_request_pending: Arc::new(AtomicBool::new(false)),
755            pending_close_requested: false,
756            resize_in_progress: false,
757            #[cfg(feature = "profiling")]
758            pending_redraw_reasons: BTreeSet::new(),
759        };
760        thread_utils::set_thread_name("TesseraMain");
761        event_loop.run_app(&mut renderer)
762    }
763}
764
765// Helper struct to group render-frame arguments and reduce parameter count.
766// Kept private to this module.
767struct RenderFrameArgs<'a> {
768    pub cursor_state: &'a mut CursorState,
769    pub keyboard_state: &'a mut KeyboardState,
770    pub ime_state: &'a mut ImeState,
771    pub ime_bridge_state: &'a mut RendererImeBridgeState,
772    #[cfg(target_os = "android")]
773    pub android_ime_opened: &'a mut bool,
774    pub app: &'a mut RenderCore,
775    #[cfg(target_os = "android")]
776    pub event_loop: &'a ActiveEventLoop,
777}
778
779struct RenderFrameContext<'a, F: Fn()> {
780    entry_point: &'a F,
781    args: &'a mut RenderFrameArgs<'a>,
782    accessibility_enabled: bool,
783    decorations: bool,
784    window_label: &'a str,
785    frame_idx: u64,
786    #[cfg(feature = "profiling")]
787    redraw_reasons: Vec<RedrawReason>,
788}
789
790struct RenderFrameOutcome {
791    accessibility_update: Option<TreeUpdate>,
792    window_action: Option<WindowAction>,
793    runtime_pending_work: RuntimePendingWork,
794    #[cfg(feature = "debug-dirty-overlay")]
795    overlay_clear_pending: bool,
796}
797
798#[derive(Clone, Debug, Default, PartialEq, Eq)]
799struct RendererImeBridgeState {
800    current_request: Option<ImeRequest>,
801    ime_allowed: bool,
802}
803
804#[derive(Clone, Debug, Default, PartialEq, Eq)]
805struct RendererImeBridgeUpdate {
806    allowed: Option<bool>,
807    cursor_area: Option<(PxPosition, PxSize)>,
808    request: Option<ImeRequest>,
809    snapshot_changed: bool,
810}
811
812impl RendererImeBridgeState {
813    #[cfg(test)]
814    fn request(&self) -> Option<&ImeRequest> {
815        self.current_request.as_ref()
816    }
817
818    fn reset(&mut self) {
819        *self = Self::default();
820    }
821
822    fn update_request(&mut self, request: Option<ImeRequest>) -> RendererImeBridgeUpdate {
823        let snapshot_changed = self.current_request != request;
824        let allowed = (self.ime_allowed != request.is_some()).then_some(request.is_some());
825        let cursor_area = request.as_ref().and_then(|next_request| {
826            let previous_area = self.current_request.as_ref().and_then(|previous_request| {
827                previous_request
828                    .position
829                    .map(|position| (position, previous_request.size))
830            });
831            let next_area = next_request
832                .position
833                .map(|position| (position, next_request.size));
834            (previous_area != next_area).then_some(next_area).flatten()
835        });
836
837        self.ime_allowed = request.is_some();
838        self.current_request = request.clone();
839
840        RendererImeBridgeUpdate {
841            allowed,
842            cursor_area,
843            request,
844            snapshot_changed,
845        }
846    }
847}
848
849impl<F: Fn()> Renderer<F> {
850    const RESIZE_EDGE_THRESHOLD: f64 = 8.0;
851    const MAX_FOCUS_BEYOND_BOUNDS_RETRIES: usize = 8;
852
853    #[cfg(target_os = "windows")]
854    fn update_native_window_shape(&self, window: &Window) {
855        use winit::platform::windows::{CornerPreference, WindowExtWindows};
856
857        if self.config.window.decorations {
858            window.set_corner_preference(CornerPreference::Default);
859            return;
860        }
861
862        let preference = if window.is_maximized() || window.fullscreen().is_some() {
863            CornerPreference::DoNotRound
864        } else {
865            CornerPreference::Round
866        };
867        window.set_corner_preference(preference);
868    }
869
870    #[cfg(target_os = "macos")]
871    fn update_native_window_shape(&self, window: &Window) {
872        use objc2::rc::Retained;
873        use objc2_app_kit::NSView;
874        use winit::raw_window_handle::{HasWindowHandle, RawWindowHandle};
875
876        let raw_window_handle = match window.window_handle() {
877            Ok(handle) => handle.as_raw(),
878            Err(err) => {
879                warn!("Failed to fetch native window handle: {}", err);
880                return;
881            }
882        };
883        let RawWindowHandle::AppKit(appkit) = raw_window_handle else {
884            return;
885        };
886
887        // SAFETY: The pointer comes from winit's window handle and is valid for
888        // the lifetime of this call on the main thread.
889        let Some(ns_view) = (unsafe { Retained::<NSView>::retain(appkit.ns_view.as_ptr().cast()) })
890        else {
891            warn!("Failed to retain NSView from raw window handle");
892            return;
893        };
894
895        let radius_px = if self.config.window.decorations
896            || window.is_maximized()
897            || window.fullscreen().is_some()
898        {
899            0.0
900        } else {
901            8.0 * window.scale_factor()
902        };
903
904        ns_view.setWantsLayer(true);
905        let layer = if let Some(layer) = ns_view.layer() {
906            layer
907        } else {
908            let layer = ns_view.makeBackingLayer();
909            ns_view.setLayer(Some(&layer));
910            layer
911        };
912        layer.setCornerRadius(radius_px as _);
913        layer.setMasksToBounds(radius_px > 0.0);
914    }
915
916    #[cfg(not(any(target_os = "windows", target_os = "macos")))]
917    fn update_native_window_shape(&self, _window: &Window) {}
918
919    fn should_set_cursor_pos(
920        cursor_position: Option<crate::PxPosition>,
921        window_width: f64,
922        window_height: f64,
923        edge_threshold: f64,
924    ) -> bool {
925        if let Some(pos) = cursor_position {
926            let x = pos.x.0 as f64;
927            let y = pos.y.0 as f64;
928            x > edge_threshold
929                && x < window_width - edge_threshold
930                && y > edge_threshold
931                && y < window_height - edge_threshold
932        } else {
933            false
934        }
935    }
936
937    fn cursor_resize_direction(
938        cursor_position: Option<crate::PxPosition>,
939        window_size: winit::dpi::PhysicalSize<u32>,
940        edge_threshold: f64,
941    ) -> Option<ResizeDirection> {
942        let position = cursor_position?;
943        let x = position.x.0 as f64;
944        let y = position.y.0 as f64;
945        let width = window_size.width as f64;
946        let height = window_size.height as f64;
947
948        let near_left = x <= edge_threshold;
949        let near_right = x >= width - edge_threshold;
950        let near_top = y <= edge_threshold;
951        let near_bottom = y >= height - edge_threshold;
952
953        match (near_left, near_right, near_top, near_bottom) {
954            (true, _, true, _) => Some(ResizeDirection::NorthWest),
955            (_, true, true, _) => Some(ResizeDirection::NorthEast),
956            (true, _, _, true) => Some(ResizeDirection::SouthWest),
957            (_, true, _, true) => Some(ResizeDirection::SouthEast),
958            (true, _, _, _) => Some(ResizeDirection::West),
959            (_, true, _, _) => Some(ResizeDirection::East),
960            (_, _, true, _) => Some(ResizeDirection::North),
961            (_, _, _, true) => Some(ResizeDirection::South),
962            _ => None,
963        }
964    }
965
966    fn cursor_icon_for_resize(direction: ResizeDirection) -> winit::window::CursorIcon {
967        match direction {
968            ResizeDirection::East | ResizeDirection::West => winit::window::CursorIcon::EwResize,
969            ResizeDirection::North | ResizeDirection::South => winit::window::CursorIcon::NsResize,
970            ResizeDirection::NorthEast | ResizeDirection::SouthWest => {
971                winit::window::CursorIcon::NeswResize
972            }
973            ResizeDirection::NorthWest | ResizeDirection::SouthEast => {
974                winit::window::CursorIcon::NwseResize
975            }
976        }
977    }
978
979    /// Executes a single frame rendering cycle.
980    ///
981    /// This is the core rendering method that orchestrates the entire frame
982    /// rendering process. It follows a three-phase approach:
983    ///
984    /// 1. **Component Tree Building**: Calls the entry point function to build
985    ///    the UI component tree
986    /// 2. **Draw Command Computation**: Processes the component tree to
987    ///    generate rendering commands
988    /// 3. **Surface Rendering**: Executes the commands to render the final
989    ///    frame
990    ///
991    /// ## Performance Monitoring
992    ///
993    /// This method includes built-in performance monitoring that logs detailed
994    /// timing information when frame rates drop below 60 FPS, helping
995    /// identify performance bottlenecks.
996    ///
997    /// ## Parameters
998    ///
999    /// - `entry_point`: The UI entry point function to build the component tree
1000    /// - `cursor_state`: Mutable reference to cursor/mouse state for event
1001    ///   processing
1002    /// - `keyboard_state`: Mutable reference to keyboard state for event
1003    ///   processing
1004    /// - `ime_state`: Mutable reference to IME state for text input processing
1005    /// - `android_ime_opened`: (Android only) Tracks soft keyboard state
1006    /// - `app`: Mutable reference to the WGPU application context
1007    /// - `event_loop`: (Android only) Event loop for IME management
1008    ///
1009    /// ## Frame Timing Breakdown
1010    ///
1011    /// - **Build Tree Cost**: Time spent building the component tree
1012    /// - **Draw Commands Cost**: Time spent computing rendering commands
1013    /// - **Render Cost**: Time spent executing GPU rendering commands
1014    ///
1015    /// ## Thread Safety
1016    ///
1017    /// This method runs on the main thread but coordinates with other threads
1018    /// for component tree processing and resource management.
1019    fn log_frame_stats(
1020        build_tree_cost: std::time::Duration,
1021        draw_cost: std::time::Duration,
1022        render_cost: std::time::Duration,
1023        render_breakdown: Option<RenderTimingBreakdown>,
1024    ) {
1025        let total = build_tree_cost + draw_cost + render_cost;
1026        let fps = 1.0 / total.as_secs_f32();
1027        if fps < 60.0 {
1028            if let Some(breakdown) = render_breakdown {
1029                warn!(
1030                    "Jank detected! Frame statistics:
1031Build tree cost: {:?}
1032Draw commands cost: {:?}
1033Render cost: {:?}
1034Total frame cost: {:?}
1035Fps: {:.2}
1036Render breakdown:
1037Acquire: {:?}
1038Build passes: {:?}
1039Encode: {:?}
1040Submit: {:?}
1041Present: {:?}
1042Render total (core): {:?}
1043",
1044                    build_tree_cost,
1045                    draw_cost,
1046                    render_cost,
1047                    total,
1048                    1.0 / total.as_secs_f32(),
1049                    breakdown.acquire,
1050                    breakdown.build_passes,
1051                    breakdown.encode,
1052                    breakdown.submit,
1053                    breakdown.present,
1054                    breakdown.total,
1055                );
1056            } else {
1057                warn!(
1058                    "Jank detected! Frame statistics:
1059Build tree cost: {:?}
1060Draw commands cost: {:?}
1061Render cost: {:?}
1062Total frame cost: {:?}
1063Fps: {:.2}
1064",
1065                    build_tree_cost,
1066                    draw_cost,
1067                    render_cost,
1068                    total,
1069                    1.0 / total.as_secs_f32()
1070                );
1071            }
1072        }
1073    }
1074
1075    #[instrument(level = "debug", skip(args))]
1076    fn compute_draw_commands<'a>(
1077        args: &mut RenderFrameArgs<'a>,
1078        screen_size: PxSize,
1079        frame_idx: u64,
1080        retry_focus_move: Option<FocusDirection>,
1081        retry_focus_reveal: bool,
1082    ) -> RenderComputationOutput {
1083        let draw_timer = Instant::now();
1084        debug!("Computing draw commands...");
1085        let cursor_position = args.cursor_state.position();
1086        let is_retry = retry_focus_move.is_some() || retry_focus_reveal;
1087        let pointer_changes = if is_retry {
1088            Vec::new()
1089        } else {
1090            args.cursor_state.take_events()
1091        };
1092        let keyboard_events = if is_retry {
1093            Vec::new()
1094        } else {
1095            args.keyboard_state.take_events()
1096        };
1097        let ime_events = if is_retry {
1098            Vec::new()
1099        } else {
1100            args.ime_state.take_events()
1101        };
1102
1103        // Clear any existing compute resources
1104        args.app.compute_resource_manager().write().clear();
1105        let layout_dirty_nodes = take_layout_dirty_nodes();
1106
1107        let (
1108            graph,
1109            window_requests,
1110            layout_diagnostics,
1111            record_cost,
1112            pending_focus_move_retry,
1113            pending_focus_reveal_retry,
1114        ) = TesseraRuntime::with_mut(|rt| {
1115            let component_tree = &mut rt.component_tree;
1116            component_tree.compute(
1117                crate::component_tree::ComputeParams {
1118                    screen_size,
1119                    cursor_position,
1120                    pointer_changes,
1121                    keyboard_events,
1122                    ime_events,
1123                    retry_focus_move,
1124                    retry_focus_reveal,
1125                    modifiers: args.keyboard_state.modifiers(),
1126                    layout_dirty_nodes: &layout_dirty_nodes,
1127                },
1128                crate::component_tree::ComputeMode::Full {
1129                    compute_resource_manager: args.app.compute_resource_manager(),
1130                    gpu: args.app.device(),
1131                },
1132            )
1133        });
1134        flush_pending_focus_callbacks();
1135
1136        let draw_cost = draw_timer.elapsed();
1137        debug!("Draw commands computed in {draw_cost:?}");
1138        (
1139            graph,
1140            window_requests,
1141            draw_cost,
1142            layout_diagnostics,
1143            record_cost,
1144            pending_focus_move_retry,
1145            pending_focus_reveal_retry,
1146        )
1147    }
1148
1149    #[cfg(feature = "debug-dirty-overlay")]
1150    fn collect_dirty_overlay_rects(
1151        screen_size: PxSize,
1152        build_tree_result: &BuildTreeResult,
1153    ) -> Vec<PxRect> {
1154        if matches!(build_tree_result.mode(), BuildTreeMode::SkipNoInvalidation) {
1155            return Vec::new();
1156        }
1157        if matches!(build_tree_result.mode(), BuildTreeMode::RootRecompose) {
1158            if build_tree_result.had_invalidations() {
1159                return vec![PxRect::from_position_size(PxPosition::ZERO, screen_size)];
1160            }
1161            return Vec::new();
1162        }
1163        TesseraRuntime::with(|rt| {
1164            let mut rects = Vec::with_capacity(build_tree_result.dirty_replay_roots().len());
1165            for instance_key in build_tree_result.dirty_replay_roots() {
1166                let Some(node_id) = rt
1167                    .component_tree
1168                    .find_node_id_by_instance_key(*instance_key)
1169                else {
1170                    continue;
1171                };
1172                let Some(metadata) = rt.component_tree.metadatas().get(&node_id) else {
1173                    continue;
1174                };
1175                let (Some(abs_position), Some(size)) =
1176                    (metadata.abs_position, metadata.computed_data)
1177                else {
1178                    continue;
1179                };
1180                if size.width.0 <= 0 || size.height.0 <= 0 {
1181                    continue;
1182                }
1183                rects.push(PxRect::from_position_size(
1184                    abs_position,
1185                    PxSize::new(size.width, size.height),
1186                ));
1187            }
1188            rects
1189        })
1190    }
1191
1192    /// Perform the actual GPU rendering for the provided commands and return
1193    /// the render duration.
1194    #[cfg(not(feature = "debug-dirty-overlay"))]
1195    #[instrument(level = "debug", skip(args, execution))]
1196    fn perform_render<'a>(
1197        args: &mut RenderFrameArgs<'a>,
1198        execution: RenderGraphExecution,
1199    ) -> std::time::Duration {
1200        #[cfg(feature = "profiling")]
1201        let _profiler_guard =
1202            ProfilerScopeGuard::new(ProfilerPhase::RenderFrame, None, None, Some("render_frame"));
1203        let render_timer = Instant::now();
1204
1205        // skip actual rendering if window is minimized
1206        if TesseraRuntime::with(|rt| rt.window_minimized) {
1207            return render_timer.elapsed();
1208        }
1209
1210        debug!("Rendering draw commands...");
1211        if let Err(e) = args.app.render(execution) {
1212            match e {
1213                wgpu::SurfaceError::Outdated | wgpu::SurfaceError::Lost => {
1214                    debug!("Surface outdated/lost, resizing...");
1215                    args.app.resize_surface();
1216                }
1217                wgpu::SurfaceError::Timeout => warn!("Surface timeout. Frame will be dropped."),
1218                wgpu::SurfaceError::OutOfMemory => {
1219                    error!("Surface out of memory. Panicking.");
1220                    panic!("Surface out of memory");
1221                }
1222                _ => {
1223                    error!("Surface error: {e}. Attempting to continue.");
1224                }
1225            }
1226        }
1227        let render_cost = render_timer.elapsed();
1228        debug!("Rendered to surface in {render_cost:?}");
1229        render_cost
1230    }
1231
1232    #[cfg(feature = "debug-dirty-overlay")]
1233    #[instrument(level = "debug", skip(args, execution, dirty_overlay_rects))]
1234    fn perform_render<'a>(
1235        args: &mut RenderFrameArgs<'a>,
1236        execution: RenderGraphExecution,
1237        dirty_overlay_rects: &[PxRect],
1238    ) -> std::time::Duration {
1239        #[cfg(feature = "profiling")]
1240        let _profiler_guard =
1241            ProfilerScopeGuard::new(ProfilerPhase::RenderFrame, None, None, Some("render_frame"));
1242        let render_timer = Instant::now();
1243
1244        // skip actual rendering if window is minimized
1245        if TesseraRuntime::with(|rt| rt.window_minimized) {
1246            return render_timer.elapsed();
1247        }
1248
1249        debug!("Rendering draw commands...");
1250        if let Err(e) = args.app.render(execution, dirty_overlay_rects) {
1251            match e {
1252                wgpu::SurfaceError::Outdated | wgpu::SurfaceError::Lost => {
1253                    debug!("Surface outdated/lost, resizing...");
1254                    args.app.resize_surface();
1255                }
1256                wgpu::SurfaceError::Timeout => warn!("Surface timeout. Frame will be dropped."),
1257                wgpu::SurfaceError::OutOfMemory => {
1258                    error!("Surface out of memory. Panicking.");
1259                    panic!("Surface out of memory");
1260                }
1261                _ => {
1262                    error!("Surface error: {e}. Attempting to continue.");
1263                }
1264            }
1265        }
1266        let render_cost = render_timer.elapsed();
1267        debug!("Rendered to surface in {render_cost:?}");
1268        render_cost
1269    }
1270
1271    #[instrument(level = "debug", skip(context))]
1272    fn execute_render_frame(context: RenderFrameContext<'_, F>) -> RenderFrameOutcome {
1273        let RenderFrameContext {
1274            entry_point,
1275            args,
1276            accessibility_enabled,
1277            decorations,
1278            window_label,
1279            frame_idx,
1280            #[cfg(feature = "profiling")]
1281            redraw_reasons,
1282        } = context;
1283        #[cfg(feature = "profiling")]
1284        let frame_timer = Instant::now();
1285        #[cfg(feature = "profiling")]
1286        profiler_begin_frame(frame_idx);
1287        begin_frame_clock(Instant::now());
1288        // Tick frame-nanos receivers before build so their state writes are
1289        // consumed by the current recomposition pass.
1290        tick_frame_nanos_receivers();
1291        // notify the windowing system before rendering
1292        // this will help winit to properly schedule and make assumptions about its
1293        // internal state
1294        args.app.window().pre_present_notify();
1295        // and tell runtime the new size
1296        TesseraRuntime::with_mut(|rt: &mut TesseraRuntime| rt.window_size = args.app.size().into());
1297        // Build the component tree and measure time
1298        let mut build_tree_result = build_component_tree(entry_point);
1299        debug!("Component tree build mode: {:?}", build_tree_result.mode());
1300
1301        // Compute draw commands
1302        let screen_size: PxSize = args.app.size().into();
1303        let (
1304            mut new_graph,
1305            mut window_requests,
1306            mut draw_cost,
1307            mut layout_diagnostics,
1308            mut record_cost,
1309            mut pending_focus_move_retry,
1310            mut pending_focus_reveal_retry,
1311        ) = Self::compute_draw_commands(args, screen_size, frame_idx, None, false);
1312        let mut beyond_bounds_retry_count = 0usize;
1313        while pending_focus_move_retry.is_some() || pending_focus_reveal_retry {
1314            if beyond_bounds_retry_count >= Self::MAX_FOCUS_BEYOND_BOUNDS_RETRIES
1315                || !has_pending_build_invalidations()
1316            {
1317                break;
1318            }
1319
1320            beyond_bounds_retry_count += 1;
1321            let retry_build = build_component_tree(entry_point);
1322            build_tree_result.absorb_retry(retry_build);
1323
1324            let (
1325                retry_graph,
1326                retry_window_requests,
1327                retry_draw_cost,
1328                retry_layout_diagnostics,
1329                retry_record_cost,
1330                next_focus_move_retry,
1331                next_focus_reveal_retry,
1332            ) = Self::compute_draw_commands(
1333                args,
1334                screen_size,
1335                frame_idx,
1336                pending_focus_move_retry,
1337                pending_focus_reveal_retry,
1338            );
1339            new_graph = retry_graph;
1340            window_requests = retry_window_requests;
1341            draw_cost += retry_draw_cost;
1342            layout_diagnostics = retry_layout_diagnostics;
1343            record_cost += retry_record_cost;
1344            pending_focus_move_retry = next_focus_move_retry;
1345            pending_focus_reveal_retry = next_focus_reveal_retry;
1346        }
1347        let final_frame_instance_keys =
1348            TesseraRuntime::with(|runtime| runtime.component_tree.live_instance_keys());
1349        let removed_focus_handles = retain_persistent_focus_handles(&final_frame_instance_keys);
1350        if !removed_focus_handles.handle_ids.is_empty()
1351            || !removed_focus_handles.requester_ids.is_empty()
1352        {
1353            TesseraRuntime::with_mut(|runtime| {
1354                runtime.component_tree.focus_owner_mut().remove_handles(
1355                    &removed_focus_handles.handle_ids,
1356                    &removed_focus_handles.requester_ids,
1357                );
1358            });
1359        }
1360        #[cfg(not(feature = "profiling"))]
1361        let _ = (layout_diagnostics, record_cost);
1362        #[cfg(feature = "debug-dirty-overlay")]
1363        let dirty_overlay_rects =
1364            Self::collect_dirty_overlay_rects(screen_size, &build_tree_result);
1365        #[cfg(feature = "debug-dirty-overlay")]
1366        let overlay_clear_pending = !dirty_overlay_rects.is_empty();
1367        let (composite_context, composite_registry) =
1368            args.app.composite_context_parts(screen_size, frame_idx);
1369        let new_graph =
1370            composite::expand_composites(new_graph, composite_context, composite_registry);
1371        let RenderGraphExecution {
1372            ops,
1373            resources,
1374            external_resources,
1375        } = new_graph.into_execution();
1376        // Perform GPU render every frame.
1377        let render_cost = Self::perform_render(
1378            args,
1379            RenderGraphExecution {
1380                ops,
1381                resources,
1382                external_resources,
1383            },
1384            #[cfg(feature = "debug-dirty-overlay")]
1385            &dirty_overlay_rects,
1386        );
1387        // Log frame statistics
1388        let render_breakdown = args.app.last_render_breakdown();
1389        Self::log_frame_stats(
1390            build_tree_result.duration(),
1391            draw_cost,
1392            render_cost,
1393            render_breakdown,
1394        );
1395
1396        #[cfg(feature = "profiling")]
1397        {
1398            let render_duration_ns = Some(render_cost.as_nanos());
1399            let render_acquire_ns = render_breakdown.map(|breakdown| breakdown.acquire.as_nanos());
1400            let render_build_passes_ns =
1401                render_breakdown.map(|breakdown| breakdown.build_passes.as_nanos());
1402            let render_encode_ns = render_breakdown.map(|breakdown| breakdown.encode.as_nanos());
1403            let render_submit_ns = render_breakdown.map(|breakdown| breakdown.submit.as_nanos());
1404            let render_present_ns = render_breakdown.map(|breakdown| breakdown.present.as_nanos());
1405            let frame_total_ns = frame_timer.elapsed().as_nanos();
1406            let inter_frame_wait_ns = (frame_idx > 0).then(|| frame_delta().as_nanos());
1407            let nodes = TesseraRuntime::with(|rt| rt.component_tree.profiler_nodes());
1408            submit_frame_meta(FrameMeta {
1409                frame_idx,
1410                build_mode: build_tree_result.profiler_build_mode(),
1411                redraw_reasons,
1412                inter_frame_wait_ns,
1413                partial_replay_nodes: build_tree_result.partial_replay_nodes(),
1414                total_nodes_before_build: build_tree_result.total_nodes_before_build(),
1415                render_time_ns: render_duration_ns,
1416                render_acquire_ns,
1417                render_build_passes_ns,
1418                render_encode_ns,
1419                render_submit_ns,
1420                render_present_ns,
1421                build_tree_time_ns: Some(build_tree_result.duration().as_nanos()),
1422                draw_time_ns: Some(draw_cost.as_nanos()),
1423                record_time_ns: Some(record_cost.as_nanos()),
1424                frame_total_ns: Some(frame_total_ns),
1425                layout_diagnostics: Some(layout_diagnostics),
1426                nodes,
1427            });
1428        }
1429
1430        // Prepare accessibility tree update before clearing the component tree if
1431        // needed
1432        let accessibility_update = if accessibility_enabled {
1433            Self::build_accessibility_update(window_label)
1434        } else {
1435            None
1436        };
1437
1438        #[cfg(feature = "profiling")]
1439        profiler_end_frame();
1440
1441        // Handle the window requests (cursor / IME)
1442        // Only set cursor when not at window edges to let window manager handle resize
1443        // cursors
1444        let cursor_position = args.cursor_state.position();
1445        let window_size = args.app.size();
1446        let resize_direction = (!decorations).then(|| {
1447            Self::cursor_resize_direction(cursor_position, window_size, Self::RESIZE_EDGE_THRESHOLD)
1448        });
1449
1450        if let Some(direction) = resize_direction.flatten() {
1451            let icon = Self::cursor_icon_for_resize(direction);
1452            args.app
1453                .window()
1454                .set_cursor(winit::window::Cursor::Icon(icon));
1455        } else {
1456            let should_set_cursor = Self::should_set_cursor_pos(
1457                cursor_position,
1458                window_size.width as f64,
1459                window_size.height as f64,
1460                Self::RESIZE_EDGE_THRESHOLD,
1461            );
1462
1463            if should_set_cursor {
1464                args.app
1465                    .window()
1466                    .set_cursor(winit::window::Cursor::Icon(window_requests.cursor_icon));
1467            }
1468        }
1469
1470        let window_action = window_requests.window_action;
1471
1472        let ime_bridge_update = args
1473            .ime_bridge_state
1474            .update_request(window_requests.ime_request);
1475        if let Some(allowed) = ime_bridge_update.allowed {
1476            #[cfg(not(target_os = "android"))]
1477            args.app.window().set_ime_allowed(allowed);
1478            #[cfg(target_os = "android")]
1479            {
1480                if allowed {
1481                    if !*args.android_ime_opened {
1482                        args.app.window().set_ime_allowed(true);
1483                        show_soft_input(true, args.event_loop.android_app());
1484                        *args.android_ime_opened = true;
1485                    }
1486                } else if *args.android_ime_opened {
1487                    args.app.window().set_ime_allowed(false);
1488                    hide_soft_input(args.event_loop.android_app());
1489                    *args.android_ime_opened = false;
1490                }
1491            }
1492        }
1493        if let Some((position, size)) = ime_bridge_update.cursor_area {
1494            args.app
1495                .window()
1496                .set_ime_cursor_area::<PxPosition, PxSize>(position, size);
1497        } else if ime_bridge_update.snapshot_changed
1498            && let Some(ime_request) = ime_bridge_update.request
1499            && ime_request.position.is_none()
1500        {
1501            warn!("IME request missing position; skipping IME cursor area update");
1502        }
1503
1504        // End of frame cleanup
1505        args.cursor_state.frame_cleanup();
1506
1507        let runtime_pending_work = RuntimePendingWork {
1508            invalidation_pending: has_pending_build_invalidations(),
1509            frame_receiver_pending: has_pending_frame_nanos_receivers(),
1510        };
1511
1512        RenderFrameOutcome {
1513            accessibility_update,
1514            window_action,
1515            runtime_pending_work,
1516            #[cfg(feature = "debug-dirty-overlay")]
1517            overlay_clear_pending,
1518        }
1519    }
1520}
1521
1522impl<F: Fn()> Renderer<F> {
1523    #[cfg(target_os = "android")]
1524    fn plugin_context(&self, event_loop: &ActiveEventLoop) -> Option<PluginContext> {
1525        let app = self.app.as_ref()?;
1526        Some(PluginContext::new(
1527            app.window_arc(),
1528            event_loop.android_app().clone(),
1529        ))
1530    }
1531
1532    #[cfg(not(target_os = "android"))]
1533    fn plugin_context(&self, _event_loop: &ActiveEventLoop) -> Option<PluginContext> {
1534        let app = self.app.as_ref()?;
1535        Some(PluginContext::new(app.window_arc()))
1536    }
1537
1538    fn try_request_redraw(window: &Window, redraw_pending: &AtomicBool) {
1539        if redraw_pending
1540            .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
1541            .is_ok()
1542        {
1543            window.request_redraw();
1544        }
1545    }
1546
1547    fn request_redraw_now(&self) {
1548        if let Some(app) = self.app.as_ref() {
1549            Self::try_request_redraw(app.window(), self.redraw_request_pending.as_ref());
1550        }
1551    }
1552
1553    fn install_runtime_redraw_waker(&self) {
1554        let Some(proxy) = self.event_loop_proxy.clone() else {
1555            clear_redraw_waker();
1556            return;
1557        };
1558        install_redraw_waker(Arc::new(move || {
1559            let _ = proxy.send_event(RendererUserEvent::RuntimeRedrawWake);
1560        }));
1561    }
1562
1563    #[cfg(target_family = "wasm")]
1564    fn begin_web_initialization(&mut self, window: Arc<Window>) {
1565        let Some(proxy) = self.event_loop_proxy.clone() else {
1566            error!("Missing event loop proxy for web renderer initialization");
1567            return;
1568        };
1569
1570        self.web_init_in_progress = true;
1571        let epoch = self.web_init_epoch;
1572        let pending_web_inits = self.pending_web_inits.clone();
1573        let sample_count = self.config.sample_count;
1574        let transparent = self.config.window.transparent;
1575        spawn_local(async move {
1576            let render_core = RenderCore::new(window, sample_count, transparent).await;
1577            pending_web_inits.borrow_mut().push((epoch, render_core));
1578            let _ = proxy.send_event(RendererUserEvent::WebInitReady(epoch));
1579        });
1580    }
1581
1582    #[cfg(target_family = "wasm")]
1583    fn finish_web_initialization(&mut self, event_loop: &ActiveEventLoop, epoch: u64) -> bool {
1584        let pending_render_core = {
1585            let mut pending_web_inits = self.pending_web_inits.borrow_mut();
1586            pending_web_inits
1587                .iter()
1588                .position(|(ready_epoch, _)| *ready_epoch == epoch)
1589                .map(|position| pending_web_inits.remove(position).1)
1590        };
1591        let Some(mut render_core) = pending_render_core else {
1592            return false;
1593        };
1594
1595        if epoch != self.web_init_epoch {
1596            return false;
1597        }
1598
1599        let mut context = PipelineContext::new(&mut render_core);
1600        for module in &self.modules {
1601            module.register_pipelines(&mut context);
1602        }
1603
1604        self.app = Some(render_core);
1605        self.web_init_in_progress = false;
1606        self.install_runtime_redraw_waker();
1607        #[cfg(feature = "profiling")]
1608        self.request_redraw_with_reasons(WakeSource::Lifecycle, vec![RedrawReason::Startup]);
1609        #[cfg(not(feature = "profiling"))]
1610        self.request_redraw_now();
1611
1612        if let Some(context) = self.plugin_context(event_loop) {
1613            self.plugins.resumed(&context);
1614        }
1615        true
1616    }
1617
1618    #[cfg(feature = "profiling")]
1619    fn request_redraw_with_reasons(&mut self, source: WakeSource, mut reasons: Vec<RedrawReason>) {
1620        reasons.sort_unstable();
1621        reasons.dedup();
1622        if reasons.is_empty() {
1623            return;
1624        }
1625
1626        self.pending_redraw_reasons.extend(reasons.iter().copied());
1627
1628        submit_wake_meta(WakeMeta {
1629            frame_idx: self.frame_index,
1630            source,
1631            reasons: reasons.clone(),
1632        });
1633
1634        self.request_redraw_now();
1635    }
1636
1637    #[cfg(feature = "profiling")]
1638    fn take_pending_redraw_reasons(&mut self) -> Vec<RedrawReason> {
1639        std::mem::take(&mut self.pending_redraw_reasons)
1640            .into_iter()
1641            .collect()
1642    }
1643
1644    // These keep behavior identical but reduce per-function complexity.
1645    fn handle_close_requested(&mut self, event_loop: &ActiveEventLoop) {
1646        if let Some(context) = self.plugin_context(event_loop) {
1647            self.plugins.shutdown(&context);
1648        }
1649        if let Some(ref app) = self.app
1650            && let Err(e) = app.save_pipeline_cache()
1651        {
1652            warn!("Failed to save pipeline cache: {}", e);
1653        }
1654        event_loop.exit();
1655    }
1656
1657    fn apply_window_action(&mut self, window: &Window, action: WindowAction) {
1658        match action {
1659            WindowAction::DragWindow => {
1660                if let Err(err) = window.drag_window() {
1661                    warn!("Failed to start window drag: {}", err);
1662                }
1663            }
1664            WindowAction::Minimize => {
1665                window.set_minimized(true);
1666            }
1667            WindowAction::Maximize => {
1668                window.set_maximized(true);
1669            }
1670            WindowAction::ToggleMaximize => {
1671                window.set_maximized(!window.is_maximized());
1672            }
1673            WindowAction::Close => {
1674                self.pending_close_requested = true;
1675            }
1676        }
1677        self.update_native_window_shape(window);
1678    }
1679
1680    fn handle_resized(&mut self, size: winit::dpi::PhysicalSize<u32>) {
1681        // Obtain the app inside the method to avoid holding a mutable borrow across
1682        // other borrows of `self`.
1683        let app = match self.app.as_mut() {
1684            Some(app) => app,
1685            None => return,
1686        };
1687        let window = app.window_arc();
1688
1689        if size.width == 0 || size.height == 0 {
1690            TesseraRuntime::with_mut(|rt| {
1691                if !rt.window_minimized {
1692                    rt.window_minimized = true;
1693                }
1694            });
1695        } else {
1696            TesseraRuntime::with_mut(|rt| {
1697                if rt.window_minimized {
1698                    rt.window_minimized = false;
1699                }
1700            });
1701            app.resize(size);
1702        }
1703        self.update_native_window_shape(&window);
1704    }
1705
1706    fn handle_cursor_moved(&mut self, position: winit::dpi::PhysicalPosition<f64>) {
1707        if self.resize_in_progress {
1708            return;
1709        }
1710        let px_position = PxPosition::from_f64_arr2([position.x, position.y]);
1711        // Update cursor position
1712        self.cursor_state.update_position(px_position);
1713        self.cursor_state.push_event(CursorEvent {
1714            timestamp: Instant::now(),
1715            pointer_id: MOUSE_POINTER_ID,
1716            content: CursorEventContent::Moved(px_position),
1717            gesture_state: GestureState::TapCandidate,
1718            consumed: false,
1719        });
1720        debug!("Cursor moved to: {}, {}", position.x, position.y);
1721    }
1722
1723    fn handle_cursor_left(&mut self) {
1724        // Clear cursor position when it leaves the window
1725        // This also set the position to None
1726        self.cursor_state.clear();
1727        debug!("Cursor left the window");
1728    }
1729
1730    fn push_accessibility_update(&mut self, tree_update: TreeUpdate) {
1731        if let Some(adapter) = self.accessibility_adapter.as_mut() {
1732            adapter.update_if_active(|| tree_update);
1733        }
1734    }
1735
1736    fn send_accessibility_update(&mut self) {
1737        if let Some(tree_update) = Self::build_accessibility_update(&self.config.window_title) {
1738            self.push_accessibility_update(tree_update);
1739        }
1740    }
1741
1742    fn build_accessibility_update(window_label: &str) -> Option<TreeUpdate> {
1743        TesseraRuntime::with(|runtime| {
1744            let tree = runtime.component_tree.tree();
1745            let metadatas = runtime.component_tree.metadatas();
1746            let root_node_id = tree.get_node_id_at(
1747                std::num::NonZero::new(1).expect("root node index must be non-zero"),
1748            )?;
1749            crate::accessibility::build_tree_update(
1750                tree,
1751                metadatas,
1752                root_node_id,
1753                Some(window_label),
1754            )
1755        })
1756    }
1757
1758    fn handle_mouse_input(
1759        &mut self,
1760        state: winit::event::ElementState,
1761        button: winit::event::MouseButton,
1762    ) {
1763        let Some(event_content) = CursorEventContent::from_press_event(state, button) else {
1764            return; // Ignore unsupported buttons
1765        };
1766
1767        if self.resize_in_progress {
1768            if matches!(
1769                event_content,
1770                CursorEventContent::Released(PressKeyEventType::Left)
1771            ) {
1772                self.resize_in_progress = false;
1773            }
1774            return;
1775        }
1776
1777        if matches!(
1778            event_content,
1779            CursorEventContent::Pressed(PressKeyEventType::Left)
1780        ) && !self.config.window.decorations
1781            && let Some(app) = self.app.as_ref()
1782        {
1783            let window_size = app.size();
1784            let direction = Self::cursor_resize_direction(
1785                self.cursor_state.position(),
1786                window_size,
1787                Self::RESIZE_EDGE_THRESHOLD,
1788            );
1789            if let Some(direction) = direction {
1790                if let Err(err) = app.window().drag_resize_window(direction) {
1791                    warn!("Failed to start window resize: {}", err);
1792                } else {
1793                    self.resize_in_progress = true;
1794                    self.cursor_state.clear();
1795                    debug!("Started native border resize; suppressing cursor input");
1796                }
1797                return;
1798            }
1799        }
1800        let event = CursorEvent {
1801            timestamp: Instant::now(),
1802            pointer_id: MOUSE_POINTER_ID,
1803            content: event_content,
1804            gesture_state: GestureState::TapCandidate,
1805            consumed: false,
1806        };
1807        self.cursor_state.push_event(event);
1808        debug!("Mouse input: {state:?} button {button:?}");
1809    }
1810
1811    fn handle_mouse_wheel(&mut self, delta: winit::event::MouseScrollDelta) {
1812        if self.resize_in_progress {
1813            return;
1814        }
1815        let event_content = CursorEventContent::from_scroll_event(delta);
1816        let event = CursorEvent {
1817            timestamp: Instant::now(),
1818            pointer_id: MOUSE_POINTER_ID,
1819            content: event_content,
1820            gesture_state: GestureState::Dragged,
1821            consumed: false,
1822        };
1823        self.cursor_state.push_event(event);
1824        debug!("Mouse scroll: {delta:?}");
1825    }
1826
1827    fn handle_touch(&mut self, touch_event: winit::event::Touch) {
1828        if self.resize_in_progress {
1829            return;
1830        }
1831        let pos = PxPosition::from_f64_arr2([touch_event.location.x, touch_event.location.y]);
1832        debug!(
1833            "Touch event: id {}, phase {:?}, position {:?}",
1834            touch_event.id, touch_event.phase, pos
1835        );
1836        match touch_event.phase {
1837            winit::event::TouchPhase::Started => {
1838                // Use new touch start handling method
1839                self.cursor_state.handle_touch_start(touch_event.id, pos);
1840            }
1841            winit::event::TouchPhase::Moved => {
1842                // Use new touch move handling method, may generate scroll event
1843                if let Some(scroll_event) = self.cursor_state.handle_touch_move(touch_event.id, pos)
1844                {
1845                    // Scroll event is already added to event queue in handle_touch_move
1846                    self.cursor_state.push_event(scroll_event);
1847                }
1848            }
1849            winit::event::TouchPhase::Ended | winit::event::TouchPhase::Cancelled => {
1850                // Use new touch end handling method
1851                self.cursor_state.handle_touch_end(touch_event.id);
1852            }
1853        }
1854    }
1855
1856    fn handle_keyboard_input(&mut self, event: winit::event::KeyEvent) {
1857        debug!("Keyboard input: {event:?}");
1858        self.keyboard_state.push_event(event);
1859    }
1860
1861    fn handle_redraw_requested(
1862        &mut self,
1863        #[cfg(target_os = "android")] event_loop: &ActiveEventLoop,
1864    ) {
1865        self.redraw_request_pending.store(false, Ordering::Release);
1866        let mut app = match self.app.take() {
1867            Some(app) => app,
1868            None => return,
1869        };
1870
1871        app.resize_if_needed();
1872        let accessibility_enabled = self.accessibility_adapter.is_some();
1873        let frame_idx = self.frame_index;
1874        #[cfg(feature = "profiling")]
1875        let redraw_reasons = self.take_pending_redraw_reasons();
1876        let window_label = &self.config.window_title;
1877
1878        let RenderFrameOutcome {
1879            accessibility_update,
1880            window_action,
1881            runtime_pending_work,
1882            #[cfg(feature = "debug-dirty-overlay")]
1883            overlay_clear_pending,
1884        } = {
1885            let mut args = RenderFrameArgs {
1886                cursor_state: &mut self.cursor_state,
1887                keyboard_state: &mut self.keyboard_state,
1888                ime_state: &mut self.ime_state,
1889                ime_bridge_state: &mut self.ime_bridge_state,
1890                #[cfg(target_os = "android")]
1891                android_ime_opened: &mut self.android_ime_opened,
1892                app: &mut app,
1893                #[cfg(target_os = "android")]
1894                event_loop,
1895            };
1896            Self::execute_render_frame(RenderFrameContext {
1897                entry_point: &self.entry_point,
1898                args: &mut args,
1899                accessibility_enabled,
1900                decorations: self.config.window.decorations,
1901                window_label,
1902                frame_idx,
1903                #[cfg(feature = "profiling")]
1904                redraw_reasons,
1905            })
1906        };
1907
1908        if let Some(action) = window_action {
1909            self.apply_window_action(app.window(), action);
1910        }
1911
1912        self.frame_index = self.frame_index.wrapping_add(1);
1913
1914        if let Some(tree_update) = accessibility_update {
1915            self.push_accessibility_update(tree_update);
1916        }
1917
1918        self.app = Some(app);
1919
1920        if runtime_pending_work.requires_redraw() {
1921            #[cfg(feature = "profiling")]
1922            self.request_redraw_with_reasons(
1923                WakeSource::Runtime,
1924                runtime_pending_work.redraw_reasons(),
1925            );
1926            #[cfg(not(feature = "profiling"))]
1927            self.request_redraw_now();
1928        }
1929        #[cfg(feature = "debug-dirty-overlay")]
1930        if overlay_clear_pending {
1931            self.request_redraw_now();
1932        }
1933    }
1934}
1935
1936/// Implementation of winit's `ApplicationHandler` trait for the Tessera
1937/// renderer.
1938///
1939/// This implementation handles the application lifecycle events from winit,
1940/// including window creation, suspension/resumption, and various window events.
1941/// It bridges the gap between winit's event system and Tessera's
1942/// component-based UI framework.
1943impl<F: Fn()> ApplicationHandler<RendererUserEvent> for Renderer<F> {
1944    /// Called when the application is resumed or started.
1945    ///
1946    /// This method is responsible for:
1947    /// - Creating the application window with appropriate attributes
1948    /// - Initializing the WGPU context and surface
1949    /// - Registering rendering pipelines
1950    /// - Setting up the initial application state
1951    ///
1952    /// On desktop platforms, this is typically called once at startup.
1953    /// On mobile platforms (especially Android), this may be called multiple
1954    /// times as the app is suspended and resumed.
1955    ///
1956    /// ## Window Configuration
1957    ///
1958    /// The window is created with:
1959    /// - Title: "Tessera"
1960    /// - Transparency: Enabled (allows for transparent backgrounds)
1961    /// - Default size and position (platform-dependent)
1962    ///
1963    /// ## Pipeline Registration
1964    ///
1965    /// After WGPU initialization, render modules register pipelines through
1966    /// [`PipelineContext`]. This typically includes basic component pipelines
1967    /// and any custom shaders your application requires.
1968    #[tracing::instrument(level = "debug", skip(self, event_loop))]
1969    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
1970        event_loop.set_control_flow(ControlFlow::Wait);
1971        #[cfg(feature = "profiling")]
1972        submit_runtime_meta(RuntimeMeta {
1973            kind: RuntimeEventKind::Resumed,
1974        });
1975        #[cfg(target_family = "wasm")]
1976        self.finish_web_initialization(event_loop, self.web_init_epoch);
1977        // Just return if the app is already created
1978        if self.app.is_some() {
1979            self.install_runtime_redraw_waker();
1980            #[cfg(feature = "profiling")]
1981            self.request_redraw_with_reasons(WakeSource::Lifecycle, vec![RedrawReason::Startup]);
1982            #[cfg(not(feature = "profiling"))]
1983            self.request_redraw_now();
1984            return;
1985        }
1986        #[cfg(target_family = "wasm")]
1987        if self.web_init_in_progress {
1988            return;
1989        }
1990
1991        // Create a new window (initially hidden for AccessKit initialization)
1992        let window_attributes = Window::default_attributes()
1993            .with_title(&self.config.window_title)
1994            .with_decorations(self.config.window.decorations)
1995            .with_resizable(self.config.window.resizable)
1996            .with_transparent(self.config.window.transparent)
1997            .with_visible(false); // Hide initially for AccessKit
1998        #[cfg(target_family = "wasm")]
1999        let window_attributes = {
2000            use winit::platform::web::WindowAttributesExtWebSys;
2001
2002            if let Some(canvas) = Self::resolve_web_canvas(&self.config) {
2003                window_attributes
2004                    .with_canvas(Some(canvas))
2005                    .with_append(false)
2006            } else {
2007                window_attributes.with_append(true)
2008            }
2009        };
2010        let window = match event_loop.create_window(window_attributes) {
2011            Ok(window) => Arc::new(window),
2012            Err(err) => {
2013                error!("Failed to create window: {err}");
2014                return;
2015            }
2016        };
2017
2018        // Initialize AccessKit adapter BEFORE showing the window
2019        if let Some(proxy) = self.event_loop_proxy.clone() {
2020            self.accessibility_adapter = Some(AccessKitAdapter::with_event_loop_proxy(
2021                event_loop, &window, proxy,
2022            ));
2023        }
2024
2025        // Now show the window after AccessKit is initialized
2026        window.set_visible(true);
2027        self.update_native_window_shape(&window);
2028
2029        #[cfg(target_family = "wasm")]
2030        {
2031            self.begin_web_initialization(window);
2032            return;
2033        }
2034
2035        #[cfg(not(target_family = "wasm"))]
2036        {
2037            let mut render_core = pollster::block_on(RenderCore::new(
2038                window.clone(),
2039                self.config.sample_count,
2040                self.config.window.transparent,
2041            ));
2042
2043            // Register pipelines
2044            let mut context = PipelineContext::new(&mut render_core);
2045            for module in &self.modules {
2046                module.register_pipelines(&mut context);
2047            }
2048
2049            self.app = Some(render_core);
2050            self.install_runtime_redraw_waker();
2051            #[cfg(feature = "profiling")]
2052            self.request_redraw_with_reasons(WakeSource::Lifecycle, vec![RedrawReason::Startup]);
2053            #[cfg(not(feature = "profiling"))]
2054            self.request_redraw_now();
2055
2056            if let Some(context) = self.plugin_context(event_loop) {
2057                self.plugins.resumed(&context);
2058            }
2059        }
2060    }
2061
2062    /// Called when the application is suspended.
2063    ///
2064    /// This method should handle cleanup and state preservation when the
2065    /// application is being suspended (e.g., on mobile platforms when the
2066    /// app goes to background).
2067    ///
2068    /// ## Platform Considerations
2069    ///
2070    /// - **Desktop**: Rarely called, mainly during shutdown
2071    /// - **Android**: Called when app goes to background
2072    /// - **iOS**: Called during app lifecycle transitions
2073    fn suspended(&mut self, event_loop: &ActiveEventLoop) {
2074        debug!("Suspending renderer; tearing down WGPU resources.");
2075        #[cfg(feature = "profiling")]
2076        submit_runtime_meta(RuntimeMeta {
2077            kind: RuntimeEventKind::Suspended,
2078        });
2079
2080        if let Some(context) = self.plugin_context(event_loop) {
2081            self.plugins.suspended(&context);
2082        }
2083
2084        if let Some(app) = self.app.take() {
2085            app.compute_resource_manager().write().clear();
2086        }
2087
2088        // Clean up AccessKit adapter
2089        self.accessibility_adapter = None;
2090
2091        self.cursor_state = CursorState::default();
2092        self.keyboard_state = KeyboardState::default();
2093        self.ime_state = ImeState::default();
2094        self.ime_bridge_state.reset();
2095        self.resize_in_progress = false;
2096        self.redraw_request_pending.store(false, Ordering::Release);
2097        #[cfg(target_family = "wasm")]
2098        {
2099            self.web_init_epoch = self.web_init_epoch.wrapping_add(1);
2100            self.web_init_in_progress = false;
2101            self.pending_web_inits.borrow_mut().clear();
2102        }
2103        #[cfg(target_os = "android")]
2104        {
2105            self.android_ime_opened = false;
2106        }
2107
2108        TesseraRuntime::with_mut(|runtime| {
2109            runtime.component_tree.reset();
2110            runtime.cursor_icon_request = None;
2111            runtime.window_minimized = false;
2112            runtime.window_size = [0, 0];
2113        });
2114        clear_layout_snapshots();
2115        reset_layout_dirty_tracking();
2116        reset_component_replay_tracking();
2117        reset_focus_read_dependencies();
2118        reset_render_slot_read_dependencies();
2119        reset_state_read_dependencies();
2120        reset_component_context_tracking();
2121        reset_context_read_dependencies();
2122        reset_build_invalidations();
2123        reset_frame_clock();
2124        clear_redraw_waker();
2125        clear_persistent_focus_handles();
2126        crate::runtime::reset_slots();
2127        #[cfg(feature = "profiling")]
2128        self.pending_redraw_reasons.clear();
2129    }
2130
2131    #[tracing::instrument(level = "debug", skip(self, event_loop))]
2132    fn window_event(
2133        &mut self,
2134        event_loop: &ActiveEventLoop,
2135        _window_id: WindowId,
2136        event: WindowEvent,
2137    ) {
2138        if self.pending_close_requested {
2139            self.pending_close_requested = false;
2140            self.handle_close_requested(event_loop);
2141            return;
2142        }
2143
2144        // Forward event to AccessKit adapter
2145        if let (Some(adapter), Some(app)) = (&mut self.accessibility_adapter, &self.app) {
2146            adapter.process_event(app.window(), &event);
2147        }
2148
2149        // Handle window events
2150        let mut request_redraw = false;
2151        #[cfg(feature = "profiling")]
2152        let mut redraw_reasons = Vec::new();
2153        match event {
2154            WindowEvent::CloseRequested => {
2155                self.handle_close_requested(event_loop);
2156            }
2157            WindowEvent::Resized(size) => {
2158                self.handle_resized(size);
2159                request_redraw = true;
2160                #[cfg(feature = "profiling")]
2161                redraw_reasons.push(RedrawReason::WindowResized);
2162            }
2163            WindowEvent::CursorMoved {
2164                device_id: _,
2165                position,
2166            } => {
2167                self.handle_cursor_moved(position);
2168                request_redraw = true;
2169                #[cfg(feature = "profiling")]
2170                redraw_reasons.push(RedrawReason::CursorMoved);
2171            }
2172            WindowEvent::CursorLeft { device_id: _ } => {
2173                self.handle_cursor_left();
2174                request_redraw = true;
2175                #[cfg(feature = "profiling")]
2176                redraw_reasons.push(RedrawReason::CursorLeft);
2177            }
2178            WindowEvent::MouseInput {
2179                device_id: _,
2180                state,
2181                button,
2182            } => {
2183                self.handle_mouse_input(state, button);
2184                request_redraw = true;
2185                #[cfg(feature = "profiling")]
2186                redraw_reasons.push(RedrawReason::MouseInput);
2187            }
2188            WindowEvent::MouseWheel {
2189                device_id: _,
2190                delta,
2191                phase: _,
2192            } => {
2193                self.handle_mouse_wheel(delta);
2194                request_redraw = true;
2195                #[cfg(feature = "profiling")]
2196                redraw_reasons.push(RedrawReason::MouseWheel);
2197            }
2198            WindowEvent::Touch(touch_event) => {
2199                self.handle_touch(touch_event);
2200                request_redraw = true;
2201                #[cfg(feature = "profiling")]
2202                redraw_reasons.push(RedrawReason::TouchInput);
2203            }
2204            WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
2205                if let Some(scale_factor_lock) = SCALE_FACTOR.get() {
2206                    *scale_factor_lock.write() = scale_factor;
2207                } else {
2208                    let _ = SCALE_FACTOR.set(RwLock::new(scale_factor));
2209                }
2210                if let Some(app) = self.app.as_ref() {
2211                    self.update_native_window_shape(app.window());
2212                }
2213                request_redraw = true;
2214                #[cfg(feature = "profiling")]
2215                redraw_reasons.push(RedrawReason::ScaleFactorChanged);
2216            }
2217            WindowEvent::KeyboardInput { event, .. } => {
2218                self.handle_keyboard_input(event);
2219                request_redraw = true;
2220                #[cfg(feature = "profiling")]
2221                redraw_reasons.push(RedrawReason::KeyboardInput);
2222            }
2223            WindowEvent::ModifiersChanged(modifiers) => {
2224                debug!("Modifiers changed: {modifiers:?}");
2225                self.keyboard_state.update_modifiers(modifiers.state());
2226                request_redraw = true;
2227                #[cfg(feature = "profiling")]
2228                redraw_reasons.push(RedrawReason::ModifiersChanged);
2229            }
2230            WindowEvent::Ime(ime_event) => {
2231                debug!("IME event: {ime_event:?}");
2232                self.ime_state.push_event(ime_event);
2233                request_redraw = true;
2234                #[cfg(feature = "profiling")]
2235                redraw_reasons.push(RedrawReason::ImeEvent);
2236            }
2237            WindowEvent::Focused(focused) => {
2238                TesseraRuntime::with_mut(|runtime| {
2239                    runtime
2240                        .component_tree
2241                        .focus_owner_mut()
2242                        .set_owner_focused(focused);
2243                });
2244                flush_pending_focus_callbacks();
2245                if self.resize_in_progress {
2246                    self.resize_in_progress = false;
2247                    self.cursor_state.clear();
2248                }
2249                request_redraw = true;
2250                #[cfg(feature = "profiling")]
2251                redraw_reasons.push(RedrawReason::FocusChanged);
2252            }
2253            WindowEvent::RedrawRequested => {
2254                #[cfg(target_os = "android")]
2255                self.handle_redraw_requested(event_loop);
2256                #[cfg(not(target_os = "android"))]
2257                self.handle_redraw_requested();
2258            }
2259            _ => (),
2260        }
2261
2262        if request_redraw {
2263            #[cfg(feature = "profiling")]
2264            self.request_redraw_with_reasons(WakeSource::WindowEvent, redraw_reasons);
2265            #[cfg(not(feature = "profiling"))]
2266            self.request_redraw_now();
2267        }
2268    }
2269
2270    fn user_event(&mut self, event_loop: &ActiveEventLoop, event: RendererUserEvent) {
2271        use accesskit_winit::WindowEvent as AccessKitWindowEvent;
2272        #[cfg(not(target_family = "wasm"))]
2273        let _ = event_loop;
2274
2275        match event {
2276            RendererUserEvent::RuntimeRedrawWake => {
2277                #[cfg(feature = "profiling")]
2278                self.request_redraw_with_reasons(
2279                    WakeSource::Runtime,
2280                    vec![RedrawReason::RuntimeInvalidation],
2281                );
2282                #[cfg(not(feature = "profiling"))]
2283                self.request_redraw_now();
2284            }
2285            #[cfg(target_family = "wasm")]
2286            RendererUserEvent::WebInitReady(epoch) => {
2287                self.finish_web_initialization(event_loop, epoch);
2288            }
2289            RendererUserEvent::AccessKit(event) => {
2290                if self.accessibility_adapter.is_none() {
2291                    return;
2292                }
2293                match event.window_event {
2294                    AccessKitWindowEvent::InitialTreeRequested => {
2295                        self.send_accessibility_update();
2296                    }
2297                    AccessKitWindowEvent::ActionRequested(action_request) => {
2298                        // Dispatch action to the appropriate component handler
2299                        let handled = TesseraRuntime::with_mut(|runtime| {
2300                            let (tree, metadatas, focus_owner) =
2301                                runtime.component_tree.accessibility_dispatch_context();
2302                            crate::accessibility::dispatch_action(
2303                                tree,
2304                                metadatas,
2305                                focus_owner,
2306                                action_request,
2307                            )
2308                        });
2309                        flush_pending_focus_callbacks();
2310
2311                        if !handled {
2312                            debug!("Action was not handled by any component");
2313                        }
2314                    }
2315                    AccessKitWindowEvent::AccessibilityDeactivated => {
2316                        debug!("AccessKit deactivated");
2317                    }
2318                }
2319            }
2320        }
2321    }
2322}
2323
2324#[cfg(test)]
2325mod tests {
2326    use super::{RendererImeBridgeState, RendererImeBridgeUpdate};
2327    use crate::{ImeRequest, Px, PxPosition, px::PxSize};
2328
2329    fn positioned_request(
2330        position: PxPosition,
2331        size: PxSize,
2332        selection_range: Option<std::ops::Range<usize>>,
2333        composition_range: Option<std::ops::Range<usize>>,
2334    ) -> ImeRequest {
2335        let mut request = ImeRequest::new(size)
2336            .with_local_position(position)
2337            .with_selection_range(selection_range)
2338            .with_composition_range(composition_range);
2339        request.position = Some(position);
2340        request
2341    }
2342
2343    #[test]
2344    fn ime_bridge_enables_and_updates_cursor_area_for_first_request() {
2345        let mut bridge = RendererImeBridgeState::default();
2346        let request = positioned_request(
2347            PxPosition::new(Px(12), Px(18)),
2348            PxSize::new(Px(7), Px(13)),
2349            Some(2..6),
2350            Some(3..5),
2351        );
2352
2353        let update = bridge.update_request(Some(request.clone()));
2354
2355        assert_eq!(
2356            update,
2357            RendererImeBridgeUpdate {
2358                allowed: Some(true),
2359                cursor_area: Some((PxPosition::new(Px(12), Px(18)), PxSize::new(Px(7), Px(13)))),
2360                request: Some(request.clone()),
2361                snapshot_changed: true,
2362            }
2363        );
2364        assert_eq!(bridge.request(), Some(&request));
2365    }
2366
2367    #[test]
2368    fn ime_bridge_reuses_identical_request_without_window_updates() {
2369        let request = positioned_request(
2370            PxPosition::new(Px(12), Px(18)),
2371            PxSize::new(Px(7), Px(13)),
2372            Some(2..6),
2373            Some(3..5),
2374        );
2375        let mut bridge = RendererImeBridgeState::default();
2376        bridge.update_request(Some(request.clone()));
2377
2378        let update = bridge.update_request(Some(request.clone()));
2379
2380        assert_eq!(
2381            update,
2382            RendererImeBridgeUpdate {
2383                allowed: None,
2384                cursor_area: None,
2385                request: Some(request.clone()),
2386                snapshot_changed: false,
2387            }
2388        );
2389        assert_eq!(bridge.request(), Some(&request));
2390    }
2391
2392    #[test]
2393    fn ime_bridge_preserves_selection_and_composition_changes_without_cursor_reapply() {
2394        let size = PxSize::new(Px(7), Px(13));
2395        let mut bridge = RendererImeBridgeState::default();
2396        bridge.update_request(Some(positioned_request(
2397            PxPosition::new(Px(12), Px(18)),
2398            size,
2399            Some(2..6),
2400            Some(3..5),
2401        )));
2402        let next_request = positioned_request(
2403            PxPosition::new(Px(12), Px(18)),
2404            size,
2405            Some(4..8),
2406            Some(5..7),
2407        );
2408
2409        let update = bridge.update_request(Some(next_request.clone()));
2410
2411        assert_eq!(update.allowed, None);
2412        assert_eq!(update.cursor_area, None);
2413        assert!(update.snapshot_changed);
2414        assert_eq!(bridge.request(), Some(&next_request));
2415    }
2416
2417    #[test]
2418    fn ime_bridge_disables_and_clears_snapshot_when_request_is_removed() {
2419        let request = positioned_request(
2420            PxPosition::new(Px(12), Px(18)),
2421            PxSize::new(Px(7), Px(13)),
2422            Some(2..6),
2423            Some(3..5),
2424        );
2425        let mut bridge = RendererImeBridgeState::default();
2426        bridge.update_request(Some(request));
2427
2428        let update = bridge.update_request(None);
2429
2430        assert_eq!(
2431            update,
2432            RendererImeBridgeUpdate {
2433                allowed: Some(false),
2434                cursor_area: None,
2435                request: None,
2436                snapshot_changed: true,
2437            }
2438        );
2439        assert_eq!(bridge.request(), None);
2440    }
2441}
2442
2443/// Shows the Android soft keyboard (virtual keyboard).
2444///
2445/// This function uses JNI to interact with the Android system to display the
2446/// soft keyboard. It's specifically designed for Android applications and
2447/// handles the complex JNI calls required to show the input method.
2448///
2449/// ## Parameters
2450///
2451/// - `show_implicit`: Whether to show the keyboard implicitly (without explicit
2452///   user action)
2453/// - `android_app`: Reference to the Android application context
2454///
2455/// ## Platform Support
2456///
2457/// This function is only available on Android (`target_os = "android"`). It
2458/// will not be compiled on other platforms.
2459///
2460/// ## Error Handling
2461///
2462/// The function includes comprehensive error handling for JNI operations. If
2463/// any JNI call fails, the function will return early without crashing the
2464/// application. Exception handling is also included to clear any Java
2465/// exceptions that might occur.
2466///
2467/// ## Implementation Notes
2468///
2469/// This implementation is based on the android-activity crate and follows the
2470/// pattern established in: https://github.com/rust-mobile/android-activity/pull/178
2471///
2472/// The function performs these steps:
2473/// 1. Get the Java VM and activity context
2474/// 2. Find the InputMethodManager system service
2475/// 3. Get the current window's decor view
2476/// 4. Call `showSoftInput` on the InputMethodManager
2477///
2478/// ## Usage
2479///
2480/// This function is typically called internally by the renderer when IME input
2481/// is requested. You generally don't need to call this directly in application
2482/// code.
2483// https://github.com/rust-mobile/android-activity/pull/178
2484#[cfg(target_os = "android")]
2485pub fn show_soft_input(show_implicit: bool, android_app: &AndroidApp) {
2486    let ctx = android_app;
2487
2488    let jvm = unsafe { jni::JavaVM::from_raw(ctx.vm_as_ptr().cast()) }.unwrap();
2489    let na = unsafe { jni::objects::JObject::from_raw(ctx.activity_as_ptr().cast()) };
2490
2491    let mut env = jvm.attach_current_thread().unwrap();
2492    if env.exception_check().unwrap() {
2493        return;
2494    }
2495    let class_ctxt = env.find_class("android/content/Context").unwrap();
2496    if env.exception_check().unwrap() {
2497        return;
2498    }
2499    let ims = env
2500        .get_static_field(class_ctxt, "INPUT_METHOD_SERVICE", "Ljava/lang/String;")
2501        .unwrap();
2502    if env.exception_check().unwrap() {
2503        return;
2504    }
2505
2506    let im_manager = env
2507        .call_method(
2508            &na,
2509            "getSystemService",
2510            "(Ljava/lang/String;)Ljava/lang/Object;",
2511            &[(&ims).into()],
2512        )
2513        .unwrap()
2514        .l()
2515        .unwrap();
2516    if env.exception_check().unwrap() {
2517        return;
2518    }
2519
2520    let jni_window = env
2521        .call_method(&na, "getWindow", "()Landroid/view/Window;", &[])
2522        .unwrap()
2523        .l()
2524        .unwrap();
2525    if env.exception_check().unwrap() {
2526        return;
2527    }
2528    let view = env
2529        .call_method(&jni_window, "getDecorView", "()Landroid/view/View;", &[])
2530        .unwrap()
2531        .l()
2532        .unwrap();
2533    if env.exception_check().unwrap() {
2534        return;
2535    }
2536
2537    let _ = env.call_method(
2538        im_manager,
2539        "showSoftInput",
2540        "(Landroid/view/View;I)Z",
2541        &[
2542            jni::objects::JValue::Object(&view),
2543            if show_implicit {
2544                (ndk_sys::ANATIVEACTIVITY_SHOW_SOFT_INPUT_IMPLICIT as i32).into()
2545            } else {
2546                0i32.into()
2547            },
2548        ],
2549    );
2550    // showSoftInput can trigger exceptions if the keyboard is currently animating
2551    // open/closed
2552    if env.exception_check().unwrap() {
2553        let _ = env.exception_clear();
2554    }
2555}
2556
2557/// Hides the Android soft keyboard (virtual keyboard).
2558///
2559/// This function uses JNI to interact with the Android system to hide the soft
2560/// keyboard. It's the counterpart to [`show_soft_input`] and handles the
2561/// complex JNI calls required to dismiss the input method.
2562///
2563/// ## Parameters
2564///
2565/// - `android_app`: Reference to the Android application context
2566///
2567/// ## Platform Support
2568///
2569/// This function is only available on Android (`target_os = "android"`). It
2570/// will not be compiled on other platforms.
2571///
2572/// ## Error Handling
2573///
2574/// Like [`show_soft_input`], this function includes comprehensive error
2575/// handling for JNI operations. If any step fails, the function returns early
2576/// without crashing. Java exceptions are also properly handled and cleared.
2577///
2578/// ## Implementation Details
2579///
2580/// The function performs these steps:
2581/// 1. Get the Java VM and activity context
2582/// 2. Find the InputMethodManager system service
2583/// 3. Get the current window and its decor view
2584/// 4. Get the window token from the decor view
2585/// 5. Call `hideSoftInputFromWindow` on the InputMethodManager
2586///
2587/// ## Usage
2588///
2589/// This function is typically called internally by the renderer when IME input
2590/// is no longer needed. You generally don't need to call this directly in
2591/// application code.
2592///
2593/// ## Relationship to show_soft_input
2594///
2595/// This function is designed to work in tandem with [`show_soft_input`]. The
2596/// renderer automatically manages the keyboard visibility based on IME requests
2597/// from components.
2598#[cfg(target_os = "android")]
2599pub fn hide_soft_input(android_app: &AndroidApp) {
2600    use jni::objects::JValue;
2601
2602    let ctx = android_app;
2603    let jvm = match unsafe { jni::JavaVM::from_raw(ctx.vm_as_ptr().cast()) } {
2604        Ok(jvm) => jvm,
2605        Err(_) => return, // Early exit if failing to get the JVM
2606    };
2607    let activity = unsafe { jni::objects::JObject::from_raw(ctx.activity_as_ptr().cast()) };
2608
2609    let mut env = match jvm.attach_current_thread() {
2610        Ok(env) => env,
2611        Err(_) => return,
2612    };
2613
2614    // --- 1. Get the InputMethodManager ---
2615    // This part is the same as in show_soft_input.
2616    let class_ctxt = match env.find_class("android/content/Context") {
2617        Ok(c) => c,
2618        Err(_) => return,
2619    };
2620    let ims_field =
2621        match env.get_static_field(class_ctxt, "INPUT_METHOD_SERVICE", "Ljava/lang/String;") {
2622            Ok(f) => f,
2623            Err(_) => return,
2624        };
2625    let ims = match ims_field.l() {
2626        Ok(s) => s,
2627        Err(_) => return,
2628    };
2629
2630    let im_manager = match env.call_method(
2631        &activity,
2632        "getSystemService",
2633        "(Ljava/lang/String;)Ljava/lang/Object;",
2634        &[(&ims).into()],
2635    ) {
2636        Ok(m) => match m.l() {
2637            Ok(im) => im,
2638            Err(_) => return,
2639        },
2640        Err(_) => return,
2641    };
2642
2643    // --- 2. Get the current window's token ---
2644    // This is the key step that differs from show_soft_input.
2645    let window = match env.call_method(&activity, "getWindow", "()Landroid/view/Window;", &[]) {
2646        Ok(w) => match w.l() {
2647            Ok(win) => win,
2648            Err(_) => return,
2649        },
2650        Err(_) => return,
2651    };
2652
2653    let decor_view = match env.call_method(&window, "getDecorView", "()Landroid/view/View;", &[]) {
2654        Ok(v) => match v.l() {
2655            Ok(view) => view,
2656            Err(_) => return,
2657        },
2658        Err(_) => return,
2659    };
2660
2661    let window_token =
2662        match env.call_method(&decor_view, "getWindowToken", "()Landroid/os/IBinder;", &[]) {
2663            Ok(t) => match t.l() {
2664                Ok(token) => token,
2665                Err(_) => return,
2666            },
2667            Err(_) => return,
2668        };
2669
2670    // --- 3. Call hideSoftInputFromWindow ---
2671    let _ = env.call_method(
2672        &im_manager,
2673        "hideSoftInputFromWindow",
2674        "(Landroid/os/IBinder;I)Z",
2675        &[
2676            JValue::Object(&window_token),
2677            JValue::Int(0), // flags, usually 0
2678        ],
2679    );
2680
2681    // Hiding the keyboard can also cause exceptions, so we clear them.
2682    if env.exception_check().unwrap_or(false) {
2683        let _ = env.exception_clear();
2684    }
2685}