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