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 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 - default)
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 #[cfg(target_os = "android")]
321 /// Android-specific state tracking whether the soft keyboard is currently open
322 android_ime_opened: bool,
323}
324
325impl<F: Fn(), R: Fn(&mut WgpuApp) + Clone + 'static> Renderer<F, R> {
326 #[cfg(not(target_os = "android"))]
327 /// Runs the Tessera application with default configuration on desktop platforms.
328 ///
329 /// This is the most convenient way to start a Tessera application on Windows, Linux, or macOS.
330 /// It uses the default [`TesseraConfig`] settings (4x MSAA).
331 ///
332 /// # Parameters
333 ///
334 /// - `entry_point`: A function that defines your UI. This function will be called every frame
335 /// to build the component tree. It should contain your root UI components.
336 /// - `register_pipelines_fn`: A function that registers rendering pipelines with the WGPU app.
337 /// Typically, you'll call `tessera_ui_basic_components::pipelines::register_pipelines(app)` here.
338 ///
339 /// # Returns
340 ///
341 /// Returns `Ok(())` when the application exits normally, or an `EventLoopError` if the
342 /// event loop fails to start or encounters a critical error.
343 ///
344 /// # Examples
345 ///
346 /// ```rust,no_run
347 /// use tessera_ui::Renderer;
348 ///
349 /// fn my_ui() {
350 /// // Your UI components go here
351 /// }
352 ///
353 /// fn main() -> Result<(), Box<dyn std::error::Error>> {
354 /// Renderer::run(
355 /// my_ui,
356 /// |_app| {
357 /// // Register your rendering pipelines here
358 /// // tessera_ui_basic_components::pipelines::register_pipelines(app);
359 /// }
360 /// )?;
361 /// Ok(())
362 /// }
363 /// ```
364 pub fn run(entry_point: F, register_pipelines_fn: R) -> Result<(), EventLoopError> {
365 Self::run_with_config(entry_point, register_pipelines_fn, Default::default())
366 }
367
368 #[cfg(not(target_os = "android"))]
369 /// Runs the Tessera application with custom configuration on desktop platforms.
370 ///
371 /// This method allows you to customize the renderer behavior through [`TesseraConfig`].
372 /// Use this when you need to adjust settings like MSAA sample count or other rendering parameters.
373 ///
374 /// # Parameters
375 ///
376 /// - `entry_point`: A function that defines your UI
377 /// - `register_pipelines_fn`: A function that registers rendering pipelines
378 /// - `config`: Custom configuration for the renderer
379 ///
380 /// # Returns
381 ///
382 /// Returns `Ok(())` when the application exits normally, or an `EventLoopError` if the
383 /// event loop fails to start.
384 ///
385 /// # Examples
386 ///
387 /// ```rust,no_run
388 /// use tessera_ui::{Renderer, renderer::TesseraConfig};
389 ///
390 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
391 /// let config = TesseraConfig {
392 /// sample_count: 8, // 8x MSAA for higher quality
393 /// };
394 ///
395 /// Renderer::run_with_config(
396 /// || { /* my_ui */ },
397 /// |_app| { /* register_pipelines */ },
398 /// config
399 /// )?;
400 /// # Ok(())
401 /// # }
402 /// ```
403 pub fn run_with_config(
404 entry_point: F,
405 register_pipelines_fn: R,
406 config: TesseraConfig,
407 ) -> Result<(), EventLoopError> {
408 let event_loop = EventLoop::new().unwrap();
409 let app = None;
410 let cursor_state = CursorState::default();
411 let keyboard_state = KeyboardState::default();
412 let ime_state = ImeState::default();
413 let mut renderer = Self {
414 app,
415 entry_point,
416 cursor_state,
417 keyboard_state,
418 register_pipelines_fn,
419 ime_state,
420 config,
421 };
422 thread_utils::set_thread_name("Tessera Renderer");
423 event_loop.run_app(&mut renderer)
424 }
425
426 #[cfg(target_os = "android")]
427 /// Runs the Tessera application with default configuration on Android.
428 ///
429 /// This method is specifically for Android applications and requires an `AndroidApp` instance
430 /// that is typically provided by the `android_main` function.
431 ///
432 /// # Parameters
433 ///
434 /// - `entry_point`: A function that defines your UI
435 /// - `register_pipelines_fn`: A function that registers rendering pipelines
436 /// - `android_app`: The Android application context
437 ///
438 /// # Returns
439 ///
440 /// Returns `Ok(())` when the application exits normally, or an `EventLoopError` if the
441 /// event loop fails to start.
442 ///
443 /// # Examples
444 ///
445 /// ```rust,no_run
446 /// use tessera_ui::Renderer;
447 /// use winit::platform::android::activity::AndroidApp;
448 ///
449 /// fn my_ui() {}
450 /// fn register_pipelines(_: &mut tessera_ui::renderer::WgpuApp) {}
451 ///
452 /// #[unsafe(no_mangle)]
453 /// fn android_main(android_app: AndroidApp) {
454 /// Renderer::run(
455 /// my_ui,
456 /// register_pipelines,
457 /// android_app
458 /// ).unwrap();
459 /// }
460 /// ```
461 pub fn run(
462 entry_point: F,
463 register_pipelines_fn: R,
464 android_app: AndroidApp,
465 ) -> Result<(), EventLoopError> {
466 Self::run_with_config(
467 entry_point,
468 register_pipelines_fn,
469 android_app,
470 Default::default(),
471 )
472 }
473
474 #[cfg(target_os = "android")]
475 /// Runs the Tessera application with custom configuration on Android.
476 ///
477 /// This method allows you to customize the renderer behavior on Android through [`TesseraConfig`].
478 ///
479 /// # Parameters
480 ///
481 /// - `entry_point`: A function that defines your UI
482 /// - `register_pipelines_fn`: A function that registers rendering pipelines
483 /// - `android_app`: The Android application context
484 /// - `config`: Custom configuration for the renderer
485 ///
486 /// # Returns
487 ///
488 /// Returns `Ok(())` when the application exits normally, or an `EventLoopError` if the
489 /// event loop fails to start.
490 ///
491 /// # Examples
492 ///
493 /// ```rust,no_run
494 /// use tessera_ui::{Renderer, renderer::TesseraConfig};
495 /// use winit::platform::android::activity::AndroidApp;
496 ///
497 /// fn my_ui() {}
498 /// fn register_pipelines(_: &mut tessera_ui::renderer::WgpuApp) {}
499 ///
500 /// #[unsafe(no_mangle)]
501 /// fn android_main(android_app: AndroidApp) {
502 /// let config = TesseraConfig {
503 /// sample_count: 2, // Lower MSAA for mobile performance
504 /// };
505 ///
506 /// Renderer::run_with_config(
507 /// my_ui,
508 /// register_pipelines,
509 /// android_app,
510 /// config
511 /// ).unwrap();
512 /// }
513 /// ```
514 pub fn run_with_config(
515 entry_point: F,
516 register_pipelines_fn: R,
517 android_app: AndroidApp,
518 config: TesseraConfig,
519 ) -> Result<(), EventLoopError> {
520 let event_loop = EventLoop::builder()
521 .with_android_app(android_app)
522 .build()
523 .unwrap();
524 let app = None;
525 let cursor_state = CursorState::default();
526 let keyboard_state = KeyboardState::default();
527 let ime_state = ImeState::default();
528 let mut renderer = Self {
529 app,
530 entry_point,
531 cursor_state,
532 keyboard_state,
533 register_pipelines_fn,
534 ime_state,
535 android_ime_opened: false,
536 config,
537 };
538 thread_utils::set_thread_name("Tessera Renderer");
539 event_loop.run_app(&mut renderer)
540 }
541}
542
543impl<F: Fn(), R: Fn(&mut WgpuApp) + Clone + 'static> Renderer<F, R> {
544 /// Executes a single frame rendering cycle.
545 ///
546 /// This is the core rendering method that orchestrates the entire frame rendering process.
547 /// It follows a three-phase approach:
548 ///
549 /// 1. **Component Tree Building**: Calls the entry point function to build the UI component tree
550 /// 2. **Draw Command Computation**: Processes the component tree to generate rendering commands
551 /// 3. **Surface Rendering**: Executes the commands to render the final frame
552 ///
553 /// ## Performance Monitoring
554 ///
555 /// This method includes built-in performance monitoring that logs detailed timing information
556 /// when frame rates drop below 60 FPS, helping identify performance bottlenecks.
557 ///
558 /// ## Parameters
559 ///
560 /// - `entry_point`: The UI entry point function to build the component tree
561 /// - `cursor_state`: Mutable reference to cursor/mouse state for event processing
562 /// - `keyboard_state`: Mutable reference to keyboard state for event processing
563 /// - `ime_state`: Mutable reference to IME state for text input processing
564 /// - `android_ime_opened`: (Android only) Tracks soft keyboard state
565 /// - `app`: Mutable reference to the WGPU application context
566 /// - `event_loop`: (Android only) Event loop for IME management
567 ///
568 /// ## Frame Timing Breakdown
569 ///
570 /// - **Build Tree Cost**: Time spent building the component tree
571 /// - **Draw Commands Cost**: Time spent computing rendering commands
572 /// - **Render Cost**: Time spent executing GPU rendering commands
573 ///
574 /// ## Thread Safety
575 ///
576 /// This method runs on the main thread but coordinates with other threads for
577 /// component tree processing and resource management.
578 fn execute_render_frame(
579 entry_point: &F,
580 cursor_state: &mut CursorState,
581 keyboard_state: &mut KeyboardState,
582 ime_state: &mut ImeState,
583 #[cfg(target_os = "android")] android_ime_opened: &mut bool,
584 app: &mut WgpuApp,
585 #[cfg(target_os = "android")] event_loop: &ActiveEventLoop,
586 ) {
587 // notify the windowing system before rendering
588 // this will help winit to properly schedule and make assumptions about its internal state
589 app.window.pre_present_notify();
590 // and tell runtime the new size
591 TesseraRuntime::write().window_size = app.size().into();
592 // render the surface
593 // timer for performance measurement
594 let tree_timer = Instant::now();
595 // build the component tree
596 debug!("Building component tree...");
597 entry_point();
598 let build_tree_cost = tree_timer.elapsed();
599 debug!("Component tree built in {build_tree_cost:?}");
600 // get the component tree from the runtime
601 let component_tree = &mut TesseraRuntime::write().component_tree;
602 // timer for performance measurement
603 let draw_timer = Instant::now();
604 // Compute the draw commands then we can clear component tree for next build
605 debug!("Computing draw commands...");
606 let cursor_position = cursor_state.position();
607 let cursor_events = cursor_state.take_events();
608 let keyboard_events = keyboard_state.take_events();
609 let ime_events = ime_state.take_events();
610 let screen_size: PxSize = app.size().into();
611 // Clear any existing compute resources
612 app.resource_manager.write().clear();
613 // Compute the draw commands
614 let (commands, window_requests) = component_tree.compute(
615 screen_size,
616 cursor_position,
617 cursor_events,
618 keyboard_events,
619 ime_events,
620 app.resource_manager.clone(),
621 &app.gpu,
622 );
623 let draw_cost = draw_timer.elapsed();
624 debug!("Draw commands computed in {draw_cost:?}");
625 component_tree.clear();
626 // Handle the window requests
627 // After compute, check for cursor change requests
628 app.window
629 .set_cursor(winit::window::Cursor::Icon(window_requests.cursor_icon));
630 // Handle IME requests
631 if let Some(ime_request) = window_requests.ime_request {
632 app.window.set_ime_allowed(true);
633 #[cfg(target_os = "android")]
634 {
635 if !*android_ime_opened {
636 show_soft_input(true, event_loop.android_app());
637 *android_ime_opened = true;
638 }
639 }
640 app.window.set_ime_cursor_area::<PxPosition, PxSize>(
641 ime_request.position.unwrap(),
642 ime_request.size,
643 );
644 } else {
645 app.window.set_ime_allowed(false);
646 #[cfg(target_os = "android")]
647 {
648 if *android_ime_opened {
649 hide_soft_input(event_loop.android_app());
650 *android_ime_opened = false;
651 }
652 }
653 }
654 // timer for performance measurement
655 let render_timer = Instant::now();
656 // Render the commands
657 debug!("Rendering draw commands...");
658 // Render the commands to the surface
659 app.render(commands).unwrap();
660 let render_cost = render_timer.elapsed();
661 debug!("Rendered to surface in {render_cost:?}");
662
663 // print frame statistics
664 let fps = 1.0 / (build_tree_cost + draw_cost + render_cost).as_secs_f32();
665 if fps < 60.0 {
666 warn!(
667 "Jank detected! Frame statistics:
668 Build tree cost: {:?}
669 Draw commands cost: {:?}
670 Render cost: {:?}
671 Total frame cost: {:?}
672 Fps: {:.2}
673",
674 build_tree_cost,
675 draw_cost,
676 render_cost,
677 build_tree_cost + draw_cost + render_cost,
678 1.0 / (build_tree_cost + draw_cost + render_cost).as_secs_f32()
679 );
680 }
681
682 // Currently we render every frame
683 app.window.request_redraw();
684 }
685}
686
687/// Implementation of winit's `ApplicationHandler` trait for the Tessera renderer.
688///
689/// This implementation handles the application lifecycle events from winit, including
690/// window creation, suspension/resumption, and various window events. It bridges the
691/// gap between winit's event system and Tessera's component-based UI framework.
692impl<F: Fn(), R: Fn(&mut WgpuApp) + Clone + 'static> ApplicationHandler for Renderer<F, R> {
693 /// Called when the application is resumed or started.
694 ///
695 /// This method is responsible for:
696 /// - Creating the application window with appropriate attributes
697 /// - Initializing the WGPU context and surface
698 /// - Registering rendering pipelines
699 /// - Setting up the initial application state
700 ///
701 /// On desktop platforms, this is typically called once at startup.
702 /// On mobile platforms (especially Android), this may be called multiple times
703 /// as the app is suspended and resumed.
704 ///
705 /// ## Window Configuration
706 ///
707 /// The window is created with:
708 /// - Title: "Tessera"
709 /// - Transparency: Enabled (allows for transparent backgrounds)
710 /// - Default size and position (platform-dependent)
711 ///
712 /// ## Pipeline Registration
713 ///
714 /// After WGPU initialization, the `register_pipelines_fn` is called to set up
715 /// all rendering pipelines. This typically includes basic component pipelines
716 /// and any custom shaders your application requires.
717 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
718 // Just return if the app is already created
719 if self.app.is_some() {
720 return;
721 }
722
723 // Create a new window
724 let window_attributes = Window::default_attributes()
725 .with_title("Tessera")
726 .with_transparent(true);
727 let window = Arc::new(event_loop.create_window(window_attributes).unwrap());
728 let register_pipelines_fn = self.register_pipelines_fn.clone();
729
730 let mut wgpu_app =
731 tokio_runtime::get().block_on(WgpuApp::new(window, self.config.sample_count));
732
733 // Register pipelines
734 wgpu_app.register_pipelines(register_pipelines_fn);
735
736 self.app = Some(wgpu_app);
737 }
738
739 /// Called when the application is suspended.
740 ///
741 /// This method should handle cleanup and state preservation when the application
742 /// is being suspended (e.g., on mobile platforms when the app goes to background).
743 ///
744 /// ## Current Status
745 ///
746 /// This method is currently not fully implemented (`todo!`). In a complete
747 /// implementation, it should:
748 /// - Save application state
749 /// - Release GPU resources if necessary
750 /// - Prepare for potential termination
751 ///
752 /// ## Platform Considerations
753 ///
754 /// - **Desktop**: Rarely called, mainly during shutdown
755 /// - **Android**: Called when app goes to background
756 /// - **iOS**: Called during app lifecycle transitions
757 fn suspended(&mut self, _event_loop: &ActiveEventLoop) {
758 todo!("Handle suspend event");
759 }
760
761 /// Handles window-specific events from the windowing system.
762 ///
763 /// This method processes all window events including user input, window state changes,
764 /// and rendering requests. It's the main event processing hub that translates winit
765 /// events into Tessera's internal event system.
766 ///
767 /// ## Event Categories
768 ///
769 /// ### Window Management
770 /// - `CloseRequested`: User requested to close the window
771 /// - `Resized`: Window size changed
772 /// - `ScaleFactorChanged`: Display scaling changed (high-DPI support)
773 ///
774 /// ### Input Events
775 /// - `CursorMoved`: Mouse cursor position changed
776 /// - `CursorLeft`: Mouse cursor left the window
777 /// - `MouseInput`: Mouse button press/release
778 /// - `MouseWheel`: Mouse wheel scrolling
779 /// - `Touch`: Touch screen interactions (mobile)
780 /// - `KeyboardInput`: Keyboard key press/release
781 /// - `Ime`: Input Method Editor events (international text input)
782 ///
783 /// ### Rendering
784 /// - `RedrawRequested`: System requests a frame to be rendered
785 ///
786 /// ## Event Processing Flow
787 ///
788 /// 1. **Input Events**: Captured and stored in respective state managers
789 /// 2. **State Updates**: Internal state (cursor, keyboard, IME) is updated
790 /// 3. **Rendering**: On redraw requests, the full rendering pipeline is executed
791 ///
792 /// ## Platform-Specific Handling
793 ///
794 /// Some events have platform-specific behavior, particularly:
795 /// - Touch events (mobile platforms)
796 /// - IME events (different implementations per platform)
797 /// - Scale factor changes (high-DPI displays)
798 fn window_event(
799 &mut self,
800 event_loop: &ActiveEventLoop,
801 _window_id: WindowId,
802 event: WindowEvent,
803 ) {
804 let app = match self.app.as_mut() {
805 Some(app) => app,
806 None => return,
807 };
808
809 // Handle window events
810 match event {
811 WindowEvent::CloseRequested => {
812 event_loop.exit();
813 }
814 WindowEvent::Resized(size) => {
815 if size.width == 0 || size.height == 0 {
816 todo!("Handle minimize");
817 } else {
818 app.resize(size);
819 }
820 }
821 WindowEvent::CursorMoved {
822 device_id: _,
823 position,
824 } => {
825 // Update cursor position
826 self.cursor_state
827 .update_position(PxPosition::from_f64_arr2([position.x, position.y]));
828 debug!("Cursor moved to: {}, {}", position.x, position.y);
829 }
830 WindowEvent::CursorLeft { device_id: _ } => {
831 // Clear cursor position when it leaves the window
832 // This also set the position to None
833 self.cursor_state.clear();
834 debug!("Cursor left the window");
835 }
836 WindowEvent::MouseInput {
837 device_id: _,
838 state,
839 button,
840 } => {
841 let Some(event_content) = CursorEventContent::from_press_event(state, button)
842 else {
843 return; // Ignore unsupported buttons
844 };
845 let event = CursorEvent {
846 timestamp: Instant::now(),
847 content: event_content,
848 };
849 self.cursor_state.push_event(event);
850 debug!("Mouse input: {state:?} button {button:?}");
851 }
852 WindowEvent::MouseWheel {
853 device_id: _,
854 delta,
855 phase: _,
856 } => {
857 let event_content = CursorEventContent::from_scroll_event(delta);
858 let event = CursorEvent {
859 timestamp: Instant::now(),
860 content: event_content,
861 };
862 self.cursor_state.push_event(event);
863 debug!("Mouse scroll: {delta:?}");
864 }
865 WindowEvent::Touch(touch_event) => {
866 let pos =
867 PxPosition::from_f64_arr2([touch_event.location.x, touch_event.location.y]);
868 debug!(
869 "Touch event: id {}, phase {:?}, position {:?}",
870 touch_event.id, touch_event.phase, pos
871 );
872 match touch_event.phase {
873 winit::event::TouchPhase::Started => {
874 // Use new touch start handling method
875 self.cursor_state.handle_touch_start(touch_event.id, pos);
876 }
877 winit::event::TouchPhase::Moved => {
878 // Use new touch move handling method, may generate scroll event
879 if let Some(scroll_event) =
880 self.cursor_state.handle_touch_move(touch_event.id, pos)
881 {
882 // Scroll event is already added to event queue in handle_touch_move
883 self.cursor_state.push_event(scroll_event);
884 }
885 }
886 winit::event::TouchPhase::Ended | winit::event::TouchPhase::Cancelled => {
887 // Use new touch end handling method
888 self.cursor_state.handle_touch_end(touch_event.id);
889 }
890 }
891 }
892 WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
893 *SCALE_FACTOR.get().unwrap().write() = scale_factor;
894 }
895 WindowEvent::KeyboardInput { event, .. } => {
896 debug!("Keyboard input: {event:?}");
897 self.keyboard_state.push_event(event);
898 }
899 WindowEvent::Ime(ime_event) => {
900 debug!("IME event: {ime_event:?}");
901 self.ime_state.push_event(ime_event);
902 }
903 WindowEvent::RedrawRequested => {
904 app.resize_if_needed();
905 Self::execute_render_frame(
906 &self.entry_point,
907 &mut self.cursor_state,
908 &mut self.keyboard_state,
909 &mut self.ime_state,
910 #[cfg(target_os = "android")]
911 &mut self.android_ime_opened,
912 app,
913 #[cfg(target_os = "android")]
914 event_loop,
915 );
916 }
917 _ => (),
918 }
919 }
920}
921
922/// Shows the Android soft keyboard (virtual keyboard).
923///
924/// This function uses JNI to interact with the Android system to display the soft keyboard.
925/// It's specifically designed for Android applications and handles the complex JNI calls
926/// required to show the input method.
927///
928/// ## Parameters
929///
930/// - `show_implicit`: Whether to show the keyboard implicitly (without explicit user action)
931/// - `android_app`: Reference to the Android application context
932///
933/// ## Platform Support
934///
935/// This function is only available on Android (`target_os = "android"`). It will not be
936/// compiled on other platforms.
937///
938/// ## Error Handling
939///
940/// The function includes comprehensive error handling for JNI operations. If any JNI
941/// call fails, the function will return early without crashing the application.
942/// Exception handling is also included to clear any Java exceptions that might occur.
943///
944/// ## Implementation Notes
945///
946/// This implementation is based on the android-activity crate and follows the pattern
947/// established in: https://github.com/rust-mobile/android-activity/pull/178
948///
949/// The function performs these steps:
950/// 1. Get the Java VM and activity context
951/// 2. Find the InputMethodManager system service
952/// 3. Get the current window's decor view
953/// 4. Call `showSoftInput` on the InputMethodManager
954///
955/// ## Usage
956///
957/// This function is typically called internally by the renderer when IME input is requested.
958/// You generally don't need to call this directly in application code.
959// https://github.com/rust-mobile/android-activity/pull/178
960#[cfg(target_os = "android")]
961pub fn show_soft_input(show_implicit: bool, android_app: &AndroidApp) {
962 let ctx = android_app;
963
964 let jvm = unsafe { jni::JavaVM::from_raw(ctx.vm_as_ptr().cast()) }.unwrap();
965 let na = unsafe { jni::objects::JObject::from_raw(ctx.activity_as_ptr().cast()) };
966
967 let mut env = jvm.attach_current_thread().unwrap();
968 if env.exception_check().unwrap() {
969 return;
970 }
971 let class_ctxt = env.find_class("android/content/Context").unwrap();
972 if env.exception_check().unwrap() {
973 return;
974 }
975 let ims = env
976 .get_static_field(class_ctxt, "INPUT_METHOD_SERVICE", "Ljava/lang/String;")
977 .unwrap();
978 if env.exception_check().unwrap() {
979 return;
980 }
981
982 let im_manager = env
983 .call_method(
984 &na,
985 "getSystemService",
986 "(Ljava/lang/String;)Ljava/lang/Object;",
987 &[(&ims).into()],
988 )
989 .unwrap()
990 .l()
991 .unwrap();
992 if env.exception_check().unwrap() {
993 return;
994 }
995
996 let jni_window = env
997 .call_method(&na, "getWindow", "()Landroid/view/Window;", &[])
998 .unwrap()
999 .l()
1000 .unwrap();
1001 if env.exception_check().unwrap() {
1002 return;
1003 }
1004 let view = env
1005 .call_method(&jni_window, "getDecorView", "()Landroid/view/View;", &[])
1006 .unwrap()
1007 .l()
1008 .unwrap();
1009 if env.exception_check().unwrap() {
1010 return;
1011 }
1012
1013 let _ = env.call_method(
1014 im_manager,
1015 "showSoftInput",
1016 "(Landroid/view/View;I)Z",
1017 &[
1018 jni::objects::JValue::Object(&view),
1019 if show_implicit {
1020 (ndk_sys::ANATIVEACTIVITY_SHOW_SOFT_INPUT_IMPLICIT as i32).into()
1021 } else {
1022 0i32.into()
1023 },
1024 ],
1025 );
1026 // showSoftInput can trigger exceptions if the keyboard is currently animating open/closed
1027 if env.exception_check().unwrap() {
1028 let _ = env.exception_clear();
1029 }
1030}
1031
1032/// Hides the Android soft keyboard (virtual keyboard).
1033///
1034/// This function uses JNI to interact with the Android system to hide the soft keyboard.
1035/// It's the counterpart to [`show_soft_input`] and handles the complex JNI calls required
1036/// to dismiss the input method.
1037///
1038/// ## Parameters
1039///
1040/// - `android_app`: Reference to the Android application context
1041///
1042/// ## Platform Support
1043///
1044/// This function is only available on Android (`target_os = "android"`). It will not be
1045/// compiled on other platforms.
1046///
1047/// ## Error Handling
1048///
1049/// Like [`show_soft_input`], this function includes comprehensive error handling for JNI
1050/// operations. If any step fails, the function returns early without crashing. Java
1051/// exceptions are also properly handled and cleared.
1052///
1053/// ## Implementation Details
1054///
1055/// The function performs these steps:
1056/// 1. Get the Java VM and activity context
1057/// 2. Find the InputMethodManager system service
1058/// 3. Get the current window and its decor view
1059/// 4. Get the window token from the decor view
1060/// 5. Call `hideSoftInputFromWindow` on the InputMethodManager
1061///
1062/// ## Usage
1063///
1064/// This function is typically called internally by the renderer when IME input is no longer
1065/// needed. You generally don't need to call this directly in application code.
1066///
1067/// ## Relationship to show_soft_input
1068///
1069/// This function is designed to work in tandem with [`show_soft_input`]. The renderer
1070/// automatically manages the keyboard visibility based on IME requests from components.
1071#[cfg(target_os = "android")]
1072pub fn hide_soft_input(android_app: &AndroidApp) {
1073 use jni::objects::JValue;
1074
1075 let ctx = android_app;
1076 let jvm = match unsafe { jni::JavaVM::from_raw(ctx.vm_as_ptr().cast()) } {
1077 Ok(jvm) => jvm,
1078 Err(_) => return, // Early exit if failing to get the JVM
1079 };
1080 let activity = unsafe { jni::objects::JObject::from_raw(ctx.activity_as_ptr().cast()) };
1081
1082 let mut env = match jvm.attach_current_thread() {
1083 Ok(env) => env,
1084 Err(_) => return,
1085 };
1086
1087 // --- 1. Get the InputMethodManager ---
1088 // This part is the same as in show_soft_input.
1089 let class_ctxt = match env.find_class("android/content/Context") {
1090 Ok(c) => c,
1091 Err(_) => return,
1092 };
1093 let ims_field =
1094 match env.get_static_field(class_ctxt, "INPUT_METHOD_SERVICE", "Ljava/lang/String;") {
1095 Ok(f) => f,
1096 Err(_) => return,
1097 };
1098 let ims = match ims_field.l() {
1099 Ok(s) => s,
1100 Err(_) => return,
1101 };
1102
1103 let im_manager = match env.call_method(
1104 &activity,
1105 "getSystemService",
1106 "(Ljava/lang/String;)Ljava/lang/Object;",
1107 &[(&ims).into()],
1108 ) {
1109 Ok(m) => match m.l() {
1110 Ok(im) => im,
1111 Err(_) => return,
1112 },
1113 Err(_) => return,
1114 };
1115
1116 // --- 2. Get the current window's token ---
1117 // This is the key step that differs from show_soft_input.
1118 let window = match env.call_method(&activity, "getWindow", "()Landroid/view/Window;", &[]) {
1119 Ok(w) => match w.l() {
1120 Ok(win) => win,
1121 Err(_) => return,
1122 },
1123 Err(_) => return,
1124 };
1125
1126 let decor_view = match env.call_method(&window, "getDecorView", "()Landroid/view/View;", &[]) {
1127 Ok(v) => match v.l() {
1128 Ok(view) => view,
1129 Err(_) => return,
1130 },
1131 Err(_) => return,
1132 };
1133
1134 let window_token =
1135 match env.call_method(&decor_view, "getWindowToken", "()Landroid/os/IBinder;", &[]) {
1136 Ok(t) => match t.l() {
1137 Ok(token) => token,
1138 Err(_) => return,
1139 },
1140 Err(_) => return,
1141 };
1142
1143 // --- 3. Call hideSoftInputFromWindow ---
1144 let _ = env.call_method(
1145 &im_manager,
1146 "hideSoftInputFromWindow",
1147 "(Landroid/os/IBinder;I)Z",
1148 &[
1149 JValue::Object(&window_token),
1150 JValue::Int(0), // flags, usually 0
1151 ],
1152 );
1153
1154 // Hiding the keyboard can also cause exceptions, so we clear them.
1155 if env.exception_check().unwrap_or(false) {
1156 let _ = env.exception_clear();
1157 }
1158}