Skip to main content

tessera_ui/
renderer.rs

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