tessera_ui/renderer.rs
1//! # Tessera Renderer
2//!
3//! The core rendering system for the Tessera UI framework. This module provides the main
4//! [`Renderer`] struct that manages the application lifecycle, event handling, and rendering
5//! pipeline for cross-platform UI applications.
6//!
7//! ## Overview
8//!
9//! The renderer is built on top of WGPU and winit, providing:
10//! - Cross-platform window management (Windows, Linux, macOS, Android)
11//! - Event handling (mouse, touch, keyboard, IME)
12//! - Pluggable rendering pipeline system
13//! - Component tree management and rendering
14//! - Performance monitoring and optimization
15//!
16//! ## Architecture
17//!
18//! The renderer follows a modular architecture with several key components:
19//!
20//! - **[`app`]**: WGPU application management and surface handling
21//! - **[`command`]**: Rendering command abstraction
22//! - **[`compute`]**: Compute shader pipeline management
23//! - **[`drawer`]**: Drawing pipeline management and execution
24//!
25//! ## Basic Usage
26//!
27//! The most common way to use the renderer is through the [`Renderer::run`] method:
28//!
29//! ```no_run
30//! use tessera_ui::Renderer;
31//!
32//! // Define your UI entry point
33//! fn my_app() {
34//! // Your UI components go here
35//! }
36//!
37//! // Run the application
38//! Renderer::run(
39//! my_app, // Entry point function
40//! |app| {
41//! // Register rendering pipelines
42//! // For example, tessera_ui_basic_components::pipelines::register_pipelines(app);
43//! }
44//! ).unwrap();
45//! ```
46//!
47//! ## Configuration
48//!
49//! You can customize the renderer behavior using [`TesseraConfig`]:
50//!
51//! ```no_run
52//! use tessera_ui::{Renderer, renderer::TesseraConfig};
53//!
54//! # fn foo() -> Result<(), Box<dyn std::error::Error>> {
55//! let config = TesseraConfig {
56//! sample_count: 8, // 8x MSAA
57//! ..Default::default()
58//! };
59//!
60//! Renderer::run_with_config(
61//! || { /* my_app */ },
62//! |_app| { /* register_pipelines */ },
63//! config
64//! )?;
65//! # Ok(())
66//! # }
67//! ```
68//!
69//! ## Platform Support
70//!
71//! ### Desktop Platforms (Windows, Linux, macOS)
72//!
73//! ```rust,ignore
74//! use tessera_ui::Renderer;
75//! use tessera_ui_macros::tessera;
76//!
77//! #[tessera] // You need to mark every component function with `#[tessera_macros::tessera]`
78//! fn entry_point() {}
79//! fn register_pipelines(_: &mut tessera_ui::renderer::WgpuApp) {}
80//!
81//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
82//! Renderer::run(entry_point, register_pipelines)?;
83//! # Ok(())
84//! # }
85//! ```
86//!
87//! ### Android
88//!
89//! ```no_run
90//! use tessera_ui::Renderer;
91//! #[cfg(target_os = "android")]
92//! use winit::platform::android::activity::AndroidApp;
93//!
94//! fn entry_point() {}
95//! fn register_pipelines(_: &mut tessera_ui::renderer::WgpuApp) {}
96//!
97//! #[cfg(target_os = "android")]
98//! fn android_main(android_app: AndroidApp) {
99//! Renderer::run(entry_point, register_pipelines, android_app).unwrap();
100//! }
101//! ```
102//!
103//! ## Event Handling
104//!
105//! The renderer automatically handles various input events:
106//!
107//! - **Mouse Events**: Click, move, scroll, enter/leave
108//! - **Touch Events**: Multi-touch support with gesture recognition
109//! - **Keyboard Events**: Key press/release, with platform-specific handling
110//! - **IME Events**: Input method support for international text input
111//!
112//! Events are processed and forwarded to the component tree for handling.
113//!
114//! ## Performance Monitoring
115//!
116//! The renderer includes built-in performance monitoring that logs frame statistics
117//! when performance drops below 60 FPS:
118//!
119//! ```text
120//! WARN Jank detected! Frame statistics:
121//! Build tree cost: 2.1ms
122//! Draw commands cost: 1.8ms
123//! Render cost: 12.3ms
124//! Total frame cost: 16.2ms
125//! Fps: 61.73
126//! ```
127//!
128//! ## Examples
129//!
130//! ### Simple Counter Application
131//!
132//! ```rust,ignore
133//! use std::sync::{Arc, atomic::{AtomicU32, Ordering}};
134//!
135//! use tessera_ui::{Renderer, Color, Dp};
136//! use tessera_ui_macros::tessera;
137//!
138//! struct AppState {
139//! count: AtomicU32,
140//! }
141//!
142//! #[tessera] // You need to mark every component function with `#[tessera_macros::tessera]`
143//! fn counter_app(state: Arc<AppState>) {
144//! let _count = state.count.load(Ordering::Relaxed);
145//! // Your UI components would go here
146//! // This is a simplified example without actual UI components
147//! }
148//!
149//! fn main() -> Result<(), Box<dyn std::error::Error>> {
150//! let state = Arc::new(AppState {
151//! count: AtomicU32::new(0),
152//! });
153//!
154//! Renderer::run(
155//! move || counter_app(state.clone()),
156//! |_app| {
157//! // Register your rendering pipelines here
158//! // tessera_ui_basic_components::pipelines::register_pipelines(app);
159//! }
160//! )?;
161//!
162//! Ok(())
163//! }
164//! ```
165//!
166//! ### Custom Rendering Pipeline
167//!
168//! ```no_run
169//! use tessera_ui::{Renderer, renderer::WgpuApp};
170//!
171//! fn register_custom_pipelines(app: &mut WgpuApp) {
172//! // Register basic components first
173//! // tessera_ui_basic_components::pipelines::register_pipelines(app);
174//!
175//! // Add your custom pipelines
176//! // app.drawer.register_pipeline("my_custom_shader", my_pipeline);
177//! }
178//!
179//! fn main() -> Result<(), Box<dyn std::error::Error>> {
180//! Renderer::run(
181//! || { /* your UI */ },
182//! register_custom_pipelines
183//! )?;
184//! Ok(())
185//! }
186//! ```
187
188pub mod app;
189pub mod command;
190pub mod compute;
191pub mod drawer;
192pub mod reorder;
193
194use std::{any::TypeId, sync::Arc, time::Instant};
195
196use tessera_ui_macros::tessera;
197use tracing::{debug, error, instrument, warn};
198use winit::{
199 application::ApplicationHandler,
200 error::EventLoopError,
201 event::WindowEvent,
202 event_loop::{ActiveEventLoop, EventLoop},
203 window::{Window, WindowId},
204};
205
206use crate::{
207 Clipboard, ImeState, PxPosition,
208 component_tree::WindowRequests,
209 cursor::{CursorEvent, CursorEventContent, CursorState},
210 dp::SCALE_FACTOR,
211 keyboard_state::KeyboardState,
212 px::PxSize,
213 runtime::TesseraRuntime,
214 thread_utils,
215};
216
217pub use app::WgpuApp;
218pub use command::{BarrierRequirement, Command};
219pub use compute::{ComputablePipeline, ComputePipelineRegistry};
220pub use drawer::{DrawCommand, DrawablePipeline, PipelineRegistry};
221
222#[cfg(target_os = "android")]
223use winit::platform::android::{
224 ActiveEventLoopExtAndroid, EventLoopBuilderExtAndroid, activity::AndroidApp,
225};
226
227/// Configuration for the Tessera runtime and renderer.
228///
229/// This struct allows you to customize various aspects of the renderer's behavior,
230/// including anti-aliasing settings and other rendering parameters.
231///
232/// # Examples
233///
234/// ```
235/// use tessera_ui::renderer::TesseraConfig;
236///
237/// // Default configuration (4x MSAA)
238/// let config = TesseraConfig::default();
239///
240/// // Custom configuration with 8x MSAA
241/// let config = TesseraConfig {
242/// sample_count: 8,
243/// ..Default::default()
244/// };
245///
246/// // Disable MSAA for better performance
247/// let config = TesseraConfig {
248/// sample_count: 1,
249/// ..Default::default()
250/// };
251/// ```
252#[derive(Debug, Clone)]
253pub struct TesseraConfig {
254 /// The number of samples to use for Multi-Sample Anti-Aliasing (MSAA).
255 ///
256 /// MSAA helps reduce aliasing artifacts (jagged edges) in rendered graphics
257 /// by sampling multiple points per pixel and averaging the results.
258 ///
259 /// ## Supported Values
260 /// - `1`: Disables MSAA (best performance, lower quality)
261 /// - `4`: 4x MSAA (balanced quality/performance)
262 /// - `8`: 8x MSAA (high quality, higher performance cost)
263 ///
264 /// ## Notes
265 /// - Higher sample counts provide better visual quality but consume more GPU resources
266 /// - The GPU must support the chosen sample count; unsupported values may cause errors
267 /// - Mobile devices may have limited support for higher sample counts
268 /// - Consider using lower values on resource-constrained devices
269 pub sample_count: u32,
270 /// The title of the application window.
271 /// Defaults to "Tessera" if not specified.
272 pub window_title: String,
273}
274
275impl Default for TesseraConfig {
276 /// Creates a default configuration with 4x MSAA and "Tessera" as the window title.
277 fn default() -> Self {
278 Self {
279 sample_count: 4,
280 window_title: "Tessera".to_string(),
281 }
282 }
283}
284
285/// The main renderer struct that manages the application lifecycle and rendering.
286///
287/// The `Renderer` is the core component of the Tessera UI framework, responsible for:
288/// - Managing the application window and WGPU context
289/// - Handling input events (mouse, touch, keyboard, IME)
290/// - Coordinating the component tree building and rendering process
291/// - Managing rendering pipelines and resources
292///
293/// ## Type Parameters
294///
295/// - `F`: The entry point function type that defines your UI. Must implement `Fn()`.
296/// - `R`: The pipeline registration function type. Must implement `Fn(&mut WgpuApp) + Clone + 'static`.
297///
298/// ## Lifecycle
299///
300/// The renderer follows this lifecycle:
301/// 1. **Initialization**: Create window, initialize WGPU context, register pipelines
302/// 2. **Event Loop**: Handle window events, input events, and render requests
303/// 3. **Frame Rendering**: Build component tree → Compute draw commands → Render to surface
304/// 4. **Cleanup**: Automatic cleanup when the application exits
305///
306/// ## Thread Safety
307///
308/// The renderer runs on the main thread and coordinates with other threads for:
309/// - Component tree building (potentially parallelized)
310/// - Resource management
311/// - Event processing
312///
313/// ## Examples
314///
315/// See the module-level documentation for usage examples.
316pub struct Renderer<F: Fn(), R: Fn(&mut WgpuApp) + Clone + 'static> {
317 /// The WGPU application context, initialized after window creation
318 app: Option<WgpuApp>,
319 /// The entry point function that defines the root of your UI component tree
320 entry_point: F,
321 /// Tracks cursor/mouse position and button states
322 cursor_state: CursorState,
323 /// Tracks keyboard key states and events
324 keyboard_state: KeyboardState,
325 /// Tracks Input Method Editor (IME) state for international text input
326 ime_state: ImeState,
327 /// Function called during initialization to register rendering pipelines
328 register_pipelines_fn: R,
329 /// Configuration settings for the renderer
330 config: TesseraConfig,
331 /// Clipboard manager
332 clipboard: Clipboard,
333 /// Commands from the previous frame, for dirty rectangle optimization
334 previous_commands: Vec<(Command, TypeId, PxSize, PxPosition)>,
335 #[cfg(target_os = "android")]
336 /// Android-specific state tracking whether the soft keyboard is currently open
337 android_ime_opened: bool,
338}
339
340impl<F: Fn(), R: Fn(&mut WgpuApp) + Clone + 'static> Renderer<F, R> {
341 /// Runs the Tessera application with default configuration on desktop platforms.
342 ///
343 /// This is the most convenient way to start a Tessera application on Windows, Linux, or macOS.
344 /// It uses the default [`TesseraConfig`] settings (4x MSAA).
345 ///
346 /// # Parameters
347 ///
348 /// - `entry_point`: A function that defines your UI. This function will be called every frame
349 /// to build the component tree. It should contain your root UI components.
350 /// - `register_pipelines_fn`: A function that registers rendering pipelines with the WGPU app.
351 /// Typically, you'll call `tessera_ui_basic_components::pipelines::register_pipelines(app)` here.
352 ///
353 /// # Returns
354 ///
355 /// Returns `Ok(())` when the application exits normally, or an `EventLoopError` if the
356 /// event loop fails to start or encounters a critical error.
357 ///
358 /// # Examples
359 ///
360 /// ```no_run
361 /// use tessera_ui::Renderer;
362 ///
363 /// fn my_ui() {
364 /// // Your UI components go here
365 /// }
366 ///
367 /// fn main() -> Result<(), Box<dyn std::error::Error>> {
368 /// Renderer::run(
369 /// my_ui,
370 /// |_app| {
371 /// // Register your rendering pipelines here
372 /// // tessera_ui_basic_components::pipelines::register_pipelines(app);
373 /// }
374 /// )?;
375 /// Ok(())
376 /// }
377 /// ```
378 #[cfg(not(target_os = "android"))]
379 #[tracing::instrument(level = "info", skip(entry_point, register_pipelines_fn))]
380 pub fn run(entry_point: F, register_pipelines_fn: R) -> Result<(), EventLoopError> {
381 Self::run_with_config(entry_point, register_pipelines_fn, Default::default())
382 }
383
384 /// Runs the Tessera application with custom configuration on desktop platforms.
385 ///
386 /// This method allows you to customize the renderer behavior through [`TesseraConfig`].
387 /// Use this when you need to adjust settings like MSAA sample count or other rendering parameters.
388 ///
389 /// # Parameters
390 ///
391 /// - `entry_point`: A function that defines your UI
392 /// - `register_pipelines_fn`: A function that registers rendering pipelines
393 /// - `config`: Custom configuration for the renderer
394 ///
395 /// # Returns
396 ///
397 /// Returns `Ok(())` when the application exits normally, or an `EventLoopError` if the
398 /// event loop fails to start.
399 ///
400 /// # Examples
401 ///
402 /// ```no_run
403 /// use tessera_ui::{Renderer, renderer::TesseraConfig};
404 ///
405 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
406 /// let config = TesseraConfig {
407 /// sample_count: 8, // 8x MSAA for higher quality
408 /// ..Default::default()
409 /// };
410 ///
411 /// Renderer::run_with_config(
412 /// || { /* my_ui */ },
413 /// |_app| { /* register_pipelines */ },
414 /// config
415 /// )?;
416 /// # Ok(())
417 /// # }
418 /// ```
419 #[tracing::instrument(level = "info", skip(entry_point, register_pipelines_fn))]
420 #[cfg(not(any(target_os = "android")))]
421 pub fn run_with_config(
422 entry_point: F,
423 register_pipelines_fn: R,
424 config: TesseraConfig,
425 ) -> Result<(), EventLoopError> {
426 let event_loop = EventLoop::new().unwrap();
427 let app = None;
428 let cursor_state = CursorState::default();
429 let keyboard_state = KeyboardState::default();
430 let ime_state = ImeState::default();
431 let clipboard = Clipboard::new();
432 let mut renderer = Self {
433 app,
434 entry_point,
435 cursor_state,
436 keyboard_state,
437 register_pipelines_fn,
438 ime_state,
439 config,
440 clipboard,
441 previous_commands: Vec::new(),
442 };
443 thread_utils::set_thread_name("Tessera Renderer");
444 event_loop.run_app(&mut renderer)
445 }
446
447 /// Runs the Tessera application with default configuration on Android.
448 ///
449 /// This method is specifically for Android applications and requires an `AndroidApp` instance
450 /// that is typically provided by the `android_main` function.
451 ///
452 /// # Parameters
453 ///
454 /// - `entry_point`: A function that defines your UI
455 /// - `register_pipelines_fn`: A function that registers rendering pipelines
456 /// - `android_app`: The Android application context
457 ///
458 /// # Returns
459 ///
460 /// Returns `Ok(())` when the application exits normally, or an `EventLoopError` if the
461 /// event loop fails to start.
462 ///
463 /// # Examples
464 ///
465 /// ```no_run
466 /// use tessera_ui::Renderer;
467 /// use winit::platform::android::activity::AndroidApp;
468 ///
469 /// fn my_ui() {}
470 /// fn register_pipelines(_: &mut tessera_ui::renderer::WgpuApp) {}
471 ///
472 /// #[unsafe(no_mangle)]
473 /// fn android_main(android_app: AndroidApp) {
474 /// Renderer::run(
475 /// my_ui,
476 /// register_pipelines,
477 /// android_app
478 /// ).unwrap();
479 /// }
480 /// ```
481 #[cfg(target_os = "android")]
482 #[tracing::instrument(level = "info", skip(entry_point, register_pipelines_fn, android_app))]
483 pub fn run(
484 entry_point: F,
485 register_pipelines_fn: R,
486 android_app: AndroidApp,
487 ) -> Result<(), EventLoopError> {
488 Self::run_with_config(
489 entry_point,
490 register_pipelines_fn,
491 android_app,
492 Default::default(),
493 )
494 }
495
496 /// Runs the Tessera application with custom configuration on Android.
497 ///
498 /// This method allows you to customize the renderer behavior on Android through [`TesseraConfig`].
499 ///
500 /// # Parameters
501 ///
502 /// - `entry_point`: A function that defines your UI
503 /// - `register_pipelines_fn`: A function that registers rendering pipelines
504 /// - `android_app`: The Android application context
505 /// - `config`: Custom configuration for the renderer
506 ///
507 /// # Returns
508 ///
509 /// Returns `Ok(())` when the application exits normally, or an `EventLoopError` if the
510 /// event loop fails to start.
511 ///
512 /// # Examples
513 ///
514 /// ```no_run
515 /// use tessera_ui::{Renderer, renderer::TesseraConfig};
516 /// use winit::platform::android::activity::AndroidApp;
517 ///
518 /// fn my_ui() {}
519 /// fn register_pipelines(_: &mut tessera_ui::renderer::WgpuApp) {}
520 ///
521 /// #[unsafe(no_mangle)]
522 /// fn android_main(android_app: AndroidApp) {
523 /// let config = TesseraConfig {
524 /// sample_count: 2, // Lower MSAA for mobile performance
525 /// };
526 ///
527 /// Renderer::run_with_config(
528 /// my_ui,
529 /// register_pipelines,
530 /// android_app,
531 /// config
532 /// ).unwrap();
533 /// }
534 /// ```
535 #[cfg(target_os = "android")]
536 #[tracing::instrument(level = "info", skip(entry_point, register_pipelines_fn, android_app))]
537 pub fn run_with_config(
538 entry_point: F,
539 register_pipelines_fn: R,
540 android_app: AndroidApp,
541 config: TesseraConfig,
542 ) -> Result<(), EventLoopError> {
543 let event_loop = EventLoop::builder()
544 .with_android_app(android_app.clone())
545 .build()
546 .unwrap();
547 let app = None;
548 let cursor_state = CursorState::default();
549 let keyboard_state = KeyboardState::default();
550 let ime_state = ImeState::default();
551 let clipboard = Clipboard::new(android_app);
552 let mut renderer = Self {
553 app,
554 entry_point,
555 cursor_state,
556 keyboard_state,
557 register_pipelines_fn,
558 ime_state,
559 android_ime_opened: false,
560 config,
561 clipboard,
562 previous_commands: Vec::new(),
563 };
564 thread_utils::set_thread_name("Tessera Renderer");
565 event_loop.run_app(&mut renderer)
566 }
567}
568
569// Helper struct to group render-frame arguments and reduce parameter count.
570// Kept private to this module.
571struct RenderFrameArgs<'a> {
572 pub resized: bool,
573 pub cursor_state: &'a mut CursorState,
574 pub keyboard_state: &'a mut KeyboardState,
575 pub ime_state: &'a mut ImeState,
576 #[cfg(target_os = "android")]
577 pub android_ime_opened: &'a mut bool,
578 pub app: &'a mut WgpuApp,
579 #[cfg(target_os = "android")]
580 pub event_loop: &'a ActiveEventLoop,
581 pub clipboard: &'a mut Clipboard,
582}
583
584impl<F: Fn(), R: Fn(&mut WgpuApp) + Clone + 'static> Renderer<F, R> {
585 fn should_set_cursor_pos(
586 cursor_position: Option<crate::PxPosition>,
587 window_width: f64,
588 window_height: f64,
589 edge_threshold: f64,
590 ) -> bool {
591 if let Some(pos) = cursor_position {
592 let x = pos.x.0 as f64;
593 let y = pos.y.0 as f64;
594 x > edge_threshold
595 && x < window_width - edge_threshold
596 && y > edge_threshold
597 && y < window_height - edge_threshold
598 } else {
599 false
600 }
601 }
602
603 /// Executes a single frame rendering cycle.
604 ///
605 /// This is the core rendering method that orchestrates the entire frame rendering process.
606 /// It follows a three-phase approach:
607 ///
608 /// 1. **Component Tree Building**: Calls the entry point function to build the UI component tree
609 /// 2. **Draw Command Computation**: Processes the component tree to generate rendering commands
610 /// 3. **Surface Rendering**: Executes the commands to render the final frame
611 ///
612 /// ## Performance Monitoring
613 ///
614 /// This method includes built-in performance monitoring that logs detailed timing information
615 /// when frame rates drop below 60 FPS, helping identify performance bottlenecks.
616 ///
617 /// ## Parameters
618 ///
619 /// - `entry_point`: The UI entry point function to build the component tree
620 /// - `cursor_state`: Mutable reference to cursor/mouse state for event processing
621 /// - `keyboard_state`: Mutable reference to keyboard state for event processing
622 /// - `ime_state`: Mutable reference to IME state for text input processing
623 /// - `android_ime_opened`: (Android only) Tracks soft keyboard state
624 /// - `app`: Mutable reference to the WGPU application context
625 /// - `event_loop`: (Android only) Event loop for IME management
626 ///
627 /// ## Frame Timing Breakdown
628 ///
629 /// - **Build Tree Cost**: Time spent building the component tree
630 /// - **Draw Commands Cost**: Time spent computing rendering commands
631 /// - **Render Cost**: Time spent executing GPU rendering commands
632 ///
633 /// ## Thread Safety
634 ///
635 /// This method runs on the main thread but coordinates with other threads for
636 /// component tree processing and resource management.
637 #[instrument(level = "debug", skip(entry_point))]
638 fn build_component_tree(entry_point: &F) -> std::time::Duration {
639 let tree_timer = Instant::now();
640 debug!("Building component tree...");
641 entry_wrapper(entry_point);
642 let build_tree_cost = tree_timer.elapsed();
643 debug!("Component tree built in {build_tree_cost:?}");
644 build_tree_cost
645 }
646
647 fn log_frame_stats(
648 build_tree_cost: std::time::Duration,
649 draw_cost: std::time::Duration,
650 render_cost: std::time::Duration,
651 ) {
652 let total = build_tree_cost + draw_cost + render_cost;
653 let fps = 1.0 / total.as_secs_f32();
654 if fps < 60.0 {
655 warn!(
656 "Jank detected! Frame statistics:
657Build tree cost: {:?}
658Draw commands cost: {:?}
659Render cost: {:?}
660Total frame cost: {:?}
661Fps: {:.2}
662",
663 build_tree_cost,
664 draw_cost,
665 render_cost,
666 total,
667 1.0 / total.as_secs_f32()
668 );
669 }
670 }
671
672 #[instrument(level = "debug", skip(args))]
673 fn compute_draw_commands<'a>(
674 args: &mut RenderFrameArgs<'a>,
675 screen_size: PxSize,
676 ) -> (
677 Vec<(Command, TypeId, PxSize, PxPosition)>,
678 WindowRequests,
679 std::time::Duration,
680 ) {
681 let draw_timer = Instant::now();
682 debug!("Computing draw commands...");
683 let cursor_position = args.cursor_state.position();
684 let cursor_events = args.cursor_state.take_events();
685 let keyboard_events = args.keyboard_state.take_events();
686 let ime_events = args.ime_state.take_events();
687
688 // Clear any existing compute resources
689 args.app.resource_manager.write().clear();
690
691 let (commands, window_requests) = TesseraRuntime::with_mut(|rt| {
692 rt.component_tree
693 .compute(crate::component_tree::ComputeParams {
694 screen_size,
695 cursor_position,
696 cursor_events,
697 keyboard_events,
698 ime_events,
699 modifiers: args.keyboard_state.modifiers(),
700 compute_resource_manager: args.app.resource_manager.clone(),
701 gpu: &args.app.gpu,
702 clipboard: args.clipboard,
703 })
704 });
705
706 let draw_cost = draw_timer.elapsed();
707 debug!("Draw commands computed in {draw_cost:?}");
708 (commands, window_requests, draw_cost)
709 }
710
711 /// Perform the actual GPU rendering for the provided commands and return the render duration.
712 #[instrument(level = "debug", skip(args, commands))]
713 fn perform_render<'a>(
714 args: &mut RenderFrameArgs<'a>,
715 commands: impl IntoIterator<Item = (Command, TypeId, PxSize, PxPosition)>,
716 ) -> std::time::Duration {
717 let render_timer = Instant::now();
718
719 // skip actual rendering if window is minimized
720 if TesseraRuntime::with(|rt| rt.window_minimized) {
721 args.app.window.request_redraw();
722 return render_timer.elapsed();
723 }
724
725 debug!("Rendering draw commands...");
726 // Forward commands to WgpuApp::render which accepts the same iterator type
727 args.app.render(commands).unwrap_or_else(|e| {
728 error!("Render error: {e}");
729 });
730 let render_cost = render_timer.elapsed();
731 debug!("Rendered to surface in {render_cost:?}");
732 render_cost
733 }
734
735 #[instrument(level = "debug", skip(entry_point, args, previous_commands))]
736 fn execute_render_frame(
737 entry_point: &F,
738 args: &mut RenderFrameArgs<'_>,
739 previous_commands: &mut Vec<(Command, TypeId, PxSize, PxPosition)>,
740 ) {
741 // notify the windowing system before rendering
742 // this will help winit to properly schedule and make assumptions about its internal state
743 args.app.window.pre_present_notify();
744 // and tell runtime the new size
745 TesseraRuntime::with_mut(|rt: &mut TesseraRuntime| rt.window_size = args.app.size().into());
746 // Clear any registered callbacks
747 TesseraRuntime::with_mut(|rt| rt.clear_frame_callbacks());
748
749 // Build the component tree and measure time
750 let build_tree_cost = Self::build_component_tree(entry_point);
751
752 // Compute draw commands
753 let screen_size: PxSize = args.app.size().into();
754 let (new_commands, window_requests, draw_cost) =
755 Self::compute_draw_commands(args, screen_size);
756
757 // --- Dirty Rectangle Logic ---
758 let mut dirty = false;
759 if args.resized || new_commands.len() != previous_commands.len() {
760 dirty = true;
761 } else {
762 for (new_cmd_tuple, old_cmd_tuple) in new_commands.iter().zip(previous_commands.iter())
763 {
764 let (new_cmd, _, new_size, new_pos) = new_cmd_tuple;
765 let (old_cmd, _, old_size, old_pos) = old_cmd_tuple;
766
767 let content_are_equal = match (new_cmd, old_cmd) {
768 (Command::Draw(new_draw_cmd), Command::Draw(old_draw_cmd)) => {
769 new_draw_cmd.dyn_eq(old_draw_cmd.as_ref())
770 }
771 (Command::Compute(new_compute_cmd), Command::Compute(old_compute_cmd)) => {
772 new_compute_cmd.dyn_eq(old_compute_cmd.as_ref())
773 }
774 (Command::ClipPop, Command::ClipPop) => true,
775 (Command::ClipPush(new_rect), Command::ClipPush(old_rect)) => {
776 new_rect == old_rect
777 }
778 _ => false, // Mismatched command types
779 };
780
781 if !content_are_equal || new_size != old_size || new_pos != old_pos {
782 dirty = true;
783 break;
784 }
785 }
786 }
787
788 if dirty {
789 // Perform GPU render
790 let render_cost = Self::perform_render(args, new_commands.clone());
791 // Log frame statistics
792 Self::log_frame_stats(build_tree_cost, draw_cost, render_cost);
793 } else {
794 // Perform a dummy render
795 let _ = args.app.render_dummy();
796 }
797
798 // Clear the component tree (free for next frame)
799 TesseraRuntime::with_mut(|rt| rt.component_tree.clear());
800
801 // Handle the window requests (cursor / IME)
802 // Only set cursor when not at window edges to let window manager handle resize cursors
803 let cursor_position = args.cursor_state.position();
804 let window_size = args.app.size();
805 let edge_threshold = 8.0; // Slightly larger threshold for better UX
806
807 let should_set_cursor = Self::should_set_cursor_pos(
808 cursor_position,
809 window_size.width as f64,
810 window_size.height as f64,
811 edge_threshold,
812 );
813
814 if should_set_cursor {
815 args.app
816 .window
817 .set_cursor(winit::window::Cursor::Icon(window_requests.cursor_icon));
818 }
819
820 if let Some(ime_request) = window_requests.ime_request {
821 args.app.window.set_ime_allowed(true);
822 #[cfg(target_os = "android")]
823 {
824 if !*args.android_ime_opened {
825 show_soft_input(true, args.event_loop.android_app());
826 *args.android_ime_opened = true;
827 }
828 }
829 args.app.window.set_ime_cursor_area::<PxPosition, PxSize>(
830 ime_request.position.unwrap(),
831 ime_request.size,
832 );
833 } else {
834 args.app.window.set_ime_allowed(false);
835 #[cfg(target_os = "android")]
836 {
837 if *args.android_ime_opened {
838 hide_soft_input(args.event_loop.android_app());
839 *args.android_ime_opened = false;
840 }
841 }
842 }
843
844 // End of frame cleanup
845 args.cursor_state.frame_cleanup();
846
847 // Store the commands for the next frame's comparison
848 *previous_commands = new_commands;
849
850 // Currently we render every frame, but with dirty checking, this could be conditional.
851 // For now, we still request a redraw to keep the event loop spinning for animations.
852 args.app.window.request_redraw();
853 }
854}
855
856impl<F: Fn(), R: Fn(&mut WgpuApp) + Clone + 'static> Renderer<F, R> {
857 // --- Private helper methods extracted from the large match in window_event ---
858 // These keep behavior identical but reduce per-function complexity.
859 fn handle_close_requested(&mut self, event_loop: &ActiveEventLoop) {
860 TesseraRuntime::with(|rt| rt.trigger_close_callbacks());
861 event_loop.exit();
862 }
863
864 fn handle_resized(&mut self, size: winit::dpi::PhysicalSize<u32>) {
865 // Obtain the app inside the method to avoid holding a mutable borrow across other
866 // borrows of `self`.
867 let app = match self.app.as_mut() {
868 Some(app) => app,
869 None => return,
870 };
871
872 if size.width == 0 || size.height == 0 {
873 // Window minimize handling & callback API
874 TesseraRuntime::with_mut(|rt| {
875 if !rt.window_minimized {
876 rt.window_minimized = true;
877 rt.trigger_minimize_callbacks(true);
878 }
879 });
880 } else {
881 // Window (un)minimize handling & callback API
882 TesseraRuntime::with_mut(|rt| {
883 if rt.window_minimized {
884 rt.window_minimized = false;
885 rt.trigger_minimize_callbacks(false);
886 }
887 });
888 app.resize(size);
889 }
890 }
891
892 fn handle_cursor_moved(&mut self, position: winit::dpi::PhysicalPosition<f64>) {
893 // Update cursor position
894 self.cursor_state
895 .update_position(PxPosition::from_f64_arr2([position.x, position.y]));
896 debug!("Cursor moved to: {}, {}", position.x, position.y);
897 }
898
899 fn handle_cursor_left(&mut self) {
900 // Clear cursor position when it leaves the window
901 // This also set the position to None
902 self.cursor_state.clear();
903 debug!("Cursor left the window");
904 }
905
906 fn handle_mouse_input(
907 &mut self,
908 state: winit::event::ElementState,
909 button: winit::event::MouseButton,
910 ) {
911 let Some(event_content) = CursorEventContent::from_press_event(state, button) else {
912 return; // Ignore unsupported buttons
913 };
914 let event = CursorEvent {
915 timestamp: Instant::now(),
916 content: event_content,
917 };
918 self.cursor_state.push_event(event);
919 debug!("Mouse input: {state:?} button {button:?}");
920 }
921
922 fn handle_mouse_wheel(&mut self, delta: winit::event::MouseScrollDelta) {
923 let event_content = CursorEventContent::from_scroll_event(delta);
924 let event = CursorEvent {
925 timestamp: Instant::now(),
926 content: event_content,
927 };
928 self.cursor_state.push_event(event);
929 debug!("Mouse scroll: {delta:?}");
930 }
931
932 fn handle_touch(&mut self, touch_event: winit::event::Touch) {
933 let pos = PxPosition::from_f64_arr2([touch_event.location.x, touch_event.location.y]);
934 debug!(
935 "Touch event: id {}, phase {:?}, position {:?}",
936 touch_event.id, touch_event.phase, pos
937 );
938 match touch_event.phase {
939 winit::event::TouchPhase::Started => {
940 // Use new touch start handling method
941 self.cursor_state.handle_touch_start(touch_event.id, pos);
942 }
943 winit::event::TouchPhase::Moved => {
944 // Use new touch move handling method, may generate scroll event
945 if let Some(scroll_event) = self.cursor_state.handle_touch_move(touch_event.id, pos)
946 {
947 // Scroll event is already added to event queue in handle_touch_move
948 self.cursor_state.push_event(scroll_event);
949 }
950 }
951 winit::event::TouchPhase::Ended | winit::event::TouchPhase::Cancelled => {
952 // Use new touch end handling method
953 self.cursor_state.handle_touch_end(touch_event.id);
954 }
955 }
956 }
957
958 fn handle_keyboard_input(&mut self, event: winit::event::KeyEvent) {
959 debug!("Keyboard input: {event:?}");
960 self.keyboard_state.push_event(event);
961 }
962
963 fn handle_redraw_requested(
964 &mut self,
965 #[cfg(target_os = "android")] event_loop: &ActiveEventLoop,
966 ) {
967 // Borrow the app here to avoid simultaneous mutable borrows of `self`
968 let app = match self.app.as_mut() {
969 Some(app) => app,
970 None => return,
971 };
972
973 let resized = app.resize_if_needed();
974 let mut args = RenderFrameArgs {
975 resized,
976 cursor_state: &mut self.cursor_state,
977 keyboard_state: &mut self.keyboard_state,
978 ime_state: &mut self.ime_state,
979 #[cfg(target_os = "android")]
980 android_ime_opened: &mut self.android_ime_opened,
981 app,
982 #[cfg(target_os = "android")]
983 event_loop,
984 clipboard: &mut self.clipboard,
985 };
986 Self::execute_render_frame(&self.entry_point, &mut args, &mut self.previous_commands);
987 }
988}
989
990/// Implementation of winit's `ApplicationHandler` trait for the Tessera renderer.
991///
992/// This implementation handles the application lifecycle events from winit, including
993/// window creation, suspension/resumption, and various window events. It bridges the
994/// gap between winit's event system and Tessera's component-based UI framework.
995impl<F: Fn(), R: Fn(&mut WgpuApp) + Clone + 'static> ApplicationHandler for Renderer<F, R> {
996 /// Called when the application is resumed or started.
997 ///
998 /// This method is responsible for:
999 /// - Creating the application window with appropriate attributes
1000 /// - Initializing the WGPU context and surface
1001 /// - Registering rendering pipelines
1002 /// - Setting up the initial application state
1003 ///
1004 /// On desktop platforms, this is typically called once at startup.
1005 /// On mobile platforms (especially Android), this may be called multiple times
1006 /// as the app is suspended and resumed.
1007 ///
1008 /// ## Window Configuration
1009 ///
1010 /// The window is created with:
1011 /// - Title: "Tessera"
1012 /// - Transparency: Enabled (allows for transparent backgrounds)
1013 /// - Default size and position (platform-dependent)
1014 ///
1015 /// ## Pipeline Registration
1016 ///
1017 /// After WGPU initialization, the `register_pipelines_fn` is called to set up
1018 /// all rendering pipelines. This typically includes basic component pipelines
1019 /// and any custom shaders your application requires.
1020 #[tracing::instrument(level = "debug", skip(self, event_loop))]
1021 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
1022 // Just return if the app is already created
1023 if self.app.is_some() {
1024 return;
1025 }
1026
1027 // Create a new window
1028 let window_attributes = Window::default_attributes()
1029 .with_title(&self.config.window_title)
1030 .with_transparent(true);
1031 let window = Arc::new(event_loop.create_window(window_attributes).unwrap());
1032 let register_pipelines_fn = self.register_pipelines_fn.clone();
1033
1034 let mut wgpu_app = pollster::block_on(WgpuApp::new(window, self.config.sample_count));
1035
1036 // Register pipelines
1037 wgpu_app.register_pipelines(register_pipelines_fn);
1038
1039 self.app = Some(wgpu_app);
1040 }
1041
1042 /// Called when the application is suspended.
1043 ///
1044 /// This method should handle cleanup and state preservation when the application
1045 /// is being suspended (e.g., on mobile platforms when the app goes to background).
1046 ///
1047 /// ## Current Status
1048 ///
1049 /// This method is currently not fully implemented (`todo!`). In a complete
1050 /// implementation, it should:
1051 /// - Save application state
1052 /// - Release GPU resources if necessary
1053 /// - Prepare for potential termination
1054 ///
1055 /// ## Platform Considerations
1056 ///
1057 /// - **Desktop**: Rarely called, mainly during shutdown
1058 /// - **Android**: Called when app goes to background
1059 /// - **iOS**: Called during app lifecycle transitions
1060 fn suspended(&mut self, _event_loop: &ActiveEventLoop) {
1061 todo!("Handle suspend event");
1062 }
1063
1064 /// Handles window-specific events from the windowing system.
1065 ///
1066 /// This method processes all window events including user input, window state changes,
1067 /// and rendering requests. It's the main event processing hub that translates winit
1068 /// events into Tessera's internal event system.
1069 ///
1070 /// ## Event Categories
1071 ///
1072 /// ### Window Management
1073 /// - `CloseRequested`: User requested to close the window
1074 /// - `Resized`: Window size changed
1075 /// - `ScaleFactorChanged`: Display scaling changed (high-DPI support)
1076 ///
1077 /// ### Input Events
1078 /// - `CursorMoved`: Mouse cursor position changed
1079 /// - `CursorLeft`: Mouse cursor left the window
1080 /// - `MouseInput`: Mouse button press/release
1081 /// - `MouseWheel`: Mouse wheel scrolling
1082 /// - `Touch`: Touch screen interactions (mobile)
1083 /// - `KeyboardInput`: Keyboard key press/release
1084 /// - `Ime`: Input Method Editor events (international text input)
1085 ///
1086 /// ### Rendering
1087 /// - `RedrawRequested`: System requests a frame to be rendered
1088 ///
1089 /// ## Event Processing Flow
1090 ///
1091 /// 1. **Input Events**: Captured and stored in respective state managers
1092 /// 2. **State Updates**: Internal state (cursor, keyboard, IME) is updated
1093 /// 3. **Rendering**: On redraw requests, the full rendering pipeline is executed
1094 ///
1095 /// ## Platform-Specific Handling
1096 ///
1097 /// Some events have platform-specific behavior, particularly:
1098 /// - Touch events (mobile platforms)
1099 /// - IME events (different implementations per platform)
1100 /// - Scale factor changes (high-DPI displays)
1101 #[tracing::instrument(level = "debug", skip(self, event_loop))]
1102 fn window_event(
1103 &mut self,
1104 event_loop: &ActiveEventLoop,
1105 _window_id: WindowId,
1106 event: WindowEvent,
1107 ) {
1108 // Defer borrowing `app` into specific event handlers to avoid overlapping mutable borrows.
1109 // Handlers will obtain a mutable reference to `self.app` as needed.
1110
1111 // Handle window events
1112 match event {
1113 WindowEvent::CloseRequested => {
1114 self.handle_close_requested(event_loop);
1115 }
1116 WindowEvent::Resized(size) => {
1117 self.handle_resized(size);
1118 }
1119 WindowEvent::CursorMoved {
1120 device_id: _,
1121 position,
1122 } => {
1123 self.handle_cursor_moved(position);
1124 }
1125 WindowEvent::CursorLeft { device_id: _ } => {
1126 self.handle_cursor_left();
1127 }
1128 WindowEvent::MouseInput {
1129 device_id: _,
1130 state,
1131 button,
1132 } => {
1133 self.handle_mouse_input(state, button);
1134 }
1135 WindowEvent::MouseWheel {
1136 device_id: _,
1137 delta,
1138 phase: _,
1139 } => {
1140 self.handle_mouse_wheel(delta);
1141 }
1142 WindowEvent::Touch(touch_event) => {
1143 self.handle_touch(touch_event);
1144 }
1145 WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
1146 *SCALE_FACTOR.get().unwrap().write() = scale_factor;
1147 }
1148 WindowEvent::KeyboardInput { event, .. } => {
1149 self.handle_keyboard_input(event);
1150 }
1151 WindowEvent::ModifiersChanged(modifiers) => {
1152 debug!("Modifiers changed: {modifiers:?}");
1153 self.keyboard_state.update_modifiers(modifiers.state());
1154 }
1155 WindowEvent::Ime(ime_event) => {
1156 debug!("IME event: {ime_event:?}");
1157 self.ime_state.push_event(ime_event);
1158 }
1159 WindowEvent::RedrawRequested => {
1160 #[cfg(target_os = "android")]
1161 self.handle_redraw_requested(event_loop);
1162 #[cfg(not(target_os = "android"))]
1163 self.handle_redraw_requested();
1164 }
1165 _ => (),
1166 }
1167 }
1168}
1169
1170/// Shows the Android soft keyboard (virtual keyboard).
1171///
1172/// This function uses JNI to interact with the Android system to display the soft keyboard.
1173/// It's specifically designed for Android applications and handles the complex JNI calls
1174/// required to show the input method.
1175///
1176/// ## Parameters
1177///
1178/// - `show_implicit`: Whether to show the keyboard implicitly (without explicit user action)
1179/// - `android_app`: Reference to the Android application context
1180///
1181/// ## Platform Support
1182///
1183/// This function is only available on Android (`target_os = "android"`). It will not be
1184/// compiled on other platforms.
1185///
1186/// ## Error Handling
1187///
1188/// The function includes comprehensive error handling for JNI operations. If any JNI
1189/// call fails, the function will return early without crashing the application.
1190/// Exception handling is also included to clear any Java exceptions that might occur.
1191///
1192/// ## Implementation Notes
1193///
1194/// This implementation is based on the android-activity crate and follows the pattern
1195/// established in: https://github.com/rust-mobile/android-activity/pull/178
1196///
1197/// The function performs these steps:
1198/// 1. Get the Java VM and activity context
1199/// 2. Find the InputMethodManager system service
1200/// 3. Get the current window's decor view
1201/// 4. Call `showSoftInput` on the InputMethodManager
1202///
1203/// ## Usage
1204///
1205/// This function is typically called internally by the renderer when IME input is requested.
1206/// You generally don't need to call this directly in application code.
1207// https://github.com/rust-mobile/android-activity/pull/178
1208#[cfg(target_os = "android")]
1209pub fn show_soft_input(show_implicit: bool, android_app: &AndroidApp) {
1210 let ctx = android_app;
1211
1212 let jvm = unsafe { jni::JavaVM::from_raw(ctx.vm_as_ptr().cast()) }.unwrap();
1213 let na = unsafe { jni::objects::JObject::from_raw(ctx.activity_as_ptr().cast()) };
1214
1215 let mut env = jvm.attach_current_thread().unwrap();
1216 if env.exception_check().unwrap() {
1217 return;
1218 }
1219 let class_ctxt = env.find_class("android/content/Context").unwrap();
1220 if env.exception_check().unwrap() {
1221 return;
1222 }
1223 let ims = env
1224 .get_static_field(class_ctxt, "INPUT_METHOD_SERVICE", "Ljava/lang/String;")
1225 .unwrap();
1226 if env.exception_check().unwrap() {
1227 return;
1228 }
1229
1230 let im_manager = env
1231 .call_method(
1232 &na,
1233 "getSystemService",
1234 "(Ljava/lang/String;)Ljava/lang/Object;",
1235 &[(&ims).into()],
1236 )
1237 .unwrap()
1238 .l()
1239 .unwrap();
1240 if env.exception_check().unwrap() {
1241 return;
1242 }
1243
1244 let jni_window = env
1245 .call_method(&na, "getWindow", "()Landroid/view/Window;", &[])
1246 .unwrap()
1247 .l()
1248 .unwrap();
1249 if env.exception_check().unwrap() {
1250 return;
1251 }
1252 let view = env
1253 .call_method(&jni_window, "getDecorView", "()Landroid/view/View;", &[])
1254 .unwrap()
1255 .l()
1256 .unwrap();
1257 if env.exception_check().unwrap() {
1258 return;
1259 }
1260
1261 let _ = env.call_method(
1262 im_manager,
1263 "showSoftInput",
1264 "(Landroid/view/View;I)Z",
1265 &[
1266 jni::objects::JValue::Object(&view),
1267 if show_implicit {
1268 (ndk_sys::ANATIVEACTIVITY_SHOW_SOFT_INPUT_IMPLICIT as i32).into()
1269 } else {
1270 0i32.into()
1271 },
1272 ],
1273 );
1274 // showSoftInput can trigger exceptions if the keyboard is currently animating open/closed
1275 if env.exception_check().unwrap() {
1276 let _ = env.exception_clear();
1277 }
1278}
1279
1280/// Hides the Android soft keyboard (virtual keyboard).
1281///
1282/// This function uses JNI to interact with the Android system to hide the soft keyboard.
1283/// It's the counterpart to [`show_soft_input`] and handles the complex JNI calls required
1284/// to dismiss the input method.
1285///
1286/// ## Parameters
1287///
1288/// - `android_app`: Reference to the Android application context
1289///
1290/// ## Platform Support
1291///
1292/// This function is only available on Android (`target_os = "android"`). It will not be
1293/// compiled on other platforms.
1294///
1295/// ## Error Handling
1296///
1297/// Like [`show_soft_input`], this function includes comprehensive error handling for JNI
1298/// operations. If any step fails, the function returns early without crashing. Java
1299/// exceptions are also properly handled and cleared.
1300///
1301/// ## Implementation Details
1302///
1303/// The function performs these steps:
1304/// 1. Get the Java VM and activity context
1305/// 2. Find the InputMethodManager system service
1306/// 3. Get the current window and its decor view
1307/// 4. Get the window token from the decor view
1308/// 5. Call `hideSoftInputFromWindow` on the InputMethodManager
1309///
1310/// ## Usage
1311///
1312/// This function is typically called internally by the renderer when IME input is no longer
1313/// needed. You generally don't need to call this directly in application code.
1314///
1315/// ## Relationship to show_soft_input
1316///
1317/// This function is designed to work in tandem with [`show_soft_input`]. The renderer
1318/// automatically manages the keyboard visibility based on IME requests from components.
1319#[cfg(target_os = "android")]
1320pub fn hide_soft_input(android_app: &AndroidApp) {
1321 use jni::objects::JValue;
1322
1323 let ctx = android_app;
1324 let jvm = match unsafe { jni::JavaVM::from_raw(ctx.vm_as_ptr().cast()) } {
1325 Ok(jvm) => jvm,
1326 Err(_) => return, // Early exit if failing to get the JVM
1327 };
1328 let activity = unsafe { jni::objects::JObject::from_raw(ctx.activity_as_ptr().cast()) };
1329
1330 let mut env = match jvm.attach_current_thread() {
1331 Ok(env) => env,
1332 Err(_) => return,
1333 };
1334
1335 // --- 1. Get the InputMethodManager ---
1336 // This part is the same as in show_soft_input.
1337 let class_ctxt = match env.find_class("android/content/Context") {
1338 Ok(c) => c,
1339 Err(_) => return,
1340 };
1341 let ims_field =
1342 match env.get_static_field(class_ctxt, "INPUT_METHOD_SERVICE", "Ljava/lang/String;") {
1343 Ok(f) => f,
1344 Err(_) => return,
1345 };
1346 let ims = match ims_field.l() {
1347 Ok(s) => s,
1348 Err(_) => return,
1349 };
1350
1351 let im_manager = match env.call_method(
1352 &activity,
1353 "getSystemService",
1354 "(Ljava/lang/String;)Ljava/lang/Object;",
1355 &[(&ims).into()],
1356 ) {
1357 Ok(m) => match m.l() {
1358 Ok(im) => im,
1359 Err(_) => return,
1360 },
1361 Err(_) => return,
1362 };
1363
1364 // --- 2. Get the current window's token ---
1365 // This is the key step that differs from show_soft_input.
1366 let window = match env.call_method(&activity, "getWindow", "()Landroid/view/Window;", &[]) {
1367 Ok(w) => match w.l() {
1368 Ok(win) => win,
1369 Err(_) => return,
1370 },
1371 Err(_) => return,
1372 };
1373
1374 let decor_view = match env.call_method(&window, "getDecorView", "()Landroid/view/View;", &[]) {
1375 Ok(v) => match v.l() {
1376 Ok(view) => view,
1377 Err(_) => return,
1378 },
1379 Err(_) => return,
1380 };
1381
1382 let window_token =
1383 match env.call_method(&decor_view, "getWindowToken", "()Landroid/os/IBinder;", &[]) {
1384 Ok(t) => match t.l() {
1385 Ok(token) => token,
1386 Err(_) => return,
1387 },
1388 Err(_) => return,
1389 };
1390
1391 // --- 3. Call hideSoftInputFromWindow ---
1392 let _ = env.call_method(
1393 &im_manager,
1394 "hideSoftInputFromWindow",
1395 "(Landroid/os/IBinder;I)Z",
1396 &[
1397 JValue::Object(&window_token),
1398 JValue::Int(0), // flags, usually 0
1399 ],
1400 );
1401
1402 // Hiding the keyboard can also cause exceptions, so we clear them.
1403 if env.exception_check().unwrap_or(false) {
1404 let _ = env.exception_clear();
1405 }
1406}
1407
1408/// Entry point wrapper for tessera applications.
1409///
1410/// # Why this is needed
1411///
1412/// Tessera component entry points must be functions annotated with the `tessera` macro.
1413/// Unlike some other frameworks, we cannot detect whether a provided closure has been
1414/// annotated with `tessera`. Wrapping the entry function guarantees it is invoked from
1415/// a `tessera`-annotated function, ensuring correct behavior regardless of how the user
1416/// supplied their entry point.
1417#[tessera(crate)]
1418fn entry_wrapper(entry: impl Fn()) {
1419 entry();
1420}