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