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 // --- Private helper methods extracted from the large match in window_event ---
900 // These keep behavior identical but reduce per-function complexity.
901 fn handle_close_requested(&mut self, event_loop: &ActiveEventLoop) {
902 TesseraRuntime::with(|rt| rt.trigger_close_callbacks());
903 event_loop.exit();
904 }
905
906 fn handle_resized(&mut self, size: winit::dpi::PhysicalSize<u32>) {
907 // Obtain the app inside the method to avoid holding a mutable borrow across other
908 // borrows of `self`.
909 let app = match self.app.as_mut() {
910 Some(app) => app,
911 None => return,
912 };
913
914 if size.width == 0 || size.height == 0 {
915 // Window minimize handling & callback API
916 TesseraRuntime::with_mut(|rt| {
917 if !rt.window_minimized {
918 rt.window_minimized = true;
919 rt.trigger_minimize_callbacks(true);
920 }
921 });
922 } else {
923 // Window (un)minimize handling & callback API
924 TesseraRuntime::with_mut(|rt| {
925 if rt.window_minimized {
926 rt.window_minimized = false;
927 rt.trigger_minimize_callbacks(false);
928 }
929 });
930 app.resize(size);
931 }
932 }
933
934 fn handle_cursor_moved(&mut self, position: winit::dpi::PhysicalPosition<f64>) {
935 // Update cursor position
936 self.cursor_state
937 .update_position(PxPosition::from_f64_arr2([position.x, position.y]));
938 debug!("Cursor moved to: {}, {}", position.x, position.y);
939 }
940
941 fn handle_cursor_left(&mut self) {
942 // Clear cursor position when it leaves the window
943 // This also set the position to None
944 self.cursor_state.clear();
945 debug!("Cursor left the window");
946 }
947
948 fn push_accessibility_update(&mut self, tree_update: TreeUpdate) {
949 if let Some(adapter) = self.accessibility_adapter.as_mut() {
950 adapter.update_if_active(|| tree_update);
951 }
952 }
953
954 fn send_accessibility_update(&mut self) {
955 if let Some(tree_update) = Self::build_accessibility_update(&self.config.window_title) {
956 self.push_accessibility_update(tree_update);
957 }
958 }
959
960 fn build_accessibility_update(window_label: &str) -> Option<TreeUpdate> {
961 TesseraRuntime::with(|runtime| {
962 let tree = runtime.component_tree.tree();
963 let metadatas = runtime.component_tree.metadatas();
964 let root_node_id = tree.get_node_id_at(std::num::NonZero::new(1).unwrap())?;
965 crate::accessibility::build_tree_update(
966 tree,
967 metadatas,
968 root_node_id,
969 Some(window_label),
970 )
971 })
972 }
973
974 fn handle_mouse_input(
975 &mut self,
976 state: winit::event::ElementState,
977 button: winit::event::MouseButton,
978 ) {
979 let Some(event_content) = CursorEventContent::from_press_event(state, button) else {
980 return; // Ignore unsupported buttons
981 };
982 let event = CursorEvent {
983 timestamp: Instant::now(),
984 content: event_content,
985 gesture_state: GestureState::TapCandidate,
986 };
987 self.cursor_state.push_event(event);
988 debug!("Mouse input: {state:?} button {button:?}");
989 }
990
991 fn handle_mouse_wheel(&mut self, delta: winit::event::MouseScrollDelta) {
992 let event_content = CursorEventContent::from_scroll_event(delta);
993 let event = CursorEvent {
994 timestamp: Instant::now(),
995 content: event_content,
996 gesture_state: GestureState::Dragged,
997 };
998 self.cursor_state.push_event(event);
999 debug!("Mouse scroll: {delta:?}");
1000 }
1001
1002 fn handle_touch(&mut self, touch_event: winit::event::Touch) {
1003 let pos = PxPosition::from_f64_arr2([touch_event.location.x, touch_event.location.y]);
1004 debug!(
1005 "Touch event: id {}, phase {:?}, position {:?}",
1006 touch_event.id, touch_event.phase, pos
1007 );
1008 match touch_event.phase {
1009 winit::event::TouchPhase::Started => {
1010 // Use new touch start handling method
1011 self.cursor_state.handle_touch_start(touch_event.id, pos);
1012 }
1013 winit::event::TouchPhase::Moved => {
1014 // Use new touch move handling method, may generate scroll event
1015 if let Some(scroll_event) = self.cursor_state.handle_touch_move(touch_event.id, pos)
1016 {
1017 // Scroll event is already added to event queue in handle_touch_move
1018 self.cursor_state.push_event(scroll_event);
1019 }
1020 }
1021 winit::event::TouchPhase::Ended | winit::event::TouchPhase::Cancelled => {
1022 // Use new touch end handling method
1023 self.cursor_state.handle_touch_end(touch_event.id);
1024 }
1025 }
1026 }
1027
1028 fn handle_keyboard_input(&mut self, event: winit::event::KeyEvent) {
1029 debug!("Keyboard input: {event:?}");
1030 self.keyboard_state.push_event(event);
1031 }
1032
1033 fn handle_redraw_requested(
1034 &mut self,
1035 #[cfg(target_os = "android")] event_loop: &ActiveEventLoop,
1036 ) {
1037 // Borrow the app here to avoid simultaneous mutable borrows of `self`
1038 let app = match self.app.as_mut() {
1039 Some(app) => app,
1040 None => return,
1041 };
1042
1043 let resized = app.resize_if_needed();
1044 let mut args = RenderFrameArgs {
1045 resized,
1046 cursor_state: &mut self.cursor_state,
1047 keyboard_state: &mut self.keyboard_state,
1048 ime_state: &mut self.ime_state,
1049 #[cfg(target_os = "android")]
1050 android_ime_opened: &mut self.android_ime_opened,
1051 app,
1052 #[cfg(target_os = "android")]
1053 event_loop,
1054 clipboard: &mut self.clipboard,
1055 };
1056 let accessibility_update = Self::execute_render_frame(
1057 &self.entry_point,
1058 &mut args,
1059 &mut self.previous_commands,
1060 self.accessibility_adapter.is_some(),
1061 &self.config.window_title,
1062 );
1063
1064 if let Some(tree_update) = accessibility_update {
1065 self.push_accessibility_update(tree_update);
1066 }
1067 }
1068}
1069
1070/// Implementation of winit's `ApplicationHandler` trait for the Tessera renderer.
1071///
1072/// This implementation handles the application lifecycle events from winit, including
1073/// window creation, suspension/resumption, and various window events. It bridges the
1074/// gap between winit's event system and Tessera's component-based UI framework.
1075impl<F: Fn(), R: Fn(&mut WgpuApp) + Clone + 'static> ApplicationHandler<AccessKitEvent>
1076 for Renderer<F, R>
1077{
1078 /// Called when the application is resumed or started.
1079 ///
1080 /// This method is responsible for:
1081 /// - Creating the application window with appropriate attributes
1082 /// - Initializing the WGPU context and surface
1083 /// - Registering rendering pipelines
1084 /// - Setting up the initial application state
1085 ///
1086 /// On desktop platforms, this is typically called once at startup.
1087 /// On mobile platforms (especially Android), this may be called multiple times
1088 /// as the app is suspended and resumed.
1089 ///
1090 /// ## Window Configuration
1091 ///
1092 /// The window is created with:
1093 /// - Title: "Tessera"
1094 /// - Transparency: Enabled (allows for transparent backgrounds)
1095 /// - Default size and position (platform-dependent)
1096 ///
1097 /// ## Pipeline Registration
1098 ///
1099 /// After WGPU initialization, the `register_pipelines_fn` is called to set up
1100 /// all rendering pipelines. This typically includes basic component pipelines
1101 /// and any custom shaders your application requires.
1102 #[tracing::instrument(level = "debug", skip(self, event_loop))]
1103 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
1104 // Just return if the app is already created
1105 if self.app.is_some() {
1106 return;
1107 }
1108
1109 // Create a new window (initially hidden for AccessKit initialization)
1110 let window_attributes = Window::default_attributes()
1111 .with_title(&self.config.window_title)
1112 .with_transparent(true)
1113 .with_visible(false); // Hide initially for AccessKit
1114 let window = Arc::new(event_loop.create_window(window_attributes).unwrap());
1115
1116 // Initialize AccessKit adapter BEFORE showing the window
1117 if let Some(proxy) = self.event_loop_proxy.clone() {
1118 self.accessibility_adapter = Some(AccessKitAdapter::with_event_loop_proxy(
1119 event_loop, &window, proxy,
1120 ));
1121 }
1122
1123 // Now show the window after AccessKit is initialized
1124 window.set_visible(true);
1125
1126 let register_pipelines_fn = self.register_pipelines_fn.clone();
1127
1128 let mut wgpu_app =
1129 pollster::block_on(WgpuApp::new(window.clone(), self.config.sample_count));
1130
1131 // Register pipelines
1132 wgpu_app.register_pipelines(register_pipelines_fn);
1133
1134 self.app = Some(wgpu_app);
1135
1136 #[cfg(target_os = "android")]
1137 {
1138 self.clipboard = Clipboard::new(event_loop.android_app().clone());
1139 }
1140 #[cfg(not(target_os = "android"))]
1141 {
1142 self.clipboard = Clipboard::new();
1143 }
1144 }
1145
1146 /// Called when the application is suspended.
1147 ///
1148 /// This method should handle cleanup and state preservation when the application
1149 /// is being suspended (e.g., on mobile platforms when the app goes to background).
1150 ///
1151 /// ## Platform Considerations
1152 ///
1153 /// - **Desktop**: Rarely called, mainly during shutdown
1154 /// - **Android**: Called when app goes to background
1155 /// - **iOS**: Called during app lifecycle transitions
1156 fn suspended(&mut self, _event_loop: &ActiveEventLoop) {
1157 debug!("Suspending renderer; tearing down WGPU resources.");
1158
1159 if let Some(app) = self.app.take() {
1160 app.resource_manager.write().clear();
1161 }
1162
1163 // Clean up AccessKit adapter
1164 self.accessibility_adapter = None;
1165
1166 self.previous_commands.clear();
1167 self.cursor_state = CursorState::default();
1168 self.keyboard_state = KeyboardState::default();
1169 self.ime_state = ImeState::default();
1170
1171 #[cfg(target_os = "android")]
1172 {
1173 self.android_ime_opened = false;
1174 }
1175
1176 TesseraRuntime::with_mut(|runtime| {
1177 runtime.component_tree.clear();
1178 runtime.cursor_icon_request = None;
1179 runtime.window_minimized = false;
1180 runtime.window_size = [0, 0];
1181 });
1182 }
1183
1184 /// Handles window-specific events from the windowing system.
1185 ///
1186 /// This method processes all window events including user input, window state changes,
1187 /// and rendering requests. It's the main event processing hub that translates winit
1188 /// events into Tessera's internal event system.
1189 ///
1190 /// ## Event Categories
1191 ///
1192 /// ### Window Management
1193 /// - `CloseRequested`: User requested to close the window
1194 /// - `Resized`: Window size changed
1195 /// - `ScaleFactorChanged`: Display scaling changed (high-DPI support)
1196 ///
1197 /// ### Input Events
1198 /// - `CursorMoved`: Mouse cursor position changed
1199 /// - `CursorLeft`: Mouse cursor left the window
1200 /// - `MouseInput`: Mouse button press/release
1201 /// - `MouseWheel`: Mouse wheel scrolling
1202 /// - `Touch`: Touch screen interactions (mobile)
1203 /// - `KeyboardInput`: Keyboard key press/release
1204 /// - `Ime`: Input Method Editor events (international text input)
1205 ///
1206 /// ### Rendering
1207 /// - `RedrawRequested`: System requests a frame to be rendered
1208 ///
1209 /// ## Event Processing Flow
1210 ///
1211 /// 1. **Input Events**: Captured and stored in respective state managers
1212 /// 2. **State Updates**: Internal state (cursor, keyboard, IME) is updated
1213 /// 3. **Rendering**: On redraw requests, the full rendering pipeline is executed
1214 ///
1215 /// ## Platform-Specific Handling
1216 ///
1217 /// Some events have platform-specific behavior, particularly:
1218 /// - Touch events (mobile platforms)
1219 /// - IME events (different implementations per platform)
1220 /// - Scale factor changes (high-DPI displays)
1221 #[tracing::instrument(level = "debug", skip(self, event_loop))]
1222 fn window_event(
1223 &mut self,
1224 event_loop: &ActiveEventLoop,
1225 _window_id: WindowId,
1226 event: WindowEvent,
1227 ) {
1228 // Defer borrowing `app` into specific event handlers to avoid overlapping mutable borrows.
1229 // Handlers will obtain a mutable reference to `self.app` as needed.
1230
1231 // Forward event to AccessKit adapter
1232 if let (Some(adapter), Some(app)) = (&mut self.accessibility_adapter, &self.app) {
1233 adapter.process_event(&app.window, &event);
1234 }
1235
1236 // Handle window events
1237 match event {
1238 WindowEvent::CloseRequested => {
1239 self.handle_close_requested(event_loop);
1240 }
1241 WindowEvent::Resized(size) => {
1242 self.handle_resized(size);
1243 }
1244 WindowEvent::CursorMoved {
1245 device_id: _,
1246 position,
1247 } => {
1248 self.handle_cursor_moved(position);
1249 }
1250 WindowEvent::CursorLeft { device_id: _ } => {
1251 self.handle_cursor_left();
1252 }
1253 WindowEvent::MouseInput {
1254 device_id: _,
1255 state,
1256 button,
1257 } => {
1258 self.handle_mouse_input(state, button);
1259 }
1260 WindowEvent::MouseWheel {
1261 device_id: _,
1262 delta,
1263 phase: _,
1264 } => {
1265 self.handle_mouse_wheel(delta);
1266 }
1267 WindowEvent::Touch(touch_event) => {
1268 self.handle_touch(touch_event);
1269 }
1270 WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
1271 *SCALE_FACTOR.get().unwrap().write() = scale_factor;
1272 }
1273 WindowEvent::KeyboardInput { event, .. } => {
1274 self.handle_keyboard_input(event);
1275 }
1276 WindowEvent::ModifiersChanged(modifiers) => {
1277 debug!("Modifiers changed: {modifiers:?}");
1278 self.keyboard_state.update_modifiers(modifiers.state());
1279 }
1280 WindowEvent::Ime(ime_event) => {
1281 debug!("IME event: {ime_event:?}");
1282 self.ime_state.push_event(ime_event);
1283 }
1284 WindowEvent::RedrawRequested => {
1285 #[cfg(target_os = "android")]
1286 self.handle_redraw_requested(event_loop);
1287 #[cfg(not(target_os = "android"))]
1288 self.handle_redraw_requested();
1289 }
1290 _ => (),
1291 }
1292 }
1293
1294 /// Handles user events sent through the event loop proxy.
1295 ///
1296 /// This method is called when accessibility events are sent from AccessKit.
1297 /// It processes:
1298 /// - `InitialTreeRequested`: Builds and returns the initial accessibility tree
1299 /// - `ActionRequested`: Dispatches accessibility actions to appropriate components
1300 /// - `AccessibilityDeactivated`: Cleans up when accessibility is turned off
1301 fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: AccessKitEvent) {
1302 use accesskit_winit::WindowEvent as AccessKitWindowEvent;
1303
1304 if self.accessibility_adapter.is_none() {
1305 return;
1306 }
1307
1308 match event.window_event {
1309 AccessKitWindowEvent::InitialTreeRequested => {
1310 self.send_accessibility_update();
1311 }
1312 AccessKitWindowEvent::ActionRequested(action_request) => {
1313 println!(
1314 "[tessera-ui][accessibility] Action requested: {:?}",
1315 action_request
1316 );
1317
1318 // Dispatch action to the appropriate component handler
1319 let handled = TesseraRuntime::with(|runtime| {
1320 let tree = runtime.component_tree.tree();
1321 let metadatas = runtime.component_tree.metadatas();
1322
1323 crate::accessibility::dispatch_action(tree, metadatas, action_request)
1324 });
1325
1326 if !handled {
1327 debug!("Action was not handled by any component");
1328 }
1329 }
1330 AccessKitWindowEvent::AccessibilityDeactivated => {
1331 debug!("AccessKit deactivated");
1332 }
1333 }
1334 }
1335}
1336
1337/// Shows the Android soft keyboard (virtual keyboard).
1338///
1339/// This function uses JNI to interact with the Android system to display the soft keyboard.
1340/// It's specifically designed for Android applications and handles the complex JNI calls
1341/// required to show the input method.
1342///
1343/// ## Parameters
1344///
1345/// - `show_implicit`: Whether to show the keyboard implicitly (without explicit user action)
1346/// - `android_app`: Reference to the Android application context
1347///
1348/// ## Platform Support
1349///
1350/// This function is only available on Android (`target_os = "android"`). It will not be
1351/// compiled on other platforms.
1352///
1353/// ## Error Handling
1354///
1355/// The function includes comprehensive error handling for JNI operations. If any JNI
1356/// call fails, the function will return early without crashing the application.
1357/// Exception handling is also included to clear any Java exceptions that might occur.
1358///
1359/// ## Implementation Notes
1360///
1361/// This implementation is based on the android-activity crate and follows the pattern
1362/// established in: https://github.com/rust-mobile/android-activity/pull/178
1363///
1364/// The function performs these steps:
1365/// 1. Get the Java VM and activity context
1366/// 2. Find the InputMethodManager system service
1367/// 3. Get the current window's decor view
1368/// 4. Call `showSoftInput` on the InputMethodManager
1369///
1370/// ## Usage
1371///
1372/// This function is typically called internally by the renderer when IME input is requested.
1373/// You generally don't need to call this directly in application code.
1374// https://github.com/rust-mobile/android-activity/pull/178
1375#[cfg(target_os = "android")]
1376pub fn show_soft_input(show_implicit: bool, android_app: &AndroidApp) {
1377 let ctx = android_app;
1378
1379 let jvm = unsafe { jni::JavaVM::from_raw(ctx.vm_as_ptr().cast()) }.unwrap();
1380 let na = unsafe { jni::objects::JObject::from_raw(ctx.activity_as_ptr().cast()) };
1381
1382 let mut env = jvm.attach_current_thread().unwrap();
1383 if env.exception_check().unwrap() {
1384 return;
1385 }
1386 let class_ctxt = env.find_class("android/content/Context").unwrap();
1387 if env.exception_check().unwrap() {
1388 return;
1389 }
1390 let ims = env
1391 .get_static_field(class_ctxt, "INPUT_METHOD_SERVICE", "Ljava/lang/String;")
1392 .unwrap();
1393 if env.exception_check().unwrap() {
1394 return;
1395 }
1396
1397 let im_manager = env
1398 .call_method(
1399 &na,
1400 "getSystemService",
1401 "(Ljava/lang/String;)Ljava/lang/Object;",
1402 &[(&ims).into()],
1403 )
1404 .unwrap()
1405 .l()
1406 .unwrap();
1407 if env.exception_check().unwrap() {
1408 return;
1409 }
1410
1411 let jni_window = env
1412 .call_method(&na, "getWindow", "()Landroid/view/Window;", &[])
1413 .unwrap()
1414 .l()
1415 .unwrap();
1416 if env.exception_check().unwrap() {
1417 return;
1418 }
1419 let view = env
1420 .call_method(&jni_window, "getDecorView", "()Landroid/view/View;", &[])
1421 .unwrap()
1422 .l()
1423 .unwrap();
1424 if env.exception_check().unwrap() {
1425 return;
1426 }
1427
1428 let _ = env.call_method(
1429 im_manager,
1430 "showSoftInput",
1431 "(Landroid/view/View;I)Z",
1432 &[
1433 jni::objects::JValue::Object(&view),
1434 if show_implicit {
1435 (ndk_sys::ANATIVEACTIVITY_SHOW_SOFT_INPUT_IMPLICIT as i32).into()
1436 } else {
1437 0i32.into()
1438 },
1439 ],
1440 );
1441 // showSoftInput can trigger exceptions if the keyboard is currently animating open/closed
1442 if env.exception_check().unwrap() {
1443 let _ = env.exception_clear();
1444 }
1445}
1446
1447/// Hides the Android soft keyboard (virtual keyboard).
1448///
1449/// This function uses JNI to interact with the Android system to hide the soft keyboard.
1450/// It's the counterpart to [`show_soft_input`] and handles the complex JNI calls required
1451/// to dismiss the input method.
1452///
1453/// ## Parameters
1454///
1455/// - `android_app`: Reference to the Android application context
1456///
1457/// ## Platform Support
1458///
1459/// This function is only available on Android (`target_os = "android"`). It will not be
1460/// compiled on other platforms.
1461///
1462/// ## Error Handling
1463///
1464/// Like [`show_soft_input`], this function includes comprehensive error handling for JNI
1465/// operations. If any step fails, the function returns early without crashing. Java
1466/// exceptions are also properly handled and cleared.
1467///
1468/// ## Implementation Details
1469///
1470/// The function performs these steps:
1471/// 1. Get the Java VM and activity context
1472/// 2. Find the InputMethodManager system service
1473/// 3. Get the current window and its decor view
1474/// 4. Get the window token from the decor view
1475/// 5. Call `hideSoftInputFromWindow` on the InputMethodManager
1476///
1477/// ## Usage
1478///
1479/// This function is typically called internally by the renderer when IME input is no longer
1480/// needed. You generally don't need to call this directly in application code.
1481///
1482/// ## Relationship to show_soft_input
1483///
1484/// This function is designed to work in tandem with [`show_soft_input`]. The renderer
1485/// automatically manages the keyboard visibility based on IME requests from components.
1486#[cfg(target_os = "android")]
1487pub fn hide_soft_input(android_app: &AndroidApp) {
1488 use jni::objects::JValue;
1489
1490 let ctx = android_app;
1491 let jvm = match unsafe { jni::JavaVM::from_raw(ctx.vm_as_ptr().cast()) } {
1492 Ok(jvm) => jvm,
1493 Err(_) => return, // Early exit if failing to get the JVM
1494 };
1495 let activity = unsafe { jni::objects::JObject::from_raw(ctx.activity_as_ptr().cast()) };
1496
1497 let mut env = match jvm.attach_current_thread() {
1498 Ok(env) => env,
1499 Err(_) => return,
1500 };
1501
1502 // --- 1. Get the InputMethodManager ---
1503 // This part is the same as in show_soft_input.
1504 let class_ctxt = match env.find_class("android/content/Context") {
1505 Ok(c) => c,
1506 Err(_) => return,
1507 };
1508 let ims_field =
1509 match env.get_static_field(class_ctxt, "INPUT_METHOD_SERVICE", "Ljava/lang/String;") {
1510 Ok(f) => f,
1511 Err(_) => return,
1512 };
1513 let ims = match ims_field.l() {
1514 Ok(s) => s,
1515 Err(_) => return,
1516 };
1517
1518 let im_manager = match env.call_method(
1519 &activity,
1520 "getSystemService",
1521 "(Ljava/lang/String;)Ljava/lang/Object;",
1522 &[(&ims).into()],
1523 ) {
1524 Ok(m) => match m.l() {
1525 Ok(im) => im,
1526 Err(_) => return,
1527 },
1528 Err(_) => return,
1529 };
1530
1531 // --- 2. Get the current window's token ---
1532 // This is the key step that differs from show_soft_input.
1533 let window = match env.call_method(&activity, "getWindow", "()Landroid/view/Window;", &[]) {
1534 Ok(w) => match w.l() {
1535 Ok(win) => win,
1536 Err(_) => return,
1537 },
1538 Err(_) => return,
1539 };
1540
1541 let decor_view = match env.call_method(&window, "getDecorView", "()Landroid/view/View;", &[]) {
1542 Ok(v) => match v.l() {
1543 Ok(view) => view,
1544 Err(_) => return,
1545 },
1546 Err(_) => return,
1547 };
1548
1549 let window_token =
1550 match env.call_method(&decor_view, "getWindowToken", "()Landroid/os/IBinder;", &[]) {
1551 Ok(t) => match t.l() {
1552 Ok(token) => token,
1553 Err(_) => return,
1554 },
1555 Err(_) => return,
1556 };
1557
1558 // --- 3. Call hideSoftInputFromWindow ---
1559 let _ = env.call_method(
1560 &im_manager,
1561 "hideSoftInputFromWindow",
1562 "(Landroid/os/IBinder;I)Z",
1563 &[
1564 JValue::Object(&window_token),
1565 JValue::Int(0), // flags, usually 0
1566 ],
1567 );
1568
1569 // Hiding the keyboard can also cause exceptions, so we clear them.
1570 if env.exception_check().unwrap_or(false) {
1571 let _ = env.exception_clear();
1572 }
1573}
1574
1575/// Entry point wrapper for tessera applications.
1576///
1577/// # Why this is needed
1578///
1579/// Tessera component entry points must be functions annotated with the `tessera` macro.
1580/// Unlike some other frameworks, we cannot detect whether a provided closure has been
1581/// annotated with `tessera`. Wrapping the entry function guarantees it is invoked from
1582/// a `tessera`-annotated function, ensuring correct behavior regardless of how the user
1583/// supplied their entry point.
1584#[tessera(crate)]
1585fn entry_wrapper(entry: impl Fn()) {
1586 entry();
1587}