1pub mod compute;
6pub mod core;
7pub mod drawer;
8
9#[cfg(target_os = "macos")]
10mod macos_titlebar;
11
12use std::{
13 cell::Cell,
14 sync::{
15 Arc,
16 atomic::{AtomicBool, Ordering},
17 },
18};
19
20pub use core::{RenderCore, RenderResources};
21
22use accesskit::{self, TreeUpdate};
23use accesskit_winit::{Adapter as AccessKitAdapter, Event as AccessKitEvent};
24use parking_lot::RwLock;
25use tracing::{debug, error, instrument, warn};
26use winit::{
27 application::ApplicationHandler,
28 error::EventLoopError,
29 event::WindowEvent,
30 event_loop::{ActiveEventLoop, ControlFlow, EventLoop},
31 window::{ResizeDirection, Window, WindowId},
32};
33
34use crate::{
35 ImeRequest, ImeState, PxPosition, ScrollEventContent,
36 build_tree::build_component_tree,
37 component_tree::{
38 LayoutFrameDiagnostics, WindowDragRequest, WindowRequests, clear_layout_snapshots,
39 },
40 context::reset_context_read_dependencies,
41 cursor::{
42 CursorEventContent, CursorState, GestureState, MOUSE_POINTER_ID, PointerChange,
43 PressKeyEventType,
44 },
45 dp::set_scale_factor,
46 focus::{FocusDirection, flush_pending_focus_callbacks},
47 keyboard_state::KeyboardState,
48 pipeline_context::PipelineContext,
49 plugin::{DesktopPlatformContext, DesktopWindowAction, PluginContext, PluginHost},
50 px::PxSize,
51 render_graph::RenderGraph,
52 render_module::RenderModule,
53 runtime::{
54 TesseraRuntime, begin_frame_build_invalidations, begin_frame_clock,
55 clear_persistent_focus_handles, clear_redraw_waker, has_pending_build_invalidations,
56 has_pending_frame_nanos_receivers, has_pending_layout_invalidations,
57 has_pending_persistent_focus_prune, has_ready_build_invalidations, install_redraw_waker,
58 prune_persistent_focus_handles, reset_build_invalidations, reset_component_replay_tracking,
59 reset_focus_read_dependencies, reset_frame_clock, reset_layout_dirty_tracking,
60 reset_render_slot_read_dependencies, reset_state_read_dependencies,
61 take_layout_dirty_nodes, tick_frame_nanos_receivers,
62 },
63 thread_utils,
64 time::Instant,
65};
66
67pub use crate::render_scene::{Command, DrawRegion, PaddingRect, SampleRegion};
68
69use self::core::RenderTimingBreakdown;
70
71pub use compute::{
72 ComputablePipeline, ComputeBatchItem, ComputePipelineRegistry, ErasedComputeBatchItem,
73};
74pub use drawer::{DrawCommand, DrawablePipeline, PipelineRegistry};
75
76#[cfg(feature = "debug-dirty-overlay")]
77use crate::PxRect;
78#[cfg(feature = "debug-dirty-overlay")]
79use crate::build_tree::{BuildTreeMode, BuildTreeResult};
80
81#[cfg(feature = "profiling")]
82use crate::profiler::{
83 FrameMeta, Phase as ProfilerPhase, RedrawReason, RuntimeEventKind, RuntimeMeta,
84 ScopeGuard as ProfilerScopeGuard, WakeMeta, WakeSource, begin_frame as profiler_begin_frame,
85 end_frame as profiler_end_frame, submit_frame_meta, submit_runtime_meta, submit_wake_meta,
86};
87#[cfg(feature = "profiling")]
88use crate::runtime::frame_delta;
89#[cfg(feature = "profiling")]
90use std::collections::BTreeSet;
91#[cfg(feature = "profiling")]
92use std::path::PathBuf;
93
94#[cfg(target_family = "wasm")]
95use std::{cell::RefCell, rc::Rc};
96#[cfg(target_family = "wasm")]
97use wasm_bindgen_futures::spawn_local;
98#[cfg(target_family = "wasm")]
99use web_sys::HtmlCanvasElement;
100#[cfg(target_family = "wasm")]
101use web_sys::wasm_bindgen::JsCast;
102#[cfg(target_family = "wasm")]
103use winit::platform::web::EventLoopExtWebSys;
104
105#[cfg(target_os = "android")]
106use winit::platform::android::{
107 ActiveEventLoopExtAndroid, EventLoopBuilderExtAndroid, activity::AndroidApp,
108};
109
110#[derive(Debug)]
111enum RendererUserEvent {
112 AccessKit(AccessKitEvent),
113 RuntimeRedrawWake,
114 #[cfg(target_family = "wasm")]
115 WebInitReady(u64),
116}
117
118impl From<AccessKitEvent> for RendererUserEvent {
119 fn from(event: AccessKitEvent) -> Self {
120 Self::AccessKit(event)
121 }
122}
123
124type RenderComputationOutput = (
125 RenderGraph,
126 WindowRequests,
127 std::time::Duration,
128 LayoutFrameDiagnostics,
129 std::time::Duration,
130 Option<FocusDirection>,
131 bool,
132 bool,
133);
134
135#[derive(Clone, Copy, Debug, Default)]
136struct RuntimePendingWork {
137 invalidation_pending: bool,
138 layout_pending: bool,
139 frame_receiver_pending: bool,
140}
141
142pub(crate) const MAX_FRAME_SETTLING_RETRIES: usize = 8;
143
144pub(crate) fn frame_settling_can_progress(pending_keyboard_retry: bool) -> bool {
145 pending_keyboard_retry || has_ready_build_invalidations() || has_pending_layout_invalidations()
146}
147
148impl RuntimePendingWork {
149 fn capture(pending_keyboard_retry: bool) -> Self {
150 Self {
151 invalidation_pending: has_pending_build_invalidations()
152 || crate::dispatcher::has_pending()
153 || crate::clipboard::has_pending_requests()
154 || pending_keyboard_retry
155 || has_pending_persistent_focus_prune()
156 || TesseraRuntime::with(|runtime| {
157 runtime.component_tree.has_pending_focus_reveal()
158 || runtime
159 .component_tree
160 .focus_owner()
161 .has_pending_navigation()
162 }),
163 layout_pending: has_pending_layout_invalidations(),
164 frame_receiver_pending: has_pending_frame_nanos_receivers(),
165 }
166 }
167
168 fn requires_redraw(self) -> bool {
169 self.invalidation_pending || self.layout_pending || self.frame_receiver_pending
170 }
171
172 #[cfg(feature = "profiling")]
173 fn redraw_reasons(self) -> Vec<RedrawReason> {
174 let mut reasons = Vec::new();
175 if self.invalidation_pending || self.layout_pending {
176 reasons.push(RedrawReason::RuntimeInvalidation);
177 }
178 if self.frame_receiver_pending {
179 reasons.push(RedrawReason::RuntimeFrameAwaiter);
180 }
181 reasons
182 }
183}
184
185#[cfg(feature = "profiling")]
186pub(crate) fn resolve_profiler_output_path(config: &TesseraConfig) -> PathBuf {
187 if let Ok(path) = std::env::var("TESSERA_PROFILING_OUTPUT")
188 && !path.trim().is_empty()
189 {
190 return PathBuf::from(path);
191 }
192 if let Some(path) = option_env!("TESSERA_PROFILING_OUTPUT")
193 && !path.trim().is_empty()
194 {
195 return PathBuf::from(path);
196 }
197 config.profiler_output_path.clone()
198}
199
200#[derive(Debug, Clone)]
202pub struct WindowConfig {
203 pub application_id: Option<String>,
205 pub icon: Option<winit::window::Icon>,
207 pub decorations: bool,
209 pub integrated_titlebar: bool,
213 pub transparent: bool,
215 pub resizable: bool,
217}
218
219impl Default for WindowConfig {
220 fn default() -> Self {
221 Self {
222 application_id: None,
223 icon: None,
224 decorations: true,
225 integrated_titlebar: false,
226 transparent: true,
227 resizable: true,
228 }
229 }
230}
231
232impl WindowConfig {
233 pub fn with_app_titlebar(mut self) -> Self {
236 if desktop_window_controls_available() {
237 self.decorations = cfg!(target_os = "macos");
238 self.integrated_titlebar = true;
239 }
240 self
241 }
242
243 fn app_titlebar(&self) -> AppTitleBar {
244 if !self.integrated_titlebar || !desktop_window_controls_available() {
245 AppTitleBar::None
246 } else if cfg!(target_os = "macos") && self.decorations {
247 AppTitleBar::Native
248 } else if !self.decorations {
249 AppTitleBar::Custom
250 } else {
251 AppTitleBar::None
252 }
253 }
254}
255
256#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
258pub enum AppTitleBar {
259 #[default]
261 None,
262 Native,
264 Custom,
266}
267
268impl AppTitleBar {
269 pub fn current() -> Self {
271 crate::local_runtime_resource::<Cell<Self>>().get()
272 }
273}
274
275const fn desktop_window_controls_available() -> bool {
276 !cfg!(any(
277 target_family = "wasm",
278 target_os = "android",
279 target_os = "ios"
280 ))
281}
282
283#[derive(Debug, Clone, Default)]
285pub struct WebConfig {
286 pub canvas_id: Option<String>,
289}
290
291impl WebConfig {
292 pub fn with_canvas_id(mut self, canvas_id: impl Into<String>) -> Self {
294 self.canvas_id = Some(canvas_id.into());
295 self
296 }
297}
298
299#[derive(Debug, Clone)]
301pub struct TesseraConfig {
302 pub sample_count: u32,
304 pub max_non_sampler_bindings: u32,
306 pub window_title: String,
309 pub window: WindowConfig,
311 pub web: WebConfig,
313 #[cfg(feature = "profiling")]
315 pub profiler_output_path: PathBuf,
316}
317
318impl Default for TesseraConfig {
319 fn default() -> Self {
320 Self {
321 sample_count: 1,
322 max_non_sampler_bindings: DEFAULT_MAX_NON_SAMPLER_BINDINGS,
323 window_title: "Tessera".to_string(),
324 window: WindowConfig::default(),
325 web: WebConfig::default(),
326 #[cfg(feature = "profiling")]
327 profiler_output_path: PathBuf::from("tessera-profiler.jsonl"),
328 }
329 }
330}
331
332pub(crate) const DEFAULT_MAX_NON_SAMPLER_BINDINGS: u32 = 64 * 1024;
333
334pub struct Renderer<F> {
339 session: crate::RuntimeSession,
340 app: Option<RenderCore>,
341 entry_point: F,
342 cursor_state: CursorState,
343 keyboard_state: KeyboardState,
344 ime_state: ImeState,
345 ime_bridge_state: RendererImeBridgeState,
346 modules: Vec<Box<dyn RenderModule>>,
347 plugins: PluginHost,
348 config: TesseraConfig,
349 accessibility_adapter: Option<AccessKitAdapter>,
350 accessibility_active: bool,
351 event_loop_proxy: Option<winit::event_loop::EventLoopProxy<RendererUserEvent>>,
352 #[cfg(target_os = "android")]
353 android_back: Option<crate::android::system_back::BackBridge>,
354 pending_desktop_window_action: Arc<RwLock<Option<DesktopWindowAction>>>,
355 #[cfg(target_os = "macos")]
356 macos_titlebar: macos_titlebar::TitleBarInput,
357 frame_index: u64,
358 redraw_request_pending: Arc<AtomicBool>,
359 pending_close_requested: bool,
360 resize_in_progress: bool,
361 pending_window_size: Option<winit::dpi::PhysicalSize<u32>>,
362 #[cfg(target_family = "wasm")]
363 pending_web_inits: Rc<RefCell<Vec<(u64, RenderCore)>>>,
364 #[cfg(target_family = "wasm")]
365 web_init_epoch: u64,
366 #[cfg(target_family = "wasm")]
367 web_init_in_progress: bool,
368 #[cfg(feature = "profiling")]
369 pending_redraw_reasons: BTreeSet<RedrawReason>,
370}
371
372impl<F, Output> Renderer<F>
373where
374 F: Fn() -> Output,
375{
376 #[cfg(target_family = "wasm")]
377 fn resolve_web_canvas(config: &TesseraConfig) -> Option<HtmlCanvasElement> {
378 let canvas_id = config.web.canvas_id.as_deref()?;
379 let window = web_sys::window()?;
380 let document = window.document()?;
381 let element = document.get_element_by_id(canvas_id)?;
382 element.dyn_into::<HtmlCanvasElement>().ok()
383 }
384
385 #[cfg(all(not(target_os = "android"), not(target_family = "wasm")))]
389 #[tracing::instrument(level = "info", skip(entry_point, modules))]
390 pub fn run(entry_point: F, modules: Vec<Box<dyn RenderModule>>) -> Result<(), EventLoopError> {
391 Self::run_with_config(entry_point, modules, Default::default())
392 }
393
394 #[tracing::instrument(level = "info", skip(entry_point, modules))]
398 #[cfg(all(not(target_os = "android"), not(target_family = "wasm")))]
399 pub fn run_with_config(
400 entry_point: F,
401 modules: Vec<Box<dyn RenderModule>>,
402 config: TesseraConfig,
403 ) -> Result<(), EventLoopError> {
404 let session = crate::RuntimeSession::for_root();
405 session.activate();
406 let event_loop = EventLoop::<RendererUserEvent>::with_user_event().build()?;
407 let event_loop_proxy = event_loop.create_proxy();
408 let app = None;
409 let cursor_state = CursorState::default();
410 let keyboard_state = KeyboardState::default();
411 let ime_state = ImeState::default();
412 let ime_bridge_state = RendererImeBridgeState::default();
413 #[cfg(feature = "profiling")]
414 crate::profiler::set_output_path(resolve_profiler_output_path(&config));
415 let mut renderer = Self {
416 session,
417 app,
418 entry_point,
419 cursor_state,
420 keyboard_state,
421 modules,
422 plugins: PluginHost::new(),
423 ime_state,
424 ime_bridge_state,
425 config,
426 accessibility_adapter: None,
427 accessibility_active: false,
428 event_loop_proxy: Some(event_loop_proxy),
429 pending_desktop_window_action: Arc::new(RwLock::new(None)),
430 #[cfg(target_os = "macos")]
431 macos_titlebar: macos_titlebar::TitleBarInput::default(),
432 frame_index: 0,
433 redraw_request_pending: Arc::new(AtomicBool::new(false)),
434 pending_close_requested: false,
435 resize_in_progress: false,
436 pending_window_size: None,
437 #[cfg(target_family = "wasm")]
438 pending_web_inits: Rc::new(RefCell::new(Vec::new())),
439 #[cfg(target_family = "wasm")]
440 web_init_epoch: 0,
441 #[cfg(target_family = "wasm")]
442 web_init_in_progress: false,
443 #[cfg(feature = "profiling")]
444 pending_redraw_reasons: BTreeSet::new(),
445 };
446 thread_utils::set_thread_name("TesseraMain");
447 event_loop.run_app(&mut renderer)
448 }
449
450 #[cfg(target_family = "wasm")]
453 #[tracing::instrument(level = "info", skip(entry_point, modules))]
454 pub fn run(entry_point: F, modules: Vec<Box<dyn RenderModule>>) -> Result<(), EventLoopError>
455 where
456 F: 'static,
457 {
458 Self::run_web_with_config(entry_point, modules, Default::default())
459 }
460
461 #[cfg(target_family = "wasm")]
464 #[tracing::instrument(level = "info", skip(entry_point, modules))]
465 pub fn run_web_with_config(
466 entry_point: F,
467 modules: Vec<Box<dyn RenderModule>>,
468 config: TesseraConfig,
469 ) -> Result<(), EventLoopError>
470 where
471 F: 'static,
472 {
473 let session = crate::RuntimeSession::for_root();
474 session.activate();
475 let event_loop = EventLoop::<RendererUserEvent>::with_user_event().build()?;
476 let event_loop_proxy = event_loop.create_proxy();
477 let app = None;
478 let cursor_state = CursorState::default();
479 let keyboard_state = KeyboardState::default();
480 let ime_state = ImeState::default();
481 let ime_bridge_state = RendererImeBridgeState::default();
482 #[cfg(feature = "profiling")]
483 crate::profiler::set_output_path(resolve_profiler_output_path(&config));
484 let renderer = Self {
485 session,
486 app,
487 entry_point,
488 cursor_state,
489 keyboard_state,
490 modules,
491 plugins: PluginHost::new(),
492 ime_state,
493 ime_bridge_state,
494 config,
495 accessibility_adapter: None,
496 accessibility_active: false,
497 event_loop_proxy: Some(event_loop_proxy),
498 pending_desktop_window_action: Arc::new(RwLock::new(None)),
499 frame_index: 0,
500 redraw_request_pending: Arc::new(AtomicBool::new(false)),
501 pending_close_requested: false,
502 resize_in_progress: false,
503 pending_window_size: None,
504 pending_web_inits: Rc::new(RefCell::new(Vec::new())),
505 web_init_epoch: 0,
506 web_init_in_progress: false,
507 #[cfg(feature = "profiling")]
508 pending_redraw_reasons: BTreeSet::new(),
509 };
510 thread_utils::set_thread_name("TesseraMain");
511 event_loop.spawn_app(renderer);
512 Ok(())
513 }
514
515 #[cfg(target_os = "android")]
519 #[tracing::instrument(level = "info", skip(entry_point, modules, android_app))]
520 pub fn run(
521 entry_point: F,
522 modules: Vec<Box<dyn RenderModule>>,
523 android_app: AndroidApp,
524 ) -> Result<(), EventLoopError> {
525 Self::run_with_config(entry_point, modules, android_app, Default::default())
526 }
527
528 #[cfg(target_os = "android")]
532 #[tracing::instrument(level = "info", skip(entry_point, modules, android_app))]
533 pub fn run_with_config(
534 entry_point: F,
535 modules: Vec<Box<dyn RenderModule>>,
536 android_app: AndroidApp,
537 config: TesseraConfig,
538 ) -> Result<(), EventLoopError> {
539 let session = crate::RuntimeSession::for_root();
540 session.activate();
541 let event_loop = EventLoop::<RendererUserEvent>::with_user_event()
542 .with_android_app(android_app.clone())
543 .build()
544 .unwrap();
545 let event_loop_proxy = event_loop.create_proxy();
546 let android_back =
547 crate::android::system_back::BackBridge::new(&android_app, session.dispatcher());
548 let app = None;
549 let cursor_state = CursorState::default();
550 let keyboard_state = KeyboardState::default();
551 let ime_state = ImeState::default();
552 let ime_bridge_state = RendererImeBridgeState::default();
553 #[cfg(feature = "profiling")]
554 crate::profiler::set_output_path(resolve_profiler_output_path(&config));
555 let mut renderer = Self {
556 session,
557 app,
558 entry_point,
559 cursor_state,
560 keyboard_state,
561 modules,
562 plugins: PluginHost::new(),
563 ime_state,
564 ime_bridge_state,
565 config,
566 accessibility_adapter: None,
567 accessibility_active: false,
568 event_loop_proxy: Some(event_loop_proxy),
569 android_back,
570 pending_desktop_window_action: Arc::new(RwLock::new(None)),
571 frame_index: 0,
572 redraw_request_pending: Arc::new(AtomicBool::new(false)),
573 pending_close_requested: false,
574 resize_in_progress: false,
575 pending_window_size: None,
576 #[cfg(feature = "profiling")]
577 pending_redraw_reasons: BTreeSet::new(),
578 };
579 thread_utils::set_thread_name("TesseraMain");
580 event_loop.run_app(&mut renderer)
581 }
582}
583
584struct RenderFrameArgs<'a> {
587 pub cursor_state: &'a mut CursorState,
588 pub keyboard_state: &'a mut KeyboardState,
589 pub ime_state: &'a mut ImeState,
590 pub ime_bridge_state: &'a mut RendererImeBridgeState,
591 pub app: &'a mut RenderCore,
592}
593
594struct RenderFrameContext<'a, F> {
595 entry_point: &'a F,
596 args: &'a mut RenderFrameArgs<'a>,
597 accessibility_enabled: bool,
598 decorations: bool,
599 window_label: &'a str,
600 frame_idx: u64,
601 #[cfg(feature = "profiling")]
602 redraw_reasons: Vec<RedrawReason>,
603}
604
605struct RenderFrameOutcome {
606 accessibility_update: Option<TreeUpdate>,
607 request_window_drag: Option<WindowDragRequest>,
608 runtime_pending_work: RuntimePendingWork,
609 #[cfg(feature = "profiling")]
610 frame_meta: FrameMeta,
611 #[cfg(feature = "debug-dirty-overlay")]
612 overlay_clear_pending: bool,
613}
614
615#[derive(Clone, Debug, Default, PartialEq)]
616struct RendererImeBridgeState {
617 current_request: Option<ImeRequest>,
618}
619
620#[derive(Clone, Debug, Default, PartialEq)]
621struct RendererImeBridgeUpdate {
622 allowed: Option<bool>,
623 cursor_area: Option<(PxPosition, PxSize)>,
624 text_state: Option<winit::event::ImeTextState>,
625 options: Option<winit::window::ImeOptions>,
626 warn_missing_position: bool,
627 show_keyboard: bool,
628 change_cause: winit::event::ImeChangeCause,
629}
630
631impl RendererImeBridgeState {
632 fn sync_window(&mut self, window: &Window, request: Option<crate::ImeRequest>) {
633 let ime_was_allowed = self.current_request.is_some();
634 let ime_bridge_update = self.update_request(request);
635 if ime_was_allowed && ime_bridge_update.allowed == Some(true) {
636 window.set_ime_allowed(false);
637 }
638 if let Some(options) = ime_bridge_update.options {
639 window.set_ime_options(options);
640 }
641 if let Some(state) = ime_bridge_update.text_state {
642 let _ = window.set_ime_text_state(state, ime_bridge_update.change_cause);
643 }
644 if let Some(allowed) = ime_bridge_update.allowed {
645 window.set_ime_allowed(allowed);
646 }
647 if ime_bridge_update.show_keyboard {
648 window.set_ime_allowed(true);
649 }
650 if let Some((position, size)) = ime_bridge_update.cursor_area {
651 window.set_ime_cursor_area::<PxPosition, PxSize>(position, size);
652 } else if ime_bridge_update.warn_missing_position {
653 warn!("IME request missing position; skipping IME cursor area update");
654 }
655 }
656
657 #[cfg(test)]
658 fn request(&self) -> Option<&ImeRequest> {
659 self.current_request.as_ref()
660 }
661
662 fn reset(&mut self) {
663 *self = Self::default();
664 }
665
666 fn receive_event(&mut self, event: &winit::event::Ime) -> Option<crate::focus::FocusHandleId> {
667 let request = self.current_request.as_mut()?;
668 let client = request.client_id?;
669 if let winit::event::Ime::TextState(state) = event {
670 request.text_state.clone_from(state);
672 }
673 Some(client)
674 }
675
676 fn update_request(&mut self, mut request: Option<ImeRequest>) -> RendererImeBridgeUpdate {
677 let change_cause = request
678 .as_mut()
679 .map_or(winit::event::ImeChangeCause::Other, |request| {
680 std::mem::take(&mut request.change_cause)
681 });
682
683 let show_keyboard = request
684 .as_mut()
685 .is_some_and(|request| std::mem::take(&mut request.show_keyboard));
686 let previous = self.current_request.as_ref();
687 let context_changed = previous.map(|request| (request.client_id, request.options))
688 != request
689 .as_ref()
690 .map(|request| (request.client_id, request.options));
691 let allowed = context_changed.then_some(request.is_some());
692 let cursor_area = request.as_ref().and_then(|next| {
693 let old_area =
694 previous.and_then(|old| old.position.map(|position| (position, old.size)));
695 let area = next.position.map(|position| (position, next.size));
696 (context_changed || old_area != area)
697 .then_some(area)
698 .flatten()
699 });
700 let text_state = request
701 .as_ref()
702 .filter(|next| {
703 context_changed || previous.is_none_or(|old| old.text_state != next.text_state)
704 })
705 .map(|next| next.text_state.clone());
706 let options = request
707 .as_ref()
708 .filter(|_| context_changed)
709 .map(|next| next.options);
710 let warn_missing_position = self.current_request != request
711 && request
712 .as_ref()
713 .is_some_and(|request| request.position.is_none());
714 self.current_request = request;
715 RendererImeBridgeUpdate {
716 change_cause,
717 allowed,
718 cursor_area,
719 text_state,
720 options,
721 warn_missing_position,
722 show_keyboard: show_keyboard && allowed != Some(true),
723 }
724 }
725}
726
727impl<F, Output> Renderer<F>
728where
729 F: Fn() -> Output,
730{
731 const RESIZE_EDGE_THRESHOLD: f64 = 8.0;
732
733 #[cfg(target_os = "windows")]
734 fn update_native_window_shape(&self, window: &Window) {
735 let _session = self.session.enter();
736 use winit::platform::windows::{CornerPreference, WindowExtWindows};
737
738 if self.config.window.decorations {
739 window.set_corner_preference(CornerPreference::Default);
740 return;
741 }
742
743 let preference = if window.is_maximized() || window.fullscreen().is_some() {
744 CornerPreference::DoNotRound
745 } else {
746 CornerPreference::Round
747 };
748 window.set_corner_preference(preference);
749 }
750
751 #[cfg(target_os = "macos")]
752 fn update_native_window_shape(&self, window: &Window) {
753 let _session = self.session.enter();
754 use objc2::rc::Retained;
755 use objc2_app_kit::NSView;
756 use winit::raw_window_handle::{HasWindowHandle, RawWindowHandle};
757
758 let raw_window_handle = match window.window_handle() {
759 Ok(handle) => handle.as_raw(),
760 Err(err) => {
761 warn!("Failed to fetch native window handle: {}", err);
762 return;
763 }
764 };
765 let RawWindowHandle::AppKit(appkit) = raw_window_handle else {
766 return;
767 };
768
769 let create_retained = Retained::<NSView>::retain;
772 let Some(ns_view) = (unsafe { create_retained(appkit.ns_view.as_ptr().cast()) }) else {
773 warn!("Failed to retain NSView from raw window handle");
774 return;
775 };
776
777 let radius_px = if self.config.window.decorations
778 || window.is_maximized()
779 || window.fullscreen().is_some()
780 {
781 0.0
782 } else {
783 8.0 * window.scale_factor()
784 };
785
786 ns_view.setWantsLayer(true);
787 let layer = if let Some(layer) = ns_view.layer() {
788 layer
789 } else {
790 let layer = ns_view.makeBackingLayer();
791 ns_view.setLayer(Some(&layer));
792 layer
793 };
794 layer.setCornerRadius(radius_px as _);
795 layer.setMasksToBounds(radius_px > 0.0);
796 }
797
798 #[cfg(not(any(target_os = "windows", target_os = "macos")))]
799 fn update_native_window_shape(&self, _window: &Window) {
800 let _session = self.session.enter();
801 }
802
803 fn should_set_cursor_pos(
804 cursor_position: Option<crate::PxPosition>,
805 window_width: f64,
806 window_height: f64,
807 edge_threshold: f64,
808 ) -> bool {
809 if let Some(pos) = cursor_position {
810 let x = pos.x as f64;
811 let y = pos.y as f64;
812 x > edge_threshold
813 && x < window_width - edge_threshold
814 && y > edge_threshold
815 && y < window_height - edge_threshold
816 } else {
817 false
818 }
819 }
820
821 fn cursor_resize_direction(
822 cursor_position: Option<crate::PxPosition>,
823 window_size: winit::dpi::PhysicalSize<u32>,
824 edge_threshold: f64,
825 ) -> Option<ResizeDirection> {
826 let position = cursor_position?;
827 let x = position.x as f64;
828 let y = position.y as f64;
829 let width = window_size.width as f64;
830 let height = window_size.height as f64;
831
832 let near_left = x <= edge_threshold;
833 let near_right = x >= width - edge_threshold;
834 let near_top = y <= edge_threshold;
835 let near_bottom = y >= height - edge_threshold;
836
837 match (near_left, near_right, near_top, near_bottom) {
838 (true, _, true, _) => Some(ResizeDirection::NorthWest),
839 (_, true, true, _) => Some(ResizeDirection::NorthEast),
840 (true, _, _, true) => Some(ResizeDirection::SouthWest),
841 (_, true, _, true) => Some(ResizeDirection::SouthEast),
842 (true, _, _, _) => Some(ResizeDirection::West),
843 (_, true, _, _) => Some(ResizeDirection::East),
844 (_, _, true, _) => Some(ResizeDirection::North),
845 (_, _, _, true) => Some(ResizeDirection::South),
846 _ => None,
847 }
848 }
849
850 fn cursor_icon_for_resize(direction: ResizeDirection) -> winit::window::CursorIcon {
851 match direction {
852 ResizeDirection::East | ResizeDirection::West => winit::window::CursorIcon::EwResize,
853 ResizeDirection::North | ResizeDirection::South => winit::window::CursorIcon::NsResize,
854 ResizeDirection::NorthEast | ResizeDirection::SouthWest => {
855 winit::window::CursorIcon::NeswResize
856 }
857 ResizeDirection::NorthWest | ResizeDirection::SouthEast => {
858 winit::window::CursorIcon::NwseResize
859 }
860 }
861 }
862
863 fn log_frame_stats(
864 build_tree_cost: std::time::Duration,
865 draw_cost: std::time::Duration,
866 render_cost: std::time::Duration,
867 render_breakdown: Option<RenderTimingBreakdown>,
868 ) {
869 let total = build_tree_cost + draw_cost + render_cost;
870 let fps = 1.0 / total.as_secs_f32();
871 if fps < 30.0 {
872 if let Some(breakdown) = render_breakdown {
873 warn!(
874 "Jank detected! Frame statistics:
875Build tree cost: {:?}
876Draw commands cost: {:?}
877Render cost: {:?}
878Total frame cost: {:?}
879Fps: {:.2}
880Render breakdown:
881Acquire: {:?}
882Build passes: {:?}
883Encode: {:?}
884Submit: {:?}
885Present: {:?}
886Render total (core): {:?}
887",
888 build_tree_cost,
889 draw_cost,
890 render_cost,
891 total,
892 1.0 / total.as_secs_f32(),
893 breakdown.acquire,
894 breakdown.build_passes,
895 breakdown.encode,
896 breakdown.submit,
897 breakdown.present,
898 breakdown.total,
899 );
900 } else {
901 warn!(
902 "Jank detected! Frame statistics:
903Build tree cost: {:?}
904Draw commands cost: {:?}
905Render cost: {:?}
906Total frame cost: {:?}
907Fps: {:.2}
908",
909 build_tree_cost,
910 draw_cost,
911 render_cost,
912 total,
913 1.0 / total.as_secs_f32()
914 );
915 }
916 }
917 }
918
919 #[instrument(level = "debug", skip(args))]
920 fn compute_draw_commands<'a>(
921 args: &mut RenderFrameArgs<'a>,
922 screen_size: PxSize,
923 frame_idx: u64,
924 retry_focus_move: Option<FocusDirection>,
925 retry_focus_reveal: bool,
926 is_retry: bool,
927 ) -> RenderComputationOutput {
928 let draw_timer = Instant::now();
929 debug!("Computing draw commands...");
930 let cursor_position = args.cursor_state.position();
931 let pointer_changes = if is_retry {
932 Vec::new()
933 } else {
934 args.cursor_state.take_events()
935 };
936 let keyboard_events = if is_retry {
937 Vec::new()
938 } else {
939 args.keyboard_state.take_events()
940 };
941 let ime_events = if is_retry {
942 Vec::new()
943 } else {
944 args.ime_state.take_events()
945 };
946
947 let layout_dirty_nodes = take_layout_dirty_nodes();
948
949 let (
950 graph,
951 window_requests,
952 layout_diagnostics,
953 record_cost,
954 pending_focus_move_retry,
955 pending_focus_reveal_retry,
956 pending_keyboard_retry,
957 ) = TesseraRuntime::with_mut(|rt| {
958 let component_tree = &mut rt.component_tree;
959 component_tree.compute(
960 crate::component_tree::ComputeParams {
961 screen_size,
962 cursor_position,
963 pointer_changes,
964 keyboard_events,
965 ime_events,
966 retry_focus_move,
967 retry_focus_reveal,
968 modifiers: args.keyboard_state.modifiers(),
969 layout_dirty_nodes,
970 },
971 crate::component_tree::ComputeMode::Full,
972 )
973 });
974 flush_pending_focus_callbacks();
975 crate::component_tree::flush_pending_globally_positioned_callbacks();
976 let pending_keyboard_retry = pending_keyboard_retry
977 || TesseraRuntime::with(|runtime| runtime.component_tree.has_pending_focus_reveal());
978
979 let draw_cost = draw_timer.elapsed();
980 debug!("Draw commands computed in {draw_cost:?}");
981 (
982 graph,
983 window_requests,
984 draw_cost,
985 layout_diagnostics,
986 record_cost,
987 pending_focus_move_retry,
988 pending_focus_reveal_retry,
989 pending_keyboard_retry,
990 )
991 }
992
993 #[cfg(feature = "debug-dirty-overlay")]
994 fn layout_node_overlay_rect(
995 metadatas: &crate::component_tree::ComponentNodeMetaDatas,
996 node_id: crate::NodeId,
997 ) -> Option<PxRect> {
998 let metadata = metadatas.get(&node_id)?;
999 let abs_position = metadata.abs_position?;
1000 let computed_data = metadata.computed_data?;
1001 if computed_data.width.0 <= 0 || computed_data.height.0 <= 0 {
1002 return None;
1003 }
1004 let node_rect = PxRect::from_position_size(
1005 abs_position,
1006 PxSize::new(computed_data.width, computed_data.height),
1007 );
1008 Some(
1009 metadata
1010 .event_clip_rect()
1011 .and_then(|clip_rect| clip_rect.intersection(&node_rect))
1012 .unwrap_or(node_rect),
1013 )
1014 }
1015
1016 #[cfg(feature = "debug-dirty-overlay")]
1017 fn collect_dirty_overlay_rects(
1018 screen_size: PxSize,
1019 build_tree_result: &BuildTreeResult,
1020 ) -> Vec<PxRect> {
1021 use crate::component_tree::{NodeRole, direct_layout_children};
1022
1023 if matches!(build_tree_result.mode(), BuildTreeMode::SkipNoInvalidation) {
1024 return Vec::new();
1025 }
1026 if matches!(build_tree_result.mode(), BuildTreeMode::RootRecompose) {
1027 if build_tree_result.had_invalidations() {
1028 return vec![PxRect::from_position_size(PxPosition::ZERO, screen_size)];
1029 }
1030 return Vec::new();
1031 }
1032 TesseraRuntime::with(|rt| {
1033 let tree = rt.component_tree.tree();
1034 let metadatas = rt.component_tree.metadatas();
1035 let mut rects = Vec::new();
1036 for instance_key in build_tree_result.dirty_replay_roots() {
1037 let Some(node_id) = rt
1038 .component_tree
1039 .find_node_id_by_instance_key(*instance_key)
1040 else {
1041 continue;
1042 };
1043 let layout_nodes = match tree.get(node_id) {
1044 Some(node_ref) if node_ref.get().role == NodeRole::Layout => vec![node_id],
1045 Some(_) => direct_layout_children(node_id, tree).to_vec(),
1046 None => continue,
1047 };
1048 for layout_node_id in layout_nodes {
1049 if let Some(rect) = Self::layout_node_overlay_rect(metadatas, layout_node_id) {
1050 rects.push(rect);
1051 }
1052 }
1053 }
1054 rects
1055 })
1056 }
1057
1058 #[cfg(not(feature = "debug-dirty-overlay"))]
1061 #[instrument(level = "debug", skip(args, graph))]
1062 fn perform_render<'a>(
1063 args: &mut RenderFrameArgs<'a>,
1064 graph: RenderGraph,
1065 ) -> std::time::Duration {
1066 #[cfg(feature = "profiling")]
1067 let _profiler_guard =
1068 ProfilerScopeGuard::new(ProfilerPhase::RenderFrame, None, None, Some("render_frame"));
1069 let render_timer = Instant::now();
1070
1071 if TesseraRuntime::with(|rt| rt.window_minimized) {
1073 return render_timer.elapsed();
1074 }
1075
1076 debug!("Rendering draw commands...");
1077 args.app.render(graph);
1078 let render_cost = render_timer.elapsed();
1079 debug!("Rendered to surface in {render_cost:?}");
1080 render_cost
1081 }
1082
1083 #[cfg(feature = "debug-dirty-overlay")]
1084 #[instrument(level = "debug", skip(args, graph, dirty_overlay_rects))]
1085 fn perform_render<'a>(
1086 args: &mut RenderFrameArgs<'a>,
1087 graph: RenderGraph,
1088 dirty_overlay_rects: &[PxRect],
1089 ) -> std::time::Duration {
1090 #[cfg(feature = "profiling")]
1091 let _profiler_guard =
1092 ProfilerScopeGuard::new(ProfilerPhase::RenderFrame, None, None, Some("render_frame"));
1093 let render_timer = Instant::now();
1094
1095 if TesseraRuntime::with(|rt| rt.window_minimized) {
1097 return render_timer.elapsed();
1098 }
1099
1100 debug!("Rendering draw commands...");
1101 args.app.render(graph, dirty_overlay_rects);
1102 let render_cost = render_timer.elapsed();
1103 debug!("Rendered to surface in {render_cost:?}");
1104 render_cost
1105 }
1106
1107 #[instrument(level = "debug", skip(context))]
1108 fn execute_render_frame(context: RenderFrameContext<'_, F>) -> RenderFrameOutcome {
1109 let RenderFrameContext {
1110 entry_point,
1111 args,
1112 accessibility_enabled,
1113 decorations,
1114 window_label,
1115 frame_idx,
1116 #[cfg(feature = "profiling")]
1117 redraw_reasons,
1118 } = context;
1119 #[cfg(feature = "profiling")]
1120 profiler_begin_frame(frame_idx);
1121 begin_frame_clock(Instant::now());
1122 begin_frame_build_invalidations();
1123 tick_frame_nanos_receivers();
1126 args.app.window().pre_present_notify();
1130 TesseraRuntime::with_mut(|rt| {
1132 rt.component_tree.begin_frame_diagnostics();
1133 rt.window_size = args.app.size().into();
1134 });
1135 let mut build_tree_result = build_component_tree(entry_point);
1137 debug!("Component tree build mode: {:?}", build_tree_result.mode());
1138
1139 let screen_size: PxSize = args.app.size().into();
1141 let (
1142 mut new_graph,
1143 mut window_requests,
1144 mut draw_cost,
1145 mut layout_diagnostics,
1146 mut record_cost,
1147 mut pending_focus_move_retry,
1148 mut pending_focus_reveal_retry,
1149 mut pending_keyboard_retry,
1150 ) = Self::compute_draw_commands(args, screen_size, frame_idx, None, false, false);
1151 let mut settling_retries = 0usize;
1152 while pending_focus_move_retry.is_some()
1153 || pending_focus_reveal_retry
1154 || frame_settling_can_progress(pending_keyboard_retry)
1155 {
1156 let focus_retry = pending_focus_move_retry.is_some()
1157 || pending_focus_reveal_retry
1158 || pending_keyboard_retry;
1159 if settling_retries >= MAX_FRAME_SETTLING_RETRIES
1160 || (focus_retry && !frame_settling_can_progress(pending_keyboard_retry))
1161 {
1162 break;
1163 }
1164
1165 settling_retries += 1;
1166 if focus_retry || has_ready_build_invalidations() {
1167 build_tree_result.absorb_retry(build_component_tree(entry_point));
1168 }
1169
1170 let (
1171 retry_graph,
1172 retry_window_requests,
1173 retry_draw_cost,
1174 retry_layout_diagnostics,
1175 retry_record_cost,
1176 next_focus_move_retry,
1177 next_focus_reveal_retry,
1178 next_keyboard_retry,
1179 ) = Self::compute_draw_commands(
1180 args,
1181 screen_size,
1182 frame_idx,
1183 pending_focus_move_retry,
1184 pending_focus_reveal_retry,
1185 true,
1186 );
1187 new_graph = retry_graph;
1188 window_requests.settle(retry_window_requests);
1189 draw_cost += retry_draw_cost;
1190 layout_diagnostics.accumulate_pass(retry_layout_diagnostics);
1191 record_cost += retry_record_cost;
1192 pending_focus_move_retry = next_focus_move_retry;
1193 pending_focus_reveal_retry = next_focus_reveal_retry;
1194 pending_keyboard_retry = next_keyboard_retry;
1195 }
1196 if pending_focus_reveal_retry
1197 && (has_pending_build_invalidations() || has_pending_layout_invalidations())
1198 {
1199 TesseraRuntime::with_mut(|runtime| {
1200 runtime
1201 .component_tree
1202 .focus_owner_mut()
1203 .retain_reveal_retry();
1204 });
1205 }
1206 prune_persistent_focus_handles();
1207 #[cfg(not(feature = "profiling"))]
1208 let _ = (layout_diagnostics, record_cost);
1209 #[cfg(feature = "debug-dirty-overlay")]
1210 let dirty_overlay_rects =
1211 Self::collect_dirty_overlay_rects(screen_size, &build_tree_result);
1212 #[cfg(feature = "debug-dirty-overlay")]
1213 let overlay_clear_pending = !dirty_overlay_rects.is_empty();
1214 let render_cost = Self::perform_render(
1215 args,
1216 new_graph,
1217 #[cfg(feature = "debug-dirty-overlay")]
1218 &dirty_overlay_rects,
1219 );
1220 let render_breakdown = args.app.last_render_breakdown();
1222 Self::log_frame_stats(
1223 build_tree_result.duration(),
1224 draw_cost,
1225 render_cost,
1226 render_breakdown,
1227 );
1228
1229 #[cfg(feature = "profiling")]
1230 let frame_meta = {
1231 let render_duration_ns = Some(render_cost.as_nanos());
1232 let render_acquire_ns = render_breakdown.map(|breakdown| breakdown.acquire.as_nanos());
1233 let render_build_passes_ns =
1234 render_breakdown.map(|breakdown| breakdown.build_passes.as_nanos());
1235 let render_encode_ns = render_breakdown.map(|breakdown| breakdown.encode.as_nanos());
1236 let render_submit_ns = render_breakdown.map(|breakdown| breakdown.submit.as_nanos());
1237 let render_present_ns = render_breakdown.map(|breakdown| breakdown.present.as_nanos());
1238 #[cfg(not(target_arch = "wasm32"))]
1239 let render_gpu_ns = render_breakdown
1240 .and_then(|_| args.app.read_gpu_time())
1241 .map(|time| time.as_nanos());
1242 #[cfg(target_arch = "wasm32")]
1243 let render_gpu_ns = None;
1244 let inter_frame_wait_ns = (frame_idx > 0).then(|| frame_delta().as_nanos());
1245 let nodes = TesseraRuntime::with_mut(|rt| rt.component_tree.profiler_nodes());
1246 FrameMeta {
1247 frame_idx,
1248 build_mode: build_tree_result.profiler_build_mode(),
1249 redraw_reasons,
1250 inter_frame_wait_ns,
1251 partial_replay_nodes: build_tree_result.partial_replay_nodes(),
1252 total_nodes_before_build: build_tree_result.total_nodes_before_build(),
1253 render_time_ns: render_duration_ns,
1254 render_prepare_ns: None,
1255 render_acquire_ns,
1256 render_build_passes_ns,
1257 render_encode_ns,
1258 render_submit_ns,
1259 render_present_ns,
1260 render_gpu_ns,
1261 build_tree_time_ns: Some(build_tree_result.duration().as_nanos()),
1262 draw_time_ns: Some(draw_cost.as_nanos()),
1263 record_time_ns: Some(record_cost.as_nanos()),
1264 frame_total_ns: None,
1265 layout_diagnostics: Some(layout_diagnostics),
1266 nodes,
1267 }
1268 };
1269
1270 let accessibility_update = if accessibility_enabled {
1273 Self::build_accessibility_update(window_label, false)
1274 } else {
1275 None
1276 };
1277
1278 let cursor_position = args.cursor_state.position();
1282 let window_size = args.app.size();
1283 let resize_direction = (desktop_window_controls_available() && !decorations).then(|| {
1284 Self::cursor_resize_direction(cursor_position, window_size, Self::RESIZE_EDGE_THRESHOLD)
1285 });
1286
1287 if let Some(direction) = resize_direction.flatten() {
1288 let icon = Self::cursor_icon_for_resize(direction);
1289 args.app
1290 .window()
1291 .set_cursor(winit::window::Cursor::Icon(icon));
1292 } else {
1293 let should_set_cursor = Self::should_set_cursor_pos(
1294 cursor_position,
1295 window_size.width as f64,
1296 window_size.height as f64,
1297 Self::RESIZE_EDGE_THRESHOLD,
1298 );
1299
1300 if should_set_cursor {
1301 args.app
1302 .window()
1303 .set_cursor(winit::window::Cursor::Icon(window_requests.cursor_icon));
1304 }
1305 }
1306
1307 let request_window_drag = window_requests
1308 .request_window_drag
1309 .filter(|_| desktop_window_controls_available());
1310
1311 args.ime_bridge_state
1312 .sync_window(args.app.window(), window_requests.ime_request);
1313
1314 args.cursor_state.frame_cleanup();
1316
1317 let mut runtime_pending_work = RuntimePendingWork::capture(pending_keyboard_retry);
1318 runtime_pending_work.invalidation_pending |=
1319 args.app.resources().graphics.take_deferred_upload();
1320
1321 RenderFrameOutcome {
1322 accessibility_update,
1323 request_window_drag,
1324 runtime_pending_work,
1325 #[cfg(feature = "profiling")]
1326 frame_meta,
1327 #[cfg(feature = "debug-dirty-overlay")]
1328 overlay_clear_pending,
1329 }
1330 }
1331}
1332
1333impl<F, Output> Renderer<F>
1334where
1335 F: Fn() -> Output,
1336{
1337 fn desktop_platform_context(&self) -> Option<DesktopPlatformContext> {
1338 let _session = self.session.enter();
1339 let app = self.app.as_ref()?;
1340 let redraw_pending = self.redraw_request_pending.clone();
1341 let window = app.window_arc();
1342 let wake_handler = Arc::new(move || {
1343 Self::try_request_redraw(window.as_ref(), redraw_pending.as_ref());
1344 });
1345 Some(DesktopPlatformContext::new(
1346 app.window_arc(),
1347 self.pending_desktop_window_action.clone(),
1348 wake_handler,
1349 ))
1350 }
1351
1352 fn take_pending_desktop_window_action(&self) -> Option<DesktopWindowAction> {
1353 let _session = self.session.enter();
1354 self.pending_desktop_window_action.write().take()
1355 }
1356
1357 #[cfg(target_os = "android")]
1358 fn plugin_context(&self, event_loop: &ActiveEventLoop) -> Option<PluginContext> {
1359 let _session = self.session.enter();
1360 let desktop = self.desktop_platform_context()?;
1361 Some(PluginContext::new(
1362 desktop,
1363 event_loop.android_app().clone(),
1364 ))
1365 }
1366
1367 #[cfg(not(target_os = "android"))]
1368 fn plugin_context(&self, _event_loop: &ActiveEventLoop) -> Option<PluginContext> {
1369 let _session = self.session.enter();
1370 let desktop = self.desktop_platform_context()?;
1371 Some(PluginContext::new(desktop))
1372 }
1373
1374 fn try_request_redraw(window: &Window, redraw_pending: &AtomicBool) {
1375 if redraw_pending
1376 .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
1377 .is_ok()
1378 {
1379 window.request_redraw();
1380 }
1381 }
1382
1383 fn request_redraw_now(&self) {
1384 let _session = self.session.enter();
1385 if let Some(app) = self.app.as_ref() {
1386 Self::try_request_redraw(app.window(), self.redraw_request_pending.as_ref());
1387 }
1388 }
1389
1390 fn install_runtime_redraw_waker(&self) {
1391 let _session = self.session.enter();
1392 let Some(proxy) = self.event_loop_proxy.clone() else {
1393 clear_redraw_waker();
1394 return;
1395 };
1396 install_redraw_waker(Arc::new(move || {
1397 let _ = proxy.send_event(RendererUserEvent::RuntimeRedrawWake);
1398 }));
1399 }
1400
1401 #[cfg(target_family = "wasm")]
1402 fn begin_web_initialization(&mut self, window: Arc<Window>) {
1403 let _session = self.session.enter();
1404 let Some(proxy) = self.event_loop_proxy.clone() else {
1405 error!("Missing event loop proxy for web renderer initialization");
1406 return;
1407 };
1408
1409 self.web_init_in_progress = true;
1410 let epoch = self.web_init_epoch;
1411 let pending_web_inits = self.pending_web_inits.clone();
1412 let sample_count = self.config.sample_count;
1413 let transparent = self.config.window.transparent;
1414 let max_bindings = self.config.max_non_sampler_bindings;
1415 spawn_local(async move {
1416 let render_core =
1417 RenderCore::new(window, sample_count, transparent, max_bindings).await;
1418 pending_web_inits.borrow_mut().push((epoch, render_core));
1419 let _ = proxy.send_event(RendererUserEvent::WebInitReady(epoch));
1420 });
1421 }
1422
1423 #[cfg(target_family = "wasm")]
1424 fn finish_web_initialization(&mut self, event_loop: &ActiveEventLoop, epoch: u64) -> bool {
1425 let _session = self.session.enter();
1426 let pending_render_core = {
1427 let mut pending_web_inits = self.pending_web_inits.borrow_mut();
1428 pending_web_inits
1429 .iter()
1430 .position(|(ready_epoch, _)| *ready_epoch == epoch)
1431 .map(|position| pending_web_inits.remove(position).1)
1432 };
1433 let Some(mut render_core) = pending_render_core else {
1434 return false;
1435 };
1436
1437 if epoch != self.web_init_epoch {
1438 return false;
1439 }
1440
1441 let mut context = PipelineContext::new(&mut render_core);
1442 for module in &self.modules {
1443 module.register_pipelines(&mut context);
1444 }
1445
1446 self.app = Some(render_core);
1447 self.web_init_in_progress = false;
1448 self.apply_pending_window_size();
1449 self.install_runtime_redraw_waker();
1450 #[cfg(feature = "profiling")]
1451 self.request_redraw_with_reasons(WakeSource::Lifecycle, vec![RedrawReason::Startup]);
1452 #[cfg(not(feature = "profiling"))]
1453 self.request_redraw_now();
1454
1455 if let Some(context) = self.plugin_context(event_loop) {
1456 self.plugins.resumed(&context);
1457 }
1458 true
1459 }
1460
1461 #[cfg(feature = "profiling")]
1462 fn request_redraw_with_reasons(&mut self, source: WakeSource, mut reasons: Vec<RedrawReason>) {
1463 let _session = self.session.enter();
1464 reasons.sort_unstable();
1465 reasons.dedup();
1466 if reasons.is_empty() {
1467 return;
1468 }
1469
1470 self.pending_redraw_reasons.extend(reasons.iter().copied());
1471
1472 submit_wake_meta(WakeMeta {
1473 frame_idx: self.frame_index,
1474 source,
1475 reasons: reasons.clone(),
1476 });
1477
1478 self.request_redraw_now();
1479 }
1480
1481 #[cfg(feature = "profiling")]
1482 fn take_pending_redraw_reasons(&mut self) -> Vec<RedrawReason> {
1483 let _session = self.session.enter();
1484 std::mem::take(&mut self.pending_redraw_reasons)
1485 .into_iter()
1486 .collect()
1487 }
1488
1489 fn handle_close_requested(&mut self, event_loop: &ActiveEventLoop) {
1491 let _session = self.session.enter();
1492 if let Some(context) = self.plugin_context(event_loop) {
1493 self.plugins.shutdown(&context);
1494 }
1495 if let Some(ref app) = self.app
1496 && let Err(e) = app.save_pipeline_cache()
1497 {
1498 warn!("Failed to save pipeline cache: {}", e);
1499 }
1500 event_loop.exit();
1501 }
1502
1503 fn apply_window_drag(&mut self, window: &Window, _request: WindowDragRequest) {
1504 let _session = self.session.enter();
1505 if !desktop_window_controls_available() {
1506 return;
1507 }
1508 #[cfg(target_os = "macos")]
1509 if let Some(press) = _request.mouse_press {
1510 self.macos_titlebar.handle_press(press);
1511 self.update_native_window_shape(window);
1512 return;
1513 }
1514 if let Err(err) = window.drag_window() {
1515 warn!("Failed to start window drag: {}", err);
1516 }
1517 self.update_native_window_shape(window);
1518 }
1519
1520 fn apply_desktop_window_action(&mut self, window: &Window, action: DesktopWindowAction) {
1521 let _session = self.session.enter();
1522 match action {
1523 DesktopWindowAction::Minimize => {
1524 window.set_minimized(true);
1525 }
1526 DesktopWindowAction::Maximize => {
1527 window.set_maximized(true);
1528 }
1529 DesktopWindowAction::ToggleMaximize => {
1530 window.set_maximized(!window.is_maximized());
1531 }
1532 DesktopWindowAction::Close => {
1533 self.pending_close_requested = true;
1534 }
1535 }
1536 self.update_native_window_shape(window);
1537 }
1538
1539 fn apply_pending_window_size(&mut self) {
1543 let _session = self.session.enter();
1544 let live_size = self
1545 .app
1546 .as_ref()
1547 .map(|app| window_drawable_size(app.window()));
1548 let size = resolve_startup_window_size(self.pending_window_size.take(), live_size);
1549 let Some(size) = size else {
1550 return;
1551 };
1552 self.handle_resized(size);
1553 }
1554
1555 fn handle_resized(&mut self, size: winit::dpi::PhysicalSize<u32>) {
1556 let _session = self.session.enter();
1557 let app = match self.app.as_mut() {
1560 Some(app) => app,
1561 None => {
1562 self.pending_window_size = Some(size);
1567 return;
1568 }
1569 };
1570 let window = app.window_arc();
1571 #[cfg(target_family = "wasm")]
1572 let size = if size.width > 0 && size.height > 0 {
1573 web_canvas_physical_size(&window).unwrap_or(size)
1574 } else {
1575 size
1576 };
1577
1578 if size.width == 0 || size.height == 0 {
1579 TesseraRuntime::with_mut(|rt| {
1580 if !rt.window_minimized {
1581 rt.window_minimized = true;
1582 }
1583 });
1584 } else {
1585 TesseraRuntime::with_mut(|rt| {
1586 if rt.window_minimized {
1587 rt.window_minimized = false;
1588 }
1589 });
1590 app.resize(size);
1591 }
1592 self.update_native_window_shape(&window);
1593 }
1594
1595 fn handle_cursor_moved(&mut self, position: winit::dpi::PhysicalPosition<f64>) {
1596 let _session = self.session.enter();
1597 if self.resize_in_progress {
1598 return;
1599 }
1600 let px_position = PxPosition::from_f64_arr2([position.x, position.y]);
1601 self.cursor_state.update_position(px_position);
1603 self.cursor_state.push_event(PointerChange {
1604 timestamp: Instant::now(),
1605 pointer_id: MOUSE_POINTER_ID,
1606 content: CursorEventContent::Moved(px_position),
1607 gesture_state: GestureState::TapCandidate,
1608 sampled_position: Default::default(),
1609 consumed: false,
1610 tap_claimed: false,
1611 scroll_discrete: None,
1612 });
1613 debug!("Cursor moved to: {}, {}", position.x, position.y);
1614 }
1615
1616 fn handle_cursor_left(&mut self) {
1617 let _session = self.session.enter();
1618 self.cursor_state.clear();
1621 debug!("Cursor left the window");
1622 }
1623
1624 fn push_accessibility_update(&mut self, tree_update: TreeUpdate) {
1625 let _session = self.session.enter();
1626 if let Some(adapter) = self.accessibility_adapter.as_mut() {
1627 adapter.update_if_active(|| tree_update);
1628 }
1629 }
1630
1631 fn send_accessibility_update(&mut self) {
1632 let _session = self.session.enter();
1633 if let Some(tree_update) = Self::build_accessibility_update(&self.config.window_title, true)
1634 {
1635 self.push_accessibility_update(tree_update);
1636 }
1637 }
1638
1639 fn build_accessibility_update(window_label: &str, full: bool) -> Option<TreeUpdate> {
1640 TesseraRuntime::with_mut(|runtime| {
1641 runtime
1642 .component_tree
1643 .accessibility_update(Some(window_label), full)
1644 })
1645 }
1646
1647 fn handle_mouse_input(
1648 &mut self,
1649 state: winit::event::ElementState,
1650 button: winit::event::MouseButton,
1651 ) {
1652 let _session = self.session.enter();
1653 let Some(event_content) = CursorEventContent::from_press_event(state, button) else {
1654 return; };
1656
1657 if self.resize_in_progress {
1658 if matches!(
1659 event_content,
1660 CursorEventContent::Released(PressKeyEventType::Left)
1661 ) {
1662 self.resize_in_progress = false;
1663 }
1664 return;
1665 }
1666
1667 if matches!(
1668 event_content,
1669 CursorEventContent::Pressed(PressKeyEventType::Left)
1670 ) && desktop_window_controls_available()
1671 && !self.config.window.decorations
1672 && let Some(app) = self.app.as_ref()
1673 {
1674 let window_size = app.size();
1675 let direction = Self::cursor_resize_direction(
1676 self.cursor_state.position(),
1677 window_size,
1678 Self::RESIZE_EDGE_THRESHOLD,
1679 );
1680 if let Some(direction) = direction {
1681 if let Err(err) = app.window().drag_resize_window(direction) {
1682 warn!("Failed to start window resize: {}", err);
1683 } else {
1684 self.resize_in_progress = true;
1685 self.cursor_state.clear();
1686 debug!("Started native border resize; suppressing cursor input");
1687 }
1688 return;
1689 }
1690 }
1691 let event = PointerChange {
1692 timestamp: Instant::now(),
1693 pointer_id: MOUSE_POINTER_ID,
1694 content: event_content,
1695 gesture_state: GestureState::TapCandidate,
1696 sampled_position: Default::default(),
1697 consumed: false,
1698 tap_claimed: false,
1699 scroll_discrete: None,
1700 };
1701 #[cfg(target_os = "macos")]
1702 if matches!(
1703 event.content,
1704 CursorEventContent::Pressed(PressKeyEventType::Left)
1705 ) {
1706 self.macos_titlebar.record_press(event.timestamp);
1707 }
1708 if state == winit::event::ElementState::Pressed {
1709 TesseraRuntime::with_mut(|runtime| {
1710 runtime
1711 .component_tree
1712 .focus_owner_mut()
1713 .record_pointer_press(event.timestamp);
1714 });
1715 }
1716 self.cursor_state.push_event(event);
1717 debug!("Mouse input: {state:?} button {button:?}");
1718 }
1719
1720 fn handle_mouse_wheel(&mut self, delta: winit::event::MouseScrollDelta) {
1721 let _session = self.session.enter();
1722 if self.resize_in_progress {
1723 return;
1724 }
1725 let scroll = ScrollEventContent::from_wheel(delta);
1726 let discrete = crate::scroll::is_discrete_platform_wheel(&scroll);
1727 if !scroll.delta_x.is_finite()
1728 || !scroll.delta_y.is_finite()
1729 || (scroll.delta_x == 0.0 && scroll.delta_y == 0.0)
1730 {
1731 return;
1732 }
1733 let mut event = PointerChange::new(
1734 Instant::now(),
1735 MOUSE_POINTER_ID,
1736 CursorEventContent::Scroll(scroll),
1737 GestureState::Dragged,
1738 );
1739 event.scroll_discrete = Some(discrete);
1740 self.cursor_state.push_event(event);
1741 debug!("Mouse scroll: {delta:?}");
1742 }
1743
1744 fn handle_touch(&mut self, touch_event: winit::event::Touch) {
1745 let _session = self.session.enter();
1746 if self.resize_in_progress {
1747 return;
1748 }
1749 let pos = PxPosition::from_f64_arr2([touch_event.location.x, touch_event.location.y]);
1750 debug!(
1751 "Touch event: id {}, phase {:?}, position {:?}",
1752 touch_event.id, touch_event.phase, pos
1753 );
1754 match touch_event.phase {
1755 winit::event::TouchPhase::Started => {
1756 TesseraRuntime::with_mut(|runtime| {
1757 runtime
1758 .component_tree
1759 .focus_owner_mut()
1760 .record_pointer_press(Instant::now());
1761 });
1762 self.cursor_state.handle_touch_start(touch_event.id, pos);
1764 }
1765 winit::event::TouchPhase::Moved => {
1766 if let Some(scroll_event) = self.cursor_state.handle_touch_move(touch_event.id, pos)
1768 {
1769 self.cursor_state.push_event(scroll_event);
1772 }
1773 }
1774 winit::event::TouchPhase::Ended => {
1775 self.cursor_state.handle_touch_end(touch_event.id);
1776 }
1777 winit::event::TouchPhase::Cancelled => {
1778 self.cursor_state.handle_touch_cancel(touch_event.id);
1779 }
1780 }
1781 }
1782
1783 fn handle_keyboard_input(&mut self, event: winit::event::KeyEvent) {
1784 let _session = self.session.enter();
1785 debug!("Keyboard input: {event:?}");
1786 TesseraRuntime::with_mut(|runtime| {
1787 runtime
1788 .component_tree
1789 .focus_owner_mut()
1790 .record_keyboard_input(event.state, Instant::now());
1791 });
1792 self.keyboard_state.push_event(event.into());
1793 }
1794
1795 fn flush_focused_input(&mut self) {
1796 if self.app.is_none() {
1797 return;
1798 }
1799 let modifiers = self.keyboard_state.modifiers();
1800 let keyboard = self.keyboard_state.take_events();
1801 let ime = self.ime_state.take_events();
1802 let requests = TesseraRuntime::with_mut(|runtime| {
1803 runtime.component_tree.flush_focused_input(
1804 &mut self.cursor_state,
1805 modifiers,
1806 keyboard,
1807 ime,
1808 )
1809 });
1810 flush_pending_focus_callbacks();
1811 if let Some(request) = requests.request_window_drag {
1812 let window = self.app.as_ref().unwrap().window_arc();
1813 self.apply_window_drag(&window, request);
1814 }
1815 let request = requests.ime_request.filter(|request| {
1816 TesseraRuntime::with(|runtime| {
1817 request.client_id == runtime.component_tree.focus_owner().active_handle_id()
1818 })
1819 });
1820 self.ime_bridge_state
1822 .sync_window(self.app.as_ref().unwrap().window(), request);
1823 }
1824
1825 fn handle_redraw_requested(&mut self, event_loop: &ActiveEventLoop) {
1826 let _session = self.session.enter();
1827 self.redraw_request_pending.store(false, Ordering::Release);
1828 let mut app = match self.app.take() {
1829 Some(app) => app,
1830 None => return,
1831 };
1832
1833 #[cfg(feature = "profiling")]
1834 let frame_timer = Instant::now();
1835 crate::window_insets::platform::refresh(app.window(), event_loop);
1836 app.resize_if_needed();
1837 let accessibility_enabled =
1838 self.accessibility_adapter.is_some() && self.accessibility_active;
1839 let frame_idx = self.frame_index;
1840 #[cfg(feature = "profiling")]
1841 let redraw_reasons = self.take_pending_redraw_reasons();
1842 let window_label = &self.config.window_title;
1843 let RenderFrameOutcome {
1844 accessibility_update,
1845 request_window_drag,
1846 runtime_pending_work,
1847 #[cfg(feature = "profiling")]
1848 mut frame_meta,
1849 #[cfg(feature = "debug-dirty-overlay")]
1850 overlay_clear_pending,
1851 } = {
1852 let mut args = RenderFrameArgs {
1853 cursor_state: &mut self.cursor_state,
1854 keyboard_state: &mut self.keyboard_state,
1855 ime_state: &mut self.ime_state,
1856 ime_bridge_state: &mut self.ime_bridge_state,
1857 app: &mut app,
1858 };
1859 Self::execute_render_frame(RenderFrameContext {
1860 entry_point: &self.entry_point,
1861 args: &mut args,
1862 accessibility_enabled,
1863 decorations: self.config.window.decorations,
1864 window_label,
1865 frame_idx,
1866 #[cfg(feature = "profiling")]
1867 redraw_reasons,
1868 })
1869 };
1870
1871 if let Some(request) = request_window_drag {
1872 self.apply_window_drag(app.window(), request);
1873 }
1874 #[cfg(target_os = "macos")]
1875 self.macos_titlebar.clear();
1876 if let Some(action) = self.take_pending_desktop_window_action() {
1877 self.apply_desktop_window_action(app.window(), action);
1878 }
1879
1880 self.frame_index = self.frame_index.wrapping_add(1);
1881
1882 if let Some(tree_update) = accessibility_update {
1883 self.push_accessibility_update(tree_update);
1884 }
1885
1886 #[cfg(feature = "profiling")]
1887 {
1888 frame_meta.frame_total_ns = Some(frame_timer.elapsed().as_nanos());
1889 submit_frame_meta(frame_meta);
1890 profiler_end_frame();
1891 }
1892
1893 self.app = Some(app);
1894 #[cfg(target_os = "android")]
1895 if let Some(back) = self.android_back.as_mut() {
1896 back.set_enabled(crate::system_back::enabled());
1897 }
1898
1899 if runtime_pending_work.requires_redraw() {
1900 #[cfg(feature = "profiling")]
1901 self.request_redraw_with_reasons(
1902 WakeSource::Runtime,
1903 runtime_pending_work.redraw_reasons(),
1904 );
1905 #[cfg(not(feature = "profiling"))]
1906 self.request_redraw_now();
1907 }
1908 #[cfg(feature = "debug-dirty-overlay")]
1909 if overlay_clear_pending {
1910 self.request_redraw_now();
1911 }
1912 }
1913}
1914
1915#[cfg(target_family = "wasm")]
1916fn web_canvas_physical_size(window: &Window) -> Option<winit::dpi::PhysicalSize<u32>> {
1917 use winit::platform::web::WindowExtWebSys;
1918
1919 let canvas = window.canvas()?;
1920 let style = web_sys::window()?.get_computed_style(&canvas).ok()??;
1921 let pixels = |property: &str| -> Option<f64> {
1922 style
1923 .get_property_value(property)
1924 .ok()?
1925 .strip_suffix("px")?
1926 .parse()
1927 .ok()
1928 };
1929 let mut width = pixels("width")?;
1930 let mut height = pixels("height")?;
1931 if style.get_property_value("box-sizing").ok()? == "border-box" {
1932 width -= pixels("padding-left")?
1933 + pixels("padding-right")?
1934 + pixels("border-left-width")?
1935 + pixels("border-right-width")?;
1936 height -= pixels("padding-top")?
1937 + pixels("padding-bottom")?
1938 + pixels("border-top-width")?
1939 + pixels("border-bottom-width")?;
1940 }
1941 canvas_physical_size([width, height], window.scale_factor())
1944}
1945
1946#[cfg(any(target_family = "wasm", test))]
1947fn canvas_physical_size(logical: [f64; 2], density: f64) -> Option<winit::dpi::PhysicalSize<u32>> {
1948 if !density.is_finite() || density <= 0.0 {
1949 return None;
1950 }
1951 let physical = logical.map(|size| size * density);
1952 if physical
1953 .iter()
1954 .any(|size| !size.is_finite() || *size <= 0.0 || *size > u32::MAX as f64)
1955 {
1956 return None;
1957 }
1958 Some(winit::dpi::PhysicalSize::new(
1959 physical[0].round().max(1.0) as u32,
1960 physical[1].round().max(1.0) as u32,
1961 ))
1962}
1963
1964fn window_drawable_size(window: &Window) -> winit::dpi::PhysicalSize<u32> {
1965 #[cfg(target_os = "ios")]
1967 {
1968 window.outer_size()
1969 }
1970 #[cfg(not(target_os = "ios"))]
1971 {
1972 window.inner_size()
1973 }
1974}
1975
1976fn resolve_startup_window_size(
1978 pending: Option<winit::dpi::PhysicalSize<u32>>,
1979 live: Option<winit::dpi::PhysicalSize<u32>>,
1980) -> Option<winit::dpi::PhysicalSize<u32>> {
1981 fn usable(size: &winit::dpi::PhysicalSize<u32>) -> bool {
1982 size.width > 0 && size.height > 0
1983 }
1984
1985 match pending {
1986 Some(size) if usable(&size) => Some(size),
1987 _ => live.filter(usable),
1988 }
1989}
1990
1991impl<F, Output> ApplicationHandler<RendererUserEvent> for Renderer<F>
1993where
1994 F: Fn() -> Output,
1995{
1996 #[tracing::instrument(level = "debug", skip(self, event_loop))]
1997 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
1998 let _session = self.session.enter();
1999 event_loop.set_control_flow(ControlFlow::Wait);
2000 let app_titlebar = self.config.window.app_titlebar();
2001 crate::local_runtime_resource::<Cell<AppTitleBar>>().set(app_titlebar);
2002 #[cfg(feature = "profiling")]
2003 submit_runtime_meta(RuntimeMeta {
2004 kind: RuntimeEventKind::Resumed,
2005 });
2006 #[cfg(target_family = "wasm")]
2007 self.finish_web_initialization(event_loop, self.web_init_epoch);
2008 if self.app.is_some() {
2010 self.install_runtime_redraw_waker();
2011 #[cfg(feature = "profiling")]
2012 self.request_redraw_with_reasons(WakeSource::Lifecycle, vec![RedrawReason::Startup]);
2013 #[cfg(not(feature = "profiling"))]
2014 self.request_redraw_now();
2015 return;
2016 }
2017 #[cfg(target_family = "wasm")]
2018 if self.web_init_in_progress {
2019 return;
2020 }
2021
2022 let window_attributes = Window::default_attributes()
2024 .with_title(&self.config.window_title)
2025 .with_window_icon(self.config.window.icon.clone())
2026 .with_decorations(self.config.window.decorations)
2027 .with_resizable(self.config.window.resizable)
2028 .with_transparent(self.config.window.transparent)
2029 .with_visible(false); #[cfg(target_os = "windows")]
2031 let window_attributes = {
2032 use winit::platform::windows::WindowAttributesExtWindows;
2033 if let Some(identifier) = &self.config.window.application_id {
2034 let identifier = windows::core::HSTRING::from(identifier);
2035 if let Err(error) = unsafe {
2036 windows::Win32::UI::Shell::SetCurrentProcessExplicitAppUserModelID(&identifier)
2037 } {
2038 tracing::warn!(%error, "Failed to set application identity");
2039 }
2040 }
2041 window_attributes.with_taskbar_icon(self.config.window.icon.clone())
2042 };
2043 #[cfg(any(
2044 target_os = "linux",
2045 target_os = "freebsd",
2046 target_os = "dragonfly",
2047 target_os = "netbsd",
2048 target_os = "openbsd"
2049 ))]
2050 let window_attributes = if let Some(identifier) = &self.config.window.application_id {
2051 let attributes = winit::platform::wayland::WindowAttributesExtWayland::with_name(
2052 window_attributes,
2053 identifier,
2054 identifier,
2055 );
2056 winit::platform::x11::WindowAttributesExtX11::with_name(
2057 attributes, identifier, identifier,
2058 )
2059 } else {
2060 window_attributes
2061 };
2062 #[cfg(target_os = "macos")]
2063 let window_attributes = {
2064 use winit::platform::macos::WindowAttributesExtMacOS;
2065
2066 if app_titlebar == AppTitleBar::Native {
2067 window_attributes
2068 .with_titlebar_transparent(true)
2069 .with_title_hidden(true)
2070 .with_fullsize_content_view(true)
2071 } else {
2072 window_attributes
2073 }
2074 };
2075 #[cfg(target_family = "wasm")]
2076 let window_attributes = {
2077 use winit::platform::web::WindowAttributesExtWebSys;
2078
2079 if let Some(canvas) = Self::resolve_web_canvas(&self.config) {
2080 window_attributes
2081 .with_canvas(Some(canvas))
2082 .with_append(false)
2083 } else {
2084 window_attributes.with_append(true)
2085 }
2086 };
2087 let window = match event_loop.create_window(window_attributes) {
2088 Ok(window) => Arc::new(window),
2089 Err(err) => {
2090 error!("Failed to create window: {err}");
2091 return;
2092 }
2093 };
2094
2095 if let Some(proxy) = self.event_loop_proxy.clone() {
2097 self.accessibility_active = false;
2098 self.accessibility_adapter = Some(AccessKitAdapter::with_event_loop_proxy(
2099 event_loop, &window, proxy,
2100 ));
2101 }
2102
2103 window.set_visible(true);
2105 self.update_native_window_shape(&window);
2106
2107 #[cfg(target_family = "wasm")]
2108 {
2109 self.begin_web_initialization(window);
2110 return;
2111 }
2112
2113 #[cfg(not(target_family = "wasm"))]
2114 {
2115 let mut render_core = pollster::block_on(RenderCore::new(
2116 window.clone(),
2117 self.config.sample_count,
2118 self.config.window.transparent,
2119 self.config.max_non_sampler_bindings,
2120 ));
2121
2122 let mut context = PipelineContext::new(&mut render_core);
2124 for module in &self.modules {
2125 module.register_pipelines(&mut context);
2126 }
2127
2128 self.app = Some(render_core);
2129 self.apply_pending_window_size();
2130 self.install_runtime_redraw_waker();
2131 #[cfg(feature = "profiling")]
2132 self.request_redraw_with_reasons(WakeSource::Lifecycle, vec![RedrawReason::Startup]);
2133 #[cfg(not(feature = "profiling"))]
2134 self.request_redraw_now();
2135
2136 if let Some(context) = self.plugin_context(event_loop) {
2137 self.plugins.resumed(&context);
2138 }
2139 }
2140 }
2141
2142 fn suspended(&mut self, event_loop: &ActiveEventLoop) {
2143 let _session = self.session.enter();
2144 debug!("Suspending renderer; tearing down WGPU resources.");
2145 #[cfg(target_os = "android")]
2146 if let Some(back) = self.android_back.as_mut() {
2147 back.set_enabled(false);
2148 crate::system_back::dispatch(crate::system_back::SystemBackEvent::Cancelled);
2149 }
2150 #[cfg(feature = "profiling")]
2151 submit_runtime_meta(RuntimeMeta {
2152 kind: RuntimeEventKind::Suspended,
2153 });
2154
2155 if let Some(context) = self.plugin_context(event_loop) {
2156 self.plugins.suspended(&context);
2157 }
2158
2159 self.app = None;
2160
2161 self.accessibility_adapter = None;
2163 self.accessibility_active = false;
2164
2165 self.cursor_state = CursorState::default();
2166 self.keyboard_state = KeyboardState::default();
2167 self.ime_state = ImeState::default();
2168 self.ime_bridge_state.reset();
2169 self.resize_in_progress = false;
2170 self.redraw_request_pending.store(false, Ordering::Release);
2171 #[cfg(target_family = "wasm")]
2172 {
2173 self.web_init_epoch = self.web_init_epoch.wrapping_add(1);
2174 self.web_init_in_progress = false;
2175 self.pending_web_inits.borrow_mut().clear();
2176 }
2177
2178 TesseraRuntime::with_mut(|runtime| {
2179 runtime.component_tree.reset();
2180 runtime.cursor_icon_request = None;
2181 runtime.window_minimized = false;
2182 runtime.window_size = [0, 0];
2183 });
2184 clear_layout_snapshots();
2185 reset_layout_dirty_tracking();
2186 reset_component_replay_tracking();
2187 reset_focus_read_dependencies();
2188 reset_render_slot_read_dependencies();
2189 reset_state_read_dependencies();
2190 reset_context_read_dependencies();
2191 reset_build_invalidations();
2192 reset_frame_clock();
2193 clear_redraw_waker();
2194 clear_persistent_focus_handles();
2195 crate::runtime::reset_component_slots();
2196 #[cfg(feature = "profiling")]
2197 self.pending_redraw_reasons.clear();
2198 }
2199
2200 fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
2201 let _session = self.session.enter();
2202 if cfg!(target_os = "ios") && self.redraw_request_pending.load(Ordering::Acquire) {
2203 self.handle_redraw_requested(event_loop);
2205 } else if let Some(app) = self.app.as_ref() {
2206 crate::window_insets::platform::refresh(app.window(), event_loop);
2207 }
2208 #[cfg(target_os = "ios")]
2209 {
2210 let flow = if self.redraw_request_pending.load(Ordering::Acquire) {
2211 ControlFlow::Poll
2212 } else {
2213 ControlFlow::Wait
2214 };
2215 event_loop.set_control_flow(flow);
2216 }
2217 }
2218
2219 #[tracing::instrument(level = "debug", skip(self, event_loop))]
2220 fn window_event(
2221 &mut self,
2222 event_loop: &ActiveEventLoop,
2223 _window_id: WindowId,
2224 event: WindowEvent,
2225 ) {
2226 let _session = self.session.enter();
2227 if self.pending_close_requested {
2228 self.pending_close_requested = false;
2229 self.handle_close_requested(event_loop);
2230 return;
2231 }
2232
2233 if let (Some(adapter), Some(app)) = (&mut self.accessibility_adapter, &self.app) {
2235 adapter.process_event(app.window(), &event);
2236 }
2237
2238 let mut request_redraw = false;
2240 #[cfg(feature = "profiling")]
2241 let mut redraw_reasons = Vec::new();
2242 match event {
2243 WindowEvent::CloseRequested => {
2244 self.handle_close_requested(event_loop);
2245 }
2246 WindowEvent::Resized(size) => {
2247 self.handle_resized(size);
2248 request_redraw = true;
2249 #[cfg(feature = "profiling")]
2250 redraw_reasons.push(RedrawReason::WindowResized);
2251 }
2252 WindowEvent::CursorMoved {
2253 device_id: _,
2254 position,
2255 } => {
2256 self.handle_cursor_moved(position);
2257 request_redraw = true;
2258 #[cfg(feature = "profiling")]
2259 redraw_reasons.push(RedrawReason::CursorMoved);
2260 }
2261 WindowEvent::CursorLeft { device_id: _ } => {
2262 self.handle_cursor_left();
2263 request_redraw = true;
2264 #[cfg(feature = "profiling")]
2265 redraw_reasons.push(RedrawReason::CursorLeft);
2266 }
2267 WindowEvent::MouseInput {
2268 device_id: _,
2269 state,
2270 button,
2271 } => {
2272 self.handle_mouse_input(state, button);
2273 request_redraw = true;
2274 #[cfg(feature = "profiling")]
2275 redraw_reasons.push(RedrawReason::MouseInput);
2276 }
2277 WindowEvent::MouseWheel {
2278 device_id: _,
2279 delta,
2280 phase: _,
2281 } => {
2282 self.handle_mouse_wheel(delta);
2283 request_redraw = true;
2284 #[cfg(feature = "profiling")]
2285 redraw_reasons.push(RedrawReason::MouseWheel);
2286 }
2287 WindowEvent::Touch(touch_event) => {
2288 self.handle_touch(touch_event);
2289 request_redraw = true;
2290 #[cfg(feature = "profiling")]
2291 redraw_reasons.push(RedrawReason::TouchInput);
2292 }
2293 WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
2294 set_scale_factor(scale_factor);
2295 #[cfg(target_family = "wasm")]
2296 if let Some(size) = self
2297 .app
2298 .as_ref()
2299 .map(|app| window_drawable_size(app.window()))
2300 {
2301 self.handle_resized(size);
2302 }
2303 if let Some(app) = self.app.as_ref() {
2304 self.update_native_window_shape(app.window());
2305 }
2306 request_redraw = true;
2307 #[cfg(feature = "profiling")]
2308 redraw_reasons.push(RedrawReason::ScaleFactorChanged);
2309 }
2310 WindowEvent::KeyboardInput { event, .. } => {
2311 self.handle_keyboard_input(event);
2312 self.flush_focused_input();
2313 request_redraw = true;
2314 #[cfg(feature = "profiling")]
2315 redraw_reasons.push(RedrawReason::KeyboardInput);
2316 }
2317 WindowEvent::ModifiersChanged(modifiers) => {
2318 debug!("Modifiers changed: {modifiers:?}");
2319 self.keyboard_state.update_modifiers(modifiers.state());
2320 request_redraw = true;
2321 #[cfg(feature = "profiling")]
2322 redraw_reasons.push(RedrawReason::ModifiersChanged);
2323 }
2324 WindowEvent::Ime(ime_event) => {
2325 debug!("IME event: {ime_event:?}");
2326 if let Some(client) = self.ime_bridge_state.receive_event(&ime_event) {
2327 self.ime_state.push_event_for(Some(client), ime_event);
2328 self.flush_focused_input();
2329 }
2330 request_redraw = true;
2331 #[cfg(feature = "profiling")]
2332 redraw_reasons.push(RedrawReason::ImeEvent);
2333 }
2334 WindowEvent::Focused(focused) => {
2335 TesseraRuntime::with_mut(|runtime| {
2336 runtime
2337 .component_tree
2338 .focus_owner_mut()
2339 .set_owner_focused(focused);
2340 });
2341 flush_pending_focus_callbacks();
2342 if self.resize_in_progress {
2343 self.resize_in_progress = false;
2344 self.cursor_state.clear();
2345 }
2346 request_redraw = true;
2347 #[cfg(feature = "profiling")]
2348 redraw_reasons.push(RedrawReason::FocusChanged);
2349 }
2350 WindowEvent::RedrawRequested => {
2351 self.handle_redraw_requested(event_loop);
2352 }
2353 _ => (),
2354 }
2355
2356 if request_redraw {
2357 #[cfg(feature = "profiling")]
2358 self.request_redraw_with_reasons(WakeSource::WindowEvent, redraw_reasons);
2359 #[cfg(not(feature = "profiling"))]
2360 self.request_redraw_now();
2361 }
2362 }
2363
2364 fn user_event(&mut self, event_loop: &ActiveEventLoop, event: RendererUserEvent) {
2365 let _session = self.session.enter();
2366 use accesskit_winit::WindowEvent as AccessKitWindowEvent;
2367 #[cfg(not(target_family = "wasm"))]
2368 let _ = event_loop;
2369
2370 match event {
2371 RendererUserEvent::RuntimeRedrawWake => {
2372 let pending = RuntimePendingWork::capture(false);
2374 if !pending.requires_redraw() {
2375 return;
2376 }
2377 #[cfg(feature = "profiling")]
2378 self.request_redraw_with_reasons(WakeSource::Runtime, pending.redraw_reasons());
2379 #[cfg(not(feature = "profiling"))]
2380 self.request_redraw_now();
2381 }
2382 #[cfg(target_family = "wasm")]
2383 RendererUserEvent::WebInitReady(epoch) => {
2384 self.finish_web_initialization(event_loop, epoch);
2385 }
2386 RendererUserEvent::AccessKit(event) => {
2387 if self.accessibility_adapter.is_none() {
2388 return;
2389 }
2390 match event.window_event {
2391 AccessKitWindowEvent::InitialTreeRequested => {
2392 self.accessibility_active = true;
2393 self.send_accessibility_update();
2394 }
2395 AccessKitWindowEvent::ActionRequested(action_request) => {
2396 let handled = TesseraRuntime::with_mut(|runtime| {
2398 let (tree, metadatas, focus_owner) =
2399 runtime.component_tree.accessibility_dispatch_context();
2400 crate::accessibility::dispatch_action(
2401 tree,
2402 metadatas,
2403 focus_owner,
2404 action_request,
2405 )
2406 });
2407 flush_pending_focus_callbacks();
2408
2409 if !handled {
2410 debug!("Action was not handled by any component");
2411 }
2412 }
2413 AccessKitWindowEvent::AccessibilityDeactivated => {
2414 self.accessibility_active = false;
2415 TesseraRuntime::with_mut(|runtime| {
2416 runtime.component_tree.stop_accessibility_updates();
2417 });
2418 debug!("AccessKit deactivated");
2419 }
2420 }
2421 }
2422 }
2423 }
2424}
2425
2426#[cfg(test)]
2427mod tests {
2428
2429 use winit::dpi::PhysicalSize;
2430
2431 use crate::{
2432 FocusRequester, Modifier, PxRect, State,
2433 component_tree::dispatch_pending_focus_reveal_request,
2434 focus::flush_pending_focus_callbacks,
2435 layout::{LayoutResult, MeasureScope, layout},
2436 modifier::{FocusModifierExt as _, LayoutModifierNode},
2437 remember,
2438 runtime::{TesseraRuntime, prune_persistent_focus_handles},
2439 testing::{layout_test, pump_layout_test_session, start_layout_test_session},
2440 };
2441
2442 use super::{
2443 RendererImeBridgeState, RendererImeBridgeUpdate, RuntimePendingWork, canvas_physical_size,
2444 resolve_startup_window_size,
2445 };
2446
2447 #[derive(Clone, Default, PartialEq)]
2448 struct FrameEndProbe {
2449 focus: crate::testing::TestProbe<Option<(State<bool>, FocusRequester, FocusRequester)>>,
2450 events: crate::testing::TestProbe<usize>,
2451 reveals: crate::testing::TestProbe<Vec<PxRect>>,
2452 }
2453
2454 #[derive(Clone, PartialEq)]
2455 struct FocusLeafLayout;
2456
2457 impl LayoutModifierNode for FocusLeafLayout {
2458 fn measure(
2459 &self,
2460 _scope: &mut MeasureScope<'_>,
2461 ) -> Result<LayoutResult, crate::MeasurementError> {
2462 Ok(LayoutResult::new(crate::ComputedData {
2463 width: crate::Px(64),
2464 height: crate::Px(24),
2465 }))
2466 }
2467 }
2468
2469 #[crate::tessera(crate)]
2470 fn frame_end_removable_target(requester: FocusRequester) {
2471 layout()
2472 .policy(FocusLeafLayout)
2473 .modifier(Modifier::new().focusable().focus_requester(requester));
2474 }
2475
2476 #[crate::tessera(crate)]
2477 fn frame_end_focus_fixture(probe: FrameEndProbe) {
2478 let visible = remember(|| true);
2479 let removed = remember(FocusRequester::new).get();
2480 let fallback = remember(FocusRequester::new).get();
2481 probe.focus.set(Some((visible, removed, fallback)));
2482 let reveals = probe.reveals.clone();
2483 let events = probe.events.clone();
2484 layout()
2485 .modifier(
2486 Modifier::new()
2487 .focus_restorer(Some(fallback))
2488 .focus_reveal_handler(move |request: crate::focus::FocusRevealRequest| {
2489 reveals.with_mut(|rects| rects.push(request.target_rect));
2490 false
2491 }),
2492 )
2493 .child(move || {
2494 let events = events.clone();
2495 if visible.get() {
2496 frame_end_removable_target(removed);
2497 }
2498 layout().policy(FocusLeafLayout).modifier(
2499 Modifier::new()
2500 .focusable()
2501 .focus_requester(fallback)
2502 .on_focus_changed(move |state: crate::focus::FocusState| {
2503 if state.is_focused() {
2504 events.with_mut(|events| *events += 1);
2505 }
2506 }),
2507 );
2508 });
2509 }
2510
2511 #[derive(Clone, Default, PartialEq)]
2512 struct VisibilityProbe {
2513 focus: crate::testing::TestProbe<Option<(FocusRequester, FocusRequester)>>,
2514 reads: crate::testing::TestProbe<Vec<bool>>,
2515 }
2516
2517 #[crate::tessera(crate)]
2518 fn focus_visibility_observer(
2519 requester: FocusRequester,
2520 reads: crate::testing::TestProbe<Vec<bool>>,
2521 ) {
2522 reads.with_mut(|reads| reads.push(requester.is_focus_visible()));
2523 layout().policy(FocusLeafLayout);
2524 }
2525
2526 #[crate::tessera(crate)]
2527 fn focus_visibility_fixture(probe: VisibilityProbe) {
2528 let first = remember(FocusRequester::new).get();
2529 let second = remember(FocusRequester::new).get();
2530 probe.focus.set(Some((first, second)));
2531 layout().child(move || {
2532 frame_end_removable_target(first);
2533 frame_end_removable_target(second);
2534 focus_visibility_observer(first, probe.reads.clone());
2535 });
2536 }
2537
2538 #[test]
2539 fn focus_visibility_replays_subscribers_without_changing_same_target_ownership() {
2540 let _runtime_session = crate::RuntimeSession::new();
2541 let _runtime_scope = _runtime_session.enter();
2542 use std::time::Duration;
2543
2544 use winit::event::ElementState;
2545
2546 use crate::{
2547 CursorEventContent, GestureState, MOUSE_POINTER_ID, PointerChange, PxPosition,
2548 cursor::PressKeyEventType, testing::pump_layout_test_session_with_input, time::Instant,
2549 };
2550
2551 let probe = VisibilityProbe::default();
2552 let mut session =
2553 start_layout_test_session(layout_test(|| focus_visibility_fixture(probe.clone())));
2554 pump_layout_test_session(&mut session, 0);
2555 let (first, second) = probe.focus.get().unwrap();
2556 first.request_focus();
2557 first.capture_focus();
2558 pump_layout_test_session(&mut session, 1);
2559 assert!(!first.is_focus_visible());
2560 let origin = Instant::now();
2561 let keyboard = origin + Duration::from_millis(10);
2562 let press = origin + Duration::from_millis(20);
2563 let newer_keyboard = origin + Duration::from_millis(30);
2564 let key = |timestamp| {
2565 TesseraRuntime::with_mut(|runtime| {
2566 runtime
2567 .component_tree
2568 .focus_owner_mut()
2569 .record_keyboard_input(ElementState::Pressed, timestamp);
2570 });
2571 };
2572 key(keyboard);
2573 pump_layout_test_session(&mut session, 2);
2574 assert!(first.is_captured());
2575 assert_eq!(probe.reads.with(|reads| reads.last().copied()), Some(true));
2576 let pointer = |timestamp| PointerChange {
2577 timestamp,
2578 pointer_id: MOUSE_POINTER_ID,
2579 content: CursorEventContent::Pressed(PressKeyEventType::Left),
2580 gesture_state: GestureState::TapCandidate,
2581 sampled_position: Default::default(),
2582 consumed: false,
2583 tap_claimed: false,
2584 scroll_discrete: None,
2585 };
2586 pump_layout_test_session_with_input(
2587 &mut session,
2588 3,
2589 vec![pointer(press)],
2590 Some(PxPosition::ZERO),
2591 );
2592 pump_layout_test_session(&mut session, 4);
2593 assert!(first.is_captured());
2594 assert_eq!(probe.reads.with(|reads| reads.last().copied()), Some(false));
2595 key(keyboard);
2596 first.request_focus();
2597 pump_layout_test_session(&mut session, 5);
2598 assert!(!first.is_focus_visible());
2599 key(newer_keyboard);
2600 pump_layout_test_session_with_input(
2601 &mut session,
2602 6,
2603 vec![pointer(press)],
2604 Some(PxPosition::ZERO),
2605 );
2606 pump_layout_test_session(&mut session, 7);
2607 assert!(first.is_captured());
2608 assert_eq!(probe.reads.with(|reads| reads.last().copied()), Some(true));
2609 let mut hover = pointer(origin + Duration::from_millis(40));
2610 hover.content = CursorEventContent::Moved(PxPosition::ZERO);
2611 let mut wheel = pointer(origin + Duration::from_millis(50));
2612 wheel.content = CursorEventContent::Scroll(crate::cursor::ScrollEventContent {
2613 delta_x: 0.0,
2614 delta_y: -24.0,
2615 unit: crate::cursor::ScrollDeltaUnit::Pixel,
2616 source: crate::cursor::ScrollEventSource::Wheel,
2617 });
2618 pump_layout_test_session_with_input(
2619 &mut session,
2620 8,
2621 vec![hover, wheel],
2622 Some(PxPosition::ZERO),
2623 );
2624 assert!(first.is_captured());
2625 assert!(first.is_focus_visible());
2626 first.free_focus();
2627 second.request_focus();
2628 assert!(second.is_focus_visible());
2629 assert!(!first.is_focus_visible());
2630 pump_layout_test_session_with_input(
2631 &mut session,
2632 9,
2633 vec![pointer(origin + Duration::from_millis(60))],
2634 Some(PxPosition::ZERO),
2635 );
2636 first.request_focus();
2637 assert!(first.is_focused());
2638 assert!(!first.is_focus_visible());
2639 }
2640
2641 fn dispatch_frame_end_reveal() {
2642 TesseraRuntime::with_mut(|runtime| {
2643 let (tree, metadatas, focus_owner) =
2644 runtime.component_tree.accessibility_dispatch_context();
2645 dispatch_pending_focus_reveal_request(tree, metadatas, focus_owner, false);
2646 });
2647 }
2648
2649 #[test]
2650 fn pruned_focus_restoration_keeps_a_frame_scheduled_until_its_reveal_is_dispatched() {
2651 let _runtime_session = crate::RuntimeSession::new();
2652 let _runtime_scope = _runtime_session.enter();
2653 let probe = FrameEndProbe::default();
2654 let mut session =
2655 start_layout_test_session(layout_test(|| frame_end_focus_fixture(probe.clone())));
2656 pump_layout_test_session(&mut session, 0);
2657 let (visible, removed, fallback) = probe.focus.get().unwrap();
2658 removed.request_focus();
2659 assert!(removed.is_focused());
2660 dispatch_frame_end_reveal();
2661 pump_layout_test_session(&mut session, 1);
2662 assert!(!RuntimePendingWork::capture(false).requires_redraw());
2663
2664 visible.set(false);
2665 pump_layout_test_session(&mut session, 2);
2666 assert!(!fallback.is_focused());
2667 prune_persistent_focus_handles();
2668 assert!(!fallback.is_focused());
2669 assert!(RuntimePendingWork::capture(false).requires_redraw());
2670 pump_layout_test_session(&mut session, 3);
2671 prune_persistent_focus_handles();
2672 flush_pending_focus_callbacks();
2673 assert!(fallback.is_focused());
2674 assert_eq!(probe.events.get(), 1);
2675
2676 pump_layout_test_session(&mut session, 4);
2677 assert!(!crate::runtime::has_pending_build_invalidations());
2678 assert!(!crate::runtime::has_pending_layout_invalidations());
2679 assert!(!crate::runtime::has_pending_frame_nanos_receivers());
2680 assert!(!crate::runtime::has_pending_persistent_focus_prune());
2681 assert!(!crate::clipboard::has_pending_requests());
2682 assert!(!TesseraRuntime::with(|runtime| {
2683 runtime
2684 .component_tree
2685 .focus_owner()
2686 .has_pending_navigation()
2687 }));
2688 assert!(RuntimePendingWork::capture(false).requires_redraw());
2689 assert_eq!(probe.reveals.with(Vec::len), 1);
2690
2691 dispatch_frame_end_reveal();
2692 assert_eq!(
2693 probe.reveals.with(|rects| rects.clone()),
2694 vec![
2695 PxRect::new(crate::Px(0), crate::Px(0), crate::Px(64), crate::Px(24)),
2696 PxRect::new(crate::Px(0), crate::Px(0), crate::Px(64), crate::Px(24)),
2697 ]
2698 );
2699 assert!(fallback.is_focused());
2700 assert!(!RuntimePendingWork::capture(false).requires_redraw());
2701 }
2702
2703 #[derive(Clone, Default, PartialEq)]
2704 struct ColdProbe {
2705 child: crate::testing::TestProbe<Option<PxRect>>,
2706 writes: crate::testing::TestProbe<usize>,
2707 wakes: crate::testing::TestProbe<usize>,
2708 counts: crate::testing::TestProbe<(usize, usize)>,
2709 }
2710
2711 #[crate::tessera(crate)]
2712 fn cold_geometry_child(probe: crate::testing::TestProbe<Option<PxRect>>) {
2713 layout()
2714 .policy(FocusLeafLayout)
2715 .modifier(Modifier::new().on_globally_positioned(move |rect| probe.set(Some(rect))));
2716 }
2717
2718 #[crate::tessera(crate)]
2719 fn cold_geometry_fixture(probe: ColdProbe) {
2720 let observed = remember(|| None::<PxRect>);
2721 let output = probe.clone();
2722 layout()
2723 .modifier(Modifier::new().on_globally_positioned(move |rect| {
2724 if observed.get() != Some(rect) {
2725 let before = output.wakes.get();
2726 observed.set(Some(rect));
2727 output.counts.set((before, output.wakes.get()));
2728 output.writes.set(output.writes.get() + 1);
2729 }
2730 }))
2731 .child(move || {
2732 layout().policy(FocusLeafLayout);
2733 if observed.get().is_some() {
2734 cold_geometry_child(probe.child.clone());
2735 }
2736 });
2737 }
2738
2739 #[test]
2740 fn first_geometry_publication_settles_its_conditional_child_before_the_frame_finishes() {
2741 let _runtime_session = crate::RuntimeSession::new();
2742 let _runtime_scope = _runtime_session.enter();
2743 struct WakerGuard;
2744 impl Drop for WakerGuard {
2745 fn drop(&mut self) {
2746 crate::runtime::clear_redraw_waker();
2747 }
2748 }
2749
2750 let probe = ColdProbe::default();
2751 let mut session =
2752 start_layout_test_session(layout_test(|| cold_geometry_fixture(probe.clone())));
2753 let _guard = WakerGuard;
2754 crate::runtime::install_redraw_waker(std::sync::Arc::new({
2755 let wakes = probe.wakes.clone();
2756 move || wakes.with_mut(|wakes| *wakes += 1)
2757 }));
2758 pump_layout_test_session(&mut session, 0);
2759 assert_eq!(probe.writes.get(), 1);
2760 let (before, after) = probe.counts.get();
2761 assert!(
2762 after > before,
2763 "the observer's state write must wake the runtime"
2764 );
2765 assert_eq!(
2766 probe.child.get(),
2767 Some(PxRect::new(
2768 crate::Px(0),
2769 crate::Px(0),
2770 crate::Px(64),
2771 crate::Px(24)
2772 )),
2773 "the geometry update must place the newly inserted child in the same frame"
2774 );
2775 assert_eq!(probe.writes.get(), 1);
2776 assert!(!RuntimePendingWork::capture(false).requires_redraw());
2777 let wakes = probe.wakes.get();
2778 pump_layout_test_session(&mut session, 16_666_667);
2779 assert_eq!(probe.writes.get(), 1);
2780 assert_eq!(
2781 probe.wakes.get(),
2782 wakes,
2783 "an unchanged idle layout must not wake again"
2784 );
2785 assert!(!RuntimePendingWork::capture(false).requires_redraw());
2786 }
2787
2788 #[test]
2789 fn web_canvas_size_tracks_css_density_and_fractional_dimensions() {
2790 let _runtime_session = crate::RuntimeSession::new();
2791 let _runtime_scope = _runtime_session.enter();
2792 assert_eq!(
2793 canvas_physical_size([390.0, 780.0], 1.0000000298023224),
2794 Some(PhysicalSize::new(390, 780))
2795 );
2796 assert_eq!(
2797 canvas_physical_size([877.333374, 871.333374], 1.5),
2798 Some(PhysicalSize::new(1316, 1307))
2799 );
2800 assert_eq!(
2801 canvas_physical_size([390.0, 780.0], 2.0),
2802 Some(PhysicalSize::new(780, 1560))
2803 );
2804 assert_eq!(canvas_physical_size([0.0, 780.0], 1.0), None);
2805 assert_eq!(canvas_physical_size([390.0, 780.0], f64::NAN), None);
2806 }
2807
2808 #[test]
2809 fn startup_window_size_prefers_recorded_size_over_live_size() {
2810 let _runtime_session = crate::RuntimeSession::new();
2811 let _runtime_scope = _runtime_session.enter();
2812 let recorded = PhysicalSize::new(800, 600);
2813 let live = PhysicalSize::new(1024, 768);
2814 assert_eq!(
2815 resolve_startup_window_size(Some(recorded), Some(live)),
2816 Some(recorded)
2817 );
2818 }
2819
2820 #[test]
2821 fn startup_window_size_falls_back_to_live_size_for_empty_recordings() {
2822 let _runtime_session = crate::RuntimeSession::new();
2823 let _runtime_scope = _runtime_session.enter();
2824 let live = PhysicalSize::new(1024, 768);
2825 assert_eq!(
2826 resolve_startup_window_size(Some(PhysicalSize::new(0, 0)), Some(live)),
2827 Some(live)
2828 );
2829 assert_eq!(
2830 resolve_startup_window_size(Some(PhysicalSize::new(0, 600)), Some(live)),
2831 Some(live)
2832 );
2833 }
2834
2835 #[test]
2836 fn startup_window_size_uses_live_size_without_recorded_events() {
2837 let _runtime_session = crate::RuntimeSession::new();
2838 let _runtime_scope = _runtime_session.enter();
2839 let live = PhysicalSize::new(1024, 768);
2840 assert_eq!(resolve_startup_window_size(None, Some(live)), Some(live));
2841 }
2842
2843 #[test]
2844 fn startup_window_size_is_none_when_every_reading_is_empty() {
2845 let _runtime_session = crate::RuntimeSession::new();
2846 let _runtime_scope = _runtime_session.enter();
2847 assert_eq!(
2848 resolve_startup_window_size(Some(PhysicalSize::new(0, 0)), None),
2849 None
2850 );
2851 assert_eq!(
2852 resolve_startup_window_size(None, Some(PhysicalSize::new(0, 0))),
2853 None
2854 );
2855 }
2856
2857 use crate::{ImeRequest, Px, PxPosition, px::PxSize};
2858
2859 fn positioned_request(
2860 position: PxPosition,
2861 size: PxSize,
2862 selection: std::ops::Range<usize>,
2863 composition: Option<std::ops::Range<usize>>,
2864 ) -> ImeRequest {
2865 let mut request = ImeRequest::new(size)
2866 .with_local_position(position)
2867 .with_text_state(winit::event::ImeTextState {
2868 text: "0123456789".into(),
2869 selection,
2870 composition,
2871 });
2872 request.position = Some(position);
2873 request
2874 }
2875
2876 #[test]
2877 fn ime_bridge_enables_and_updates_cursor_area_for_first_request() {
2878 let _runtime_session = crate::RuntimeSession::new();
2879 let _runtime_scope = _runtime_session.enter();
2880 let mut bridge = RendererImeBridgeState::default();
2881 let request = positioned_request(
2882 PxPosition::new(Px(12), Px(18)),
2883 PxSize::new(Px(7), Px(13)),
2884 2..6,
2885 Some(3..5),
2886 );
2887
2888 let update = bridge.update_request(Some(request.clone()));
2889
2890 assert_eq!(
2891 update,
2892 RendererImeBridgeUpdate {
2893 allowed: Some(true),
2894 cursor_area: Some((PxPosition::new(Px(12), Px(18)), PxSize::new(Px(7), Px(13)))),
2895 text_state: Some(request.text_state.clone()),
2896 options: Some(request.options),
2897 ..Default::default()
2898 }
2899 );
2900 assert_eq!(bridge.request(), Some(&request));
2901 }
2902
2903 #[test]
2904 fn ime_bridge_reuses_identical_request_without_window_updates() {
2905 let _runtime_session = crate::RuntimeSession::new();
2906 let _runtime_scope = _runtime_session.enter();
2907 let request = positioned_request(
2908 PxPosition::new(Px(12), Px(18)),
2909 PxSize::new(Px(7), Px(13)),
2910 2..6,
2911 Some(3..5),
2912 );
2913 let mut bridge = RendererImeBridgeState::default();
2914 bridge.update_request(Some(request.clone()));
2915
2916 let update = bridge.update_request(Some(request.clone()));
2917
2918 assert_eq!(update, RendererImeBridgeUpdate::default());
2919 assert_eq!(bridge.request(), Some(&request));
2920 let mut show = request.clone();
2921 show.show_keyboard = true;
2922 show.change_cause = winit::event::ImeChangeCause::InputMethod;
2923 let mut requests = crate::component_tree::WindowRequests {
2924 ime_request: Some(show),
2925 ..Default::default()
2926 };
2927 requests.settle(crate::component_tree::WindowRequests {
2928 ime_request: Some(request.clone()),
2929 ..Default::default()
2930 });
2931 let update = bridge.update_request(requests.ime_request);
2932 assert_eq!(
2933 update,
2934 RendererImeBridgeUpdate {
2935 show_keyboard: true,
2936 change_cause: winit::event::ImeChangeCause::InputMethod,
2937 ..Default::default()
2938 }
2939 );
2940 assert_eq!(
2941 bridge.update_request(Some(request)),
2942 RendererImeBridgeUpdate::default()
2943 );
2944 }
2945
2946 #[test]
2947 fn ime_bridge_preserves_selection_and_composition_changes_without_cursor_reapply() {
2948 let _runtime_session = crate::RuntimeSession::new();
2949 let _runtime_scope = _runtime_session.enter();
2950 let size = PxSize::new(Px(7), Px(13));
2951 let mut bridge = RendererImeBridgeState::default();
2952 bridge.update_request(Some(positioned_request(
2953 PxPosition::new(Px(12), Px(18)),
2954 size,
2955 2..6,
2956 Some(3..5),
2957 )));
2958 let next_request =
2959 positioned_request(PxPosition::new(Px(12), Px(18)), size, 4..8, Some(5..7));
2960
2961 let update = bridge.update_request(Some(next_request.clone()));
2962
2963 assert_eq!(update.allowed, None);
2964 assert_eq!(update.cursor_area, None);
2965 assert_eq!(update.text_state, Some(next_request.text_state.clone()));
2966 assert_eq!(bridge.request(), Some(&next_request));
2967 let mut moved = next_request.clone();
2968 moved.position = Some(PxPosition::new(Px(20), Px(30)));
2969 let update = bridge.update_request(Some(moved));
2970 assert_eq!(update.allowed, None);
2971 assert_eq!(update.text_state, None);
2972 assert_eq!(update.options, None);
2973 assert_eq!(
2974 update.cursor_area,
2975 Some((PxPosition::new(Px(20), Px(30)), size))
2976 );
2977 let mut another_client = next_request.clone();
2978 another_client.client_id = Some(42);
2979 let update = bridge.update_request(Some(another_client.clone()));
2980 assert_eq!(update.allowed, Some(true));
2981 assert_eq!(
2982 update.cursor_area,
2983 Some((PxPosition::new(Px(12), Px(18)), size))
2984 );
2985 another_client.text_state.text = "native edit".into();
2986 another_client.text_state.selection = 11..11;
2987 another_client.text_state.composition = Some(7..11);
2988 assert_eq!(
2989 bridge.receive_event(&winit::event::Ime::TextState(
2990 another_client.text_state.clone()
2991 )),
2992 Some(42)
2993 );
2994 assert_eq!(
2995 bridge.update_request(Some(another_client.clone())),
2996 RendererImeBridgeUpdate::default()
2997 );
2998 another_client.text_state.text = "transformed edit".into();
2999 let update = bridge.update_request(Some(another_client.clone()));
3000 assert_eq!(update.text_state, Some(another_client.text_state));
3001 assert_eq!(update.allowed, None);
3002 }
3003
3004 #[test]
3005 fn ime_bridge_disables_and_clears_snapshot_when_request_is_removed() {
3006 let _runtime_session = crate::RuntimeSession::new();
3007 let _runtime_scope = _runtime_session.enter();
3008 let request = positioned_request(
3009 PxPosition::new(Px(12), Px(18)),
3010 PxSize::new(Px(7), Px(13)),
3011 2..6,
3012 Some(3..5),
3013 );
3014 let mut bridge = RendererImeBridgeState::default();
3015 bridge.update_request(Some(request));
3016
3017 let update = bridge.update_request(None);
3018
3019 assert_eq!(
3020 update,
3021 RendererImeBridgeUpdate {
3022 allowed: Some(false),
3023 cursor_area: None,
3024 text_state: None,
3025 options: None,
3026 ..Default::default()
3027 }
3028 );
3029 assert_eq!(bridge.request(), None);
3030 }
3031}