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