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