1#![deny(missing_docs)]
2#![doc = include_str!("../README.md")]
3
4use std::sync::PoisonError;
5mod focus_reveal;
6mod fps_monitor;
7mod frame_rate_boost;
8mod hit_path_tracker;
9pub mod inspector;
10mod modal_focus;
11#[cfg(debug_assertions)]
12mod semantics_check;
13mod shell_debug;
14mod shell_frame;
15mod shell_input;
16mod surface;
17mod wheel;
18use std::{
19 cell::RefCell,
20 fmt::{Debug, Write},
21 rc::Rc,
22 sync::{
23 Mutex, MutexGuard,
24 atomic::{AtomicBool, Ordering},
25 },
26};
27
28use cranpose_core::{
29 Applier, Composition, Key, MemoryApplier, NodeError, NodeId, SceneNodeAttachmentScratch,
30 collections::map::HashSet, enter_event_handler_scope, location_key, run_in_mutable_snapshot,
31};
32pub use cranpose_foundation::{
33 DEFAULT_ROTARY_SCROLL_FACTOR_DP, Modifiers, PointerSource, RotaryScrollEvent,
34 rotary_scroll_pixels_from_detents,
35};
36use cranpose_foundation::{PointerButton, PointerButtons, PointerEvent, PointerEventKind};
37use cranpose_render_common::{HitTestTarget, RenderScene, Renderer, SceneUpdates};
38use cranpose_runtime_std::StdRuntime;
39use cranpose_ui::{
40 HeadlessRenderer, LayoutBox, LayoutNode, LayoutTree, MeasureLayoutOptions, SemanticsTree,
41 SubcomposeLayoutNode, WindowRootEntry, clear_transient_scroll_motion_contexts,
42 format_layout_tree, format_render_scene, format_screen_summary,
43 has_pending_focus_invalidations, has_pending_pointer_repasses,
44 has_pending_semantics_invalidations, process_focus_invalidations, process_pointer_repasses,
45 process_semantics_invalidations, request_render_invalidation, take_focus_invalidation,
46 take_layout_invalidation, take_pointer_invalidation, take_render_invalidation,
47};
48pub use cranpose_ui::{KeyCode, KeyEvent, KeyEventType};
49use cranpose_ui_graphics::{Point, PointerIcon, Rect, Size};
50pub use fps_monitor::FpsStats;
51pub use frame_rate_boost::FrameRateBoost;
52use hit_path_tracker::PointerId;
53#[cfg(test)]
54use shell_frame::build_draw_refresh_scope;
55pub use surface::{RootId, RootSurface, SurfaceMut};
56use surface::{SurfaceDirtyLane, TextInputRouter, TextInputRoutes, route_nodes_by_surface};
57use web_time::Instant;
58pub use wheel::WheelScroll;
59
60#[cfg(all(
61 feature = "clipboard-native",
62 not(target_arch = "wasm32"),
63 not(target_os = "android"),
64 not(target_os = "ios")
65))]
66#[derive(Default)]
67struct ShellClipboard {
68 inner: std::cell::OnceCell<std::cell::RefCell<Option<arboard::Clipboard>>>,
72}
73
74#[cfg(all(
75 feature = "clipboard-native",
76 not(target_arch = "wasm32"),
77 not(target_os = "android"),
78 not(target_os = "ios")
79))]
80impl ShellClipboard {
81 fn with<T>(
84 &self,
85 use_clipboard: impl FnOnce(&mut arboard::Clipboard) -> Option<T>,
86 ) -> Option<T> {
87 self.inner
88 .get_or_init(|| std::cell::RefCell::new(arboard::Clipboard::new().ok()))
89 .borrow_mut()
90 .as_mut()
91 .and_then(use_clipboard)
92 }
93}
94
95#[cfg(all(
96 feature = "clipboard-native",
97 not(target_arch = "wasm32"),
98 not(target_os = "android"),
99 not(target_os = "ios")
100))]
101impl cranpose_ui::clipboard_session::PlatformClipboard for ShellClipboard {
102 fn write_text(&self, text: &str) {
103 self.with(|clipboard| clipboard.set_text(text).ok());
104 }
105
106 fn read_text(&self) -> Option<String> {
107 self.with(|clipboard| clipboard.get_text().ok())
108 }
109}
110#[cfg(any(test, feature = "test-support"))]
111use cranpose_core::{
112 CompositionPassDebugStats, SlotId,
113 runtime::{RuntimeDebugStats, StateArenaDebugStats},
114 snapshot_pinning::{SnapshotPinningDebugStats, debug_snapshot_pinning_stats},
115 snapshot_v2::{SnapshotV2DebugStats, debug_snapshot_v2_stats},
116};
117#[cfg(any(test, feature = "test-support"))]
118use cranpose_core::{
119 MemoryApplierDebugStats, RecomposeScopeRegistryDebugStats, SlotTableDebugStats,
120 debug_recompose_scope_registry_stats,
121};
122pub use cranpose_ui::{ImeEditorState, PlatformTextInputHandler};
123#[cfg(any(test, feature = "test-support"))]
124pub mod accessibility_audit;
125#[cfg(any(test, feature = "test-support"))]
126pub mod placed_semantics;
127
128#[derive(Debug, Clone, Copy, PartialEq, Default)]
138pub enum FrameRatePreference {
139 #[default]
144 Auto,
145 NoPreference,
147 Exact(f32),
150}
151
152impl FrameRatePreference {
153 pub const AUTO_QUIET_RATE_HZ: f32 = 60.0;
160
161 pub fn desired_rate_hz(
175 self,
176 producing_frames: bool,
177 boosted: bool,
178 panel_max_hz: Option<f32>,
179 ) -> f32 {
180 match self {
181 FrameRatePreference::Auto => {
182 if boosted {
183 panel_max_hz
184 .filter(|rate| *rate > 0.0)
185 .unwrap_or(Self::AUTO_QUIET_RATE_HZ)
186 } else if producing_frames {
187 Self::AUTO_QUIET_RATE_HZ
188 } else {
189 0.0
190 }
191 }
192 FrameRatePreference::NoPreference => 0.0,
193 FrameRatePreference::Exact(rate) if rate > 0.0 => rate,
194 FrameRatePreference::Exact(_) => 0.0,
195 }
196 }
197}
198
199pub(crate) struct ShellApp {
200 pub(crate) app_context: Rc<cranpose_ui::AppContext>,
201 pub(crate) runtime: StdRuntime,
202 pub(crate) composition: Composition<MemoryApplier>,
203 pub(crate) content: Box<dyn FnMut()>,
204 pub(crate) start_time: Instant,
205 pub(crate) last_frame_time_nanos: u64,
206 pub(crate) semantics_enabled: bool,
207 pub(crate) semantics_snapshot_revision: u64,
208 pub(crate) revealed_focus: Option<NodeId>,
209 pub(crate) layout_requested: bool,
210 pub(crate) content_moved: bool,
211 pub(crate) force_layout_pass: bool,
212 pub(crate) modifiers: Option<Modifiers>,
213 pub(crate) rotary_scroll_factor: f32,
214 #[cfg(all(feature = "clipboard-native", target_os = "linux"))]
215 pub(crate) clipboard: Option<arboard::Clipboard>,
216 pub(crate) dev_options: DevOptions,
217 pub(crate) inspector_projector: Option<inspector::InspectorProjector>,
218 pub(crate) fps_monitor: fps_monitor::FpsMonitor,
219 pub(crate) text_input_routes: Rc<RefCell<TextInputRoutes>>,
220 pub(crate) text_input_router_installed: bool,
221 pub(crate) window_roots_seen: Option<u64>,
222 pub(crate) rebuilt_frames_since_observation_prune: u32,
224 #[cfg(feature = "installed-fonts")]
226 pub(crate) installed_fonts: usize,
227}
228
229pub struct AppShell<R>
237where
238 R: Renderer,
239{
240 pub(crate) app: ShellApp,
241 pub(crate) surfaces: Vec<RootSurface<R>>,
242 pub(crate) routing_scratch: SurfaceRoutingScratch,
243 pub(crate) pending_dirty_nodes: Vec<NodeId>,
244 pub(crate) pending_layer_property_nodes: Vec<NodeId>,
245 pub(crate) geometry_scene_nodes: cranpose_ui::GeometrySceneNodes,
246}
247
248#[derive(Default)]
249pub(crate) struct SurfaceRoutingScratch {
250 pub(crate) attached: Vec<Option<NodeId>>,
251 pub(crate) owners: Vec<Option<NodeId>>,
252 pub(crate) seen: HashSet<(usize, NodeId)>,
253 pub(crate) attachment: SceneNodeAttachmentScratch,
254 pub(crate) window_roots: cranpose_ui::WindowRootRoutingScratch,
255}
256
257#[derive(Clone, Copy, Debug, PartialEq, Eq)]
258pub struct PointerEventTime {
260 pub platform_time_ms: Option<i64>,
262 pub animation_time_nanos: u64,
264}
265
266fn update_stage_telemetry_threshold_ms() -> Option<f64> {
267 static THRESHOLD_MS: std::sync::OnceLock<Option<f64>> = std::sync::OnceLock::new();
268 *THRESHOLD_MS.get_or_init(|| {
269 std::env::var("CRANPOSE_UPDATE_STAGE_TELEMETRY_MS")
270 .ok()
271 .and_then(|value| value.parse::<f64>().ok())
272 .filter(|value| value.is_finite() && *value >= 0.0)
273 })
274}
275
276#[derive(Clone, Copy, Debug)]
277struct UpdateStageTelemetry {
278 started_at: Instant,
279 after_frame_callbacks: Instant,
280 after_ui_drain: Instant,
281 after_reconcile: Instant,
282 after_process_frame: Instant,
283 should_render: bool,
284 reconcile_attempted: bool,
285 reconcile_changed: bool,
286}
287
288fn log_update_stage_telemetry(telemetry: UpdateStageTelemetry) {
289 let Some(threshold_ms) = update_stage_telemetry_threshold_ms() else {
290 return;
291 };
292 let total_ms = telemetry
293 .after_process_frame
294 .duration_since(telemetry.started_at)
295 .as_secs_f64()
296 * 1000.0;
297 if total_ms < threshold_ms {
298 return;
299 }
300
301 let frame_callbacks_ms = telemetry
302 .after_frame_callbacks
303 .duration_since(telemetry.started_at)
304 .as_secs_f64()
305 * 1000.0;
306 let ui_drain_ms = telemetry
307 .after_ui_drain
308 .duration_since(telemetry.after_frame_callbacks)
309 .as_secs_f64()
310 * 1000.0;
311 let reconcile_ms = telemetry
312 .after_reconcile
313 .duration_since(telemetry.after_ui_drain)
314 .as_secs_f64()
315 * 1000.0;
316 let process_frame_ms = telemetry
317 .after_process_frame
318 .duration_since(telemetry.after_reconcile)
319 .as_secs_f64()
320 * 1000.0;
321 eprintln!(
322 "[update-stage-telemetry] total_ms={total_ms:.2} frame_callbacks_ms={frame_callbacks_ms:.2} ui_drain_ms={ui_drain_ms:.2} reconcile_ms={reconcile_ms:.2} process_frame_ms={process_frame_ms:.2} should_render={} reconcile_attempted={} reconcile_changed={}",
323 telemetry.should_render, telemetry.reconcile_attempted, telemetry.reconcile_changed
324 );
325}
326
327#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
329pub enum FramePacingMode {
330 #[default]
333 Vsync,
334 Hard60,
336 Hard120,
338 NoVsync,
341}
342
343impl FramePacingMode {
344 pub const ALL: [Self; 4] = [Self::Vsync, Self::Hard60, Self::Hard120, Self::NoVsync];
346
347 pub fn label(self) -> &'static str {
349 match self {
350 Self::Vsync => "VSync",
351 Self::Hard60 => "60fps",
352 Self::Hard120 => "120fps",
353 Self::NoVsync => "NoVSync",
354 }
355 }
356
357 pub fn target_fps(self) -> Option<u32> {
360 match self {
361 Self::Hard60 => Some(60),
362 Self::Hard120 => Some(120),
363 Self::Vsync | Self::NoVsync => None,
364 }
365 }
366}
367
368#[derive(Clone, Copy, Debug, PartialEq)]
371pub struct FrameSchedule {
372 pub needs_update: bool,
375 pub needs_frame: bool,
377 pub next_deadline: Option<web_time::Instant>,
380}
381
382#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
384pub struct FrameUpdateResult {
385 pub visual_changed: bool,
387 pub structure_changed: bool,
390 pub content_moved: bool,
395 pub content_redrawn: bool,
398}
399
400pub trait PlatformFrameDriver {
403 fn request_frame(&self);
405 fn request_wake_at(&self, deadline: web_time::Instant);
408 fn clear_wake(&self);
410}
411
412#[derive(Debug)]
415pub struct FrameScheduler {
416 update_pending: AtomicBool,
417 frame_pending: AtomicBool,
418 next_deadline: Mutex<Option<web_time::Instant>>,
419}
420
421impl Default for FrameScheduler {
422 fn default() -> Self {
423 Self {
424 update_pending: AtomicBool::new(false),
425 frame_pending: AtomicBool::new(false),
426 next_deadline: Mutex::new(None),
427 }
428 }
429}
430
431impl FrameScheduler {
432 fn lock_deadline(&self) -> MutexGuard<'_, Option<web_time::Instant>> {
433 self.next_deadline
434 .lock()
435 .unwrap_or_else(PoisonError::into_inner)
436 }
437
438 pub fn record(&self, schedule: FrameSchedule) {
440 self.update_pending
441 .store(schedule.needs_update, Ordering::SeqCst);
442 self.frame_pending
443 .store(schedule.needs_frame, Ordering::SeqCst);
444 let mut next_deadline = self.lock_deadline();
445 *next_deadline = if schedule.needs_update {
446 None
447 } else {
448 schedule.next_deadline
449 };
450 }
451
452 pub fn schedule<D>(&self, schedule: FrameSchedule, driver: &D)
454 where
455 D: PlatformFrameDriver + ?Sized,
456 {
457 self.record(schedule);
458 schedule.apply_to(driver);
459 }
460
461 pub fn snapshot(&self) -> FrameSchedule {
463 FrameSchedule {
464 needs_update: self.update_pending.load(Ordering::SeqCst),
465 needs_frame: self.frame_pending.load(Ordering::SeqCst),
466 next_deadline: *self.lock_deadline(),
467 }
468 }
469}
470
471impl FrameSchedule {
472 pub fn apply_to<D>(self, driver: &D)
475 where
476 D: PlatformFrameDriver + ?Sized,
477 {
478 if self.needs_frame {
479 driver.clear_wake();
480 driver.request_frame();
481 } else if self.needs_update {
482 driver.request_wake_at(web_time::Instant::now());
483 } else if let Some(deadline) = self.next_deadline {
484 driver.request_wake_at(deadline);
485 } else {
486 driver.clear_wake();
487 }
488 }
489}
490
491#[derive(Clone, Copy, Debug)]
492pub(crate) struct DevOverlayControl {
493 bounds: Rect,
494 mode: FramePacingMode,
495}
496
497#[derive(Clone, Debug, Default)]
502pub struct DevOptions {
503 pub fps_counter: bool,
505 pub recomposition_counter: bool,
507 pub layout_timing: bool,
509 pub frame_pacing_controls: bool,
511 pub frame_pacing_mode: FramePacingMode,
513}
514
515#[cfg(any(test, feature = "test-support"))]
516#[doc(hidden)]
517#[derive(Clone, Copy, Debug)]
518pub struct RuntimeLeakDebugStats {
519 pub applier_stats: MemoryApplierDebugStats,
520 pub live_node_heap_bytes: usize,
521 pub recycled_node_heap_bytes: usize,
522 pub slot_table_heap_bytes: usize,
523 pub pass_stats: CompositionPassDebugStats,
524 pub slot_stats: SlotTableDebugStats,
525 pub runtime_stats: RuntimeDebugStats,
526 pub state_arena_stats: StateArenaDebugStats,
527 pub recompose_scope_stats: RecomposeScopeRegistryDebugStats,
528 pub snapshot_v2_stats: SnapshotV2DebugStats,
529 pub snapshot_pinning_stats: SnapshotPinningDebugStats,
530}
531
532impl ShellApp {
533 pub(crate) fn flush_semantics_invalidations(&mut self) {
534 if has_pending_semantics_invalidations() {
535 let mut applier = self.composition.applier_mut();
536 process_semantics_invalidations(|node_id| {
537 cranpose_core::bubble_semantics_dirty(&mut *applier, node_id);
538 });
539 }
540 }
541
542 pub(crate) fn request_layout_pass(&mut self) {
543 self.layout_requested = true;
544 }
545
546 pub(crate) fn request_forced_layout_pass(&mut self) {
547 self.layout_requested = true;
548 self.force_layout_pass = true;
549 }
550
551 pub(crate) fn composition_tree_needs_layout(&mut self) -> bool {
552 let Some(root) = self.composition.root() else {
553 return true;
554 };
555 let mut applier = self.composition.applier_mut();
556 cranpose_ui::tree_needs_layout(&mut *applier, root).unwrap_or_else(|err| {
557 log::warn!("Cannot check layout dirty status for root #{root}: {err}");
558 true
559 })
560 }
561
562 pub(crate) fn has_stale_work_in_context(&self) -> bool {
563 self.layout_requested || self.app_context.has_stale_work()
564 }
565
566 pub(crate) fn wants_frame_in_context(&self) -> bool {
567 self.layout_requested
568 || self.app_context.has_pending_frame_work()
569 || self.composition.should_render()
570 }
571
572 pub(crate) fn needs_ui_update_in_context(&self, surfaces_dirty: bool) -> bool {
573 surfaces_dirty
574 || self.has_stale_work_in_context()
575 || self.composition.runtime_handle().has_pending_ui()
576 || has_pending_semantics_invalidations()
577 || self.composition.should_render()
578 }
579
580 pub(crate) fn next_event_time(&self) -> Option<web_time::Instant> {
581 let app_context = Rc::clone(&self.app_context);
582 app_context.enter(cranpose_ui::next_cursor_blink_time)
583 }
584
585 pub(crate) fn frame_time_nanos_at(&self, now: Instant) -> u64 {
586 now.checked_duration_since(self.start_time)
587 .unwrap_or_default()
588 .as_nanos()
589 .min(u128::from(u64::MAX)) as u64
590 }
591
592 pub(crate) fn realtime_pointer_event_time(
593 &self,
594 platform_time_ms: Option<i64>,
595 ) -> PointerEventTime {
596 PointerEventTime {
597 platform_time_ms,
598 animation_time_nanos: self
599 .frame_time_nanos_at(Instant::now())
600 .max(self.last_frame_time_nanos),
601 }
602 }
603
604 pub(crate) fn install_text_input_router(&mut self) {
605 if self.text_input_router_installed {
606 return;
607 }
608 let router = Rc::new(TextInputRouter {
609 routes: Rc::clone(&self.text_input_routes),
610 });
611 let app_context = Rc::clone(&self.app_context);
612 app_context
613 .enter(|| cranpose_ui::text_input_session::set_platform_text_input_handler(router));
614 self.text_input_router_installed = true;
615 }
616}
617
618impl<R> AppShell<R>
619where
620 R: Renderer,
621 R::Error: Debug,
622{
623 pub fn new(renderer: R, root_key: Key, content: impl FnMut() + 'static) -> Self {
626 Self::new_with_size(renderer, root_key, content, (800, 600), (800.0, 600.0))
627 }
628
629 pub fn new_with_size(
632 renderer: R,
633 root_key: Key,
634 content: impl FnMut() + 'static,
635 buffer_size: (u32, u32),
636 viewport: (f32, f32),
637 ) -> Self {
638 Self::new_with_size_and_density(renderer, root_key, content, buffer_size, viewport, 1.0)
639 }
640
641 pub fn new_with_size_and_density(
644 mut renderer: R,
645 root_key: Key,
646 content: impl FnMut() + 'static,
647 buffer_size: (u32, u32),
648 viewport: (f32, f32),
649 density: f32,
650 ) -> Self {
651 renderer.set_root_scale(density);
652 let app_context = cranpose_ui::AppContext::new_with_density(density);
653 let runtime = StdRuntime::new();
654 let mut composition = Composition::with_runtime(MemoryApplier::new(), runtime.runtime());
655 let app_content = Rc::new(std::cell::RefCell::new(content));
656 let mut build: Box<dyn FnMut()> = Box::new(move || {
657 let app_content = Rc::clone(&app_content);
658 cranpose_ui::density::ProvideDensity(cranpose_ui::Density::from_host(), move || {
659 cranpose_ui::widgets::PopupHost(move || {
660 (app_content.borrow_mut())();
661 });
662 });
663 });
664 renderer.attach_app_context_services(&app_context);
665 app_context.enter(|| {
666 #[cfg(all(
667 feature = "clipboard-native",
668 not(target_arch = "wasm32"),
669 not(target_os = "android"),
670 not(target_os = "ios")
671 ))]
672 {
673 cranpose_ui::clipboard_session::set_platform_clipboard(std::rc::Rc::new(
674 ShellClipboard::default(),
675 ));
676 }
677 if let Err(err) = composition.render_stable(root_key, &mut *build) {
678 log::error!("initial render failed: {err}");
679 }
680 });
681 renderer.scene_mut().clear();
682 let app = ShellApp {
683 app_context,
684 runtime,
685 composition,
686 content: build,
687 start_time: Instant::now(),
688 last_frame_time_nanos: 0,
689 semantics_enabled: false,
690 semantics_snapshot_revision: 0,
691 revealed_focus: None,
692 layout_requested: true,
693 content_moved: false,
694 force_layout_pass: true,
695 modifiers: None,
696 rotary_scroll_factor: DEFAULT_ROTARY_SCROLL_FACTOR_DP,
697 #[cfg(all(feature = "clipboard-native", target_os = "linux"))]
698 clipboard: arboard::Clipboard::new().ok(),
699 dev_options: DevOptions::default(),
700 inspector_projector: None,
701 fps_monitor: fps_monitor::FpsMonitor::new(),
702 text_input_routes: Rc::new(RefCell::new(TextInputRoutes::default())),
703 text_input_router_installed: false,
704 window_roots_seen: None,
705 rebuilt_frames_since_observation_prune: 0,
706 #[cfg(feature = "installed-fonts")]
707 installed_fonts: 0,
708 };
709 let mut shell = Self {
710 app,
711 surfaces: vec![RootSurface::new(
712 RootId::Primary,
713 renderer,
714 buffer_size,
715 viewport,
716 )],
717 routing_scratch: SurfaceRoutingScratch::default(),
718 pending_dirty_nodes: Vec::new(),
719 pending_layer_property_nodes: Vec::new(),
720 geometry_scene_nodes: cranpose_ui::GeometrySceneNodes::default(),
721 };
722 shell.process_frame();
723 shell
724 }
725
726 pub fn app_context(&self) -> &Rc<cranpose_ui::AppContext> {
731 &self.app.app_context
732 }
733
734 pub fn surface(&mut self, root: RootId) -> Option<SurfaceMut<'_, R>> {
737 let index = self.surface_index(root)?;
738 Some(SurfaceMut::new(self, index))
739 }
740
741 pub fn primary(&mut self) -> SurfaceMut<'_, R> {
743 SurfaceMut::new(self, 0)
744 }
745
746 pub fn root_holding_the_press(&mut self) -> Option<RootId> {
755 let pressed = self
756 .surfaces
757 .iter()
758 .find_map(|surface| surface.hit_path_tracker.dispatch_order(PointerId::PRIMARY))?;
759 let primary_root = self.app.composition.root()?;
760 let mut applier = self.app.composition.applier_mut();
761 let holder = pressed.into_iter().find_map(|node| {
762 let node = applier.scene_node_attached_to(node, primary_root)?;
763 Some(cranpose_ui::nearest_window_root(&mut applier, node))
764 })?;
765 drop(applier);
766 self.surfaces
767 .iter()
768 .find(|surface| surface.owns_nodes_under(holder))
769 .map(|surface| surface.id)
770 }
771
772 fn surface_index(&self, root: RootId) -> Option<usize> {
773 self.surfaces.iter().position(|surface| surface.id == root)
774 }
775
776 pub fn add_window_surface(
786 &mut self,
787 window: u64,
788 renderer: R,
789 buffer_size: (u32, u32),
790 viewport: (f32, f32),
791 ) -> Option<R> {
792 let previous = self.remove_window_surface(window);
793 self.surfaces.push(RootSurface::new(
794 RootId::Window(window),
795 renderer,
796 buffer_size,
797 viewport,
798 ));
799 self.app.window_roots_seen = None;
800 self.sync_window_roots();
801 previous
802 }
803
804 pub fn remove_window_surface(&mut self, window: u64) -> Option<R> {
806 let root = RootId::Window(window);
807 let index = self.surface_index(root)?;
808 self.app.text_input_routes.borrow_mut().remove(root);
809 Some(self.surfaces.remove(index).renderer)
810 }
811
812 pub fn surface_ids(&self) -> Vec<RootId> {
814 self.surfaces.iter().map(|surface| surface.id).collect()
815 }
816
817 pub fn window_roots(&self) -> Vec<WindowRootEntry> {
821 let app_context = Rc::clone(&self.app.app_context);
822 app_context.enter(cranpose_ui::window_roots)
823 }
824
825 pub fn window_roots_revision(&self) -> u64 {
829 let app_context = Rc::clone(&self.app.app_context);
830 app_context.enter(cranpose_ui::window_roots_revision)
831 }
832
833 pub fn primary_has_content(&mut self) -> bool {
838 self.read_primary_boxes(cranpose_ui::has_placed_content)
839 .unwrap_or(false)
840 }
841
842 pub fn primary_content_size(&mut self) -> Option<Size> {
848 self.read_primary_boxes(cranpose_ui::placed_content_extent)
849 .flatten()
850 }
851
852 fn read_primary_boxes<T>(
856 &mut self,
857 read: impl FnOnce(&mut MemoryApplier, NodeId) -> Result<T, NodeError>,
858 ) -> Option<T> {
859 let app_context = Rc::clone(&self.app.app_context);
860 app_context.enter(|| {
861 let root = self.surfaces[0].root_node(&self.app)?;
862 let mut applier = self.app.composition.applier_mut();
863 read(&mut applier, root)
864 .inspect_err(|err| log::debug!("failed to read the primary root's boxes: {err}"))
865 .ok()
866 })
867 }
868
869 pub(crate) fn drag_and_drop_target_at(
875 &mut self,
876 point: cranpose_ui::DragAndDropPoint,
877 source: usize,
878 ) -> Option<(NodeId, cranpose_ui::Point)> {
879 let app_context = Rc::clone(&self.app.app_context);
880 let candidates: Vec<(usize, cranpose_ui::Point)> = match point.screen {
881 Some(screen) => (0..self.surfaces.len())
882 .rev()
883 .filter_map(|index| {
884 let local = self.surfaces[index].screen_point_inside(screen)?;
885 Some((index, local))
886 })
887 .collect(),
888 None => vec![(source, point.local)],
889 };
890 candidates.into_iter().find_map(|(index, local)| {
891 self.surfaces[index]
892 .renderer
893 .scene()
894 .hit_test_nodes(local.x, local.y)
895 .into_iter()
896 .find(|node| app_context.drag_and_drop().is_target(*node))
897 .map(|node| (node, local))
898 })
899 }
900
901 pub fn set_active_root(&mut self, root: RootId) {
905 self.app.text_input_routes.borrow_mut().set_active(root);
906 }
907
908 pub fn active_root(&self) -> RootId {
910 self.app.text_input_routes.borrow().active()
911 }
912
913 fn sync_window_roots(&mut self) {
914 let app_context = Rc::clone(&self.app.app_context);
915 app_context.enter(|| self.sync_window_roots_in_context());
916 }
917
918 pub(crate) fn sync_window_roots_in_context(&mut self) {
919 let revision = cranpose_ui::window_roots_revision();
920 if self.app.window_roots_seen == Some(revision) {
921 return;
922 }
923 self.app.window_roots_seen = Some(revision);
924 let entries = cranpose_ui::window_roots();
925 for surface in &mut self.surfaces {
926 let RootId::Window(id) = surface.id else {
927 continue;
928 };
929 let root = entries
930 .iter()
931 .find(|entry| entry.node as u64 == id)
932 .map(|entry| entry.node);
933 surface.set_root(root);
934 }
935 }
936
937 pub(crate) fn any_surface_dirty(&self) -> bool {
938 self.surfaces
939 .iter()
940 .any(|surface| surface.is_dirty || surface.scene_dirty)
941 }
942
943 pub(crate) fn mark_all_dirty(&mut self) {
944 for surface in &mut self.surfaces {
945 surface.is_dirty = true;
946 }
947 }
948
949 fn clear_surface_dirt(&mut self) {
950 for surface in &mut self.surfaces {
951 surface.is_dirty = false;
952 }
953 }
954
955 fn invalidate_dev_overlay_text(&mut self) {
956 for surface in &mut self.surfaces {
957 surface.invalidate_dev_overlay_text();
958 }
959 }
960
961 pub fn set_dev_options(&mut self, options: DevOptions) {
966 self.app.dev_options = options;
967 self.invalidate_dev_overlay_text();
968 let app_context = Rc::clone(&self.app.app_context);
969 app_context.enter(request_render_invalidation);
970 self.mark_all_dirty();
971 }
972
973 pub fn dev_options(&self) -> &DevOptions {
975 &self.app.dev_options
976 }
977
978 pub fn frame_pacing_mode(&self) -> FramePacingMode {
980 self.app.dev_options.frame_pacing_mode
981 }
982
983 pub fn current_fps(&self) -> f32 {
985 self.app.fps_monitor.current_fps()
986 }
987
988 pub fn fps_stats(&self) -> FpsStats {
990 self.app.fps_monitor.stats()
991 }
992
993 pub fn reset_fps_stats(&mut self) {
995 self.app.fps_monitor.reset_stats();
996 self.invalidate_dev_overlay_text();
997 }
998
999 pub fn record_presented_frame(
1002 &mut self,
1003 frame_started_at: Instant,
1004 frame_finished_at: Instant,
1005 ) {
1006 self.app
1007 .fps_monitor
1008 .record_frame_work(frame_started_at, frame_finished_at);
1009 }
1010
1011 #[cfg(any(test, feature = "test-support"))]
1012 #[doc(hidden)]
1013 pub fn record_presented_frame_for_test(
1014 &mut self,
1015 frame_started_nanos: u64,
1016 frame_finished_nanos: u64,
1017 ) {
1018 let started = self.app.start_time + std::time::Duration::from_nanos(frame_started_nanos);
1019 let finished = self.app.start_time + std::time::Duration::from_nanos(frame_finished_nanos);
1020 self.record_presented_frame(started, finished);
1021 }
1022
1023 pub fn set_frame_pacing_mode(&mut self, mode: FramePacingMode) {
1026 if self.app.dev_options.frame_pacing_mode == mode {
1027 return;
1028 }
1029 self.app.dev_options.frame_pacing_mode = mode;
1030 self.invalidate_dev_overlay_text();
1031 let app_context = Rc::clone(&self.app.app_context);
1032 app_context.enter(request_render_invalidation);
1033 self.mark_all_dirty();
1034 }
1035
1036 pub fn dev_overlay_control_center(&self, mode: FramePacingMode) -> Option<(f32, f32)> {
1043 self.surfaces[0].dev_overlay_control_center(mode)
1044 }
1045
1046 pub fn set_viewport(&mut self, width: f32, height: f32) {
1049 if self.surfaces[0].viewport == (width, height) {
1050 return;
1051 }
1052 self.primary().set_viewport(width, height);
1053 self.process_frame();
1054 }
1055
1056 pub fn viewport_size(&self) -> (f32, f32) {
1058 self.surfaces[0].viewport
1059 }
1060
1061 pub fn set_screen_origin(&mut self, origin: Option<cranpose_ui_graphics::Point>) {
1064 self.surfaces[0].screen_origin = origin;
1065 }
1066
1067 pub fn set_buffer_size(&mut self, width: u32, height: u32) {
1069 self.surfaces[0].buffer_size = (width, height);
1070 }
1071
1072 pub fn buffer_size(&self) -> (u32, u32) {
1074 self.surfaces[0].buffer_size
1075 }
1076
1077 pub fn scene(&self) -> &R::Scene {
1079 self.surfaces[0].renderer.scene()
1080 }
1081
1082 pub fn renderer(&mut self) -> &mut R {
1084 &mut self.surfaces[0].renderer
1085 }
1086
1087 #[cfg(not(target_arch = "wasm32"))]
1090 pub fn set_frame_waker(&mut self, waker: impl Fn() + Send + Sync + 'static) {
1091 self.app.runtime.set_frame_waker(waker);
1092 }
1093
1094 #[cfg(target_arch = "wasm32")]
1097 pub fn set_frame_waker(&mut self, waker: impl Fn() + 'static) {
1098 self.app.runtime.set_frame_waker(waker);
1099 }
1100
1101 pub fn clear_frame_waker(&mut self) {
1103 self.app.runtime.clear_frame_waker();
1104 }
1105
1106 pub fn should_render(&self) -> bool {
1109 let app_context = Rc::clone(&self.app.app_context);
1110 app_context.enter(|| {
1111 self.app.wants_frame_in_context()
1112 || self.surfaces.iter().any(|surface| surface.scene_dirty)
1113 })
1114 }
1115
1116 pub fn needs_update(&self) -> bool {
1119 let app_context = Rc::clone(&self.app.app_context);
1120 app_context.enter(|| {
1121 self.app
1122 .needs_ui_update_in_context(self.any_surface_dirty())
1123 })
1124 }
1125
1126 pub fn needs_update_without_frame(&self) -> bool {
1129 let app_context = Rc::clone(&self.app.app_context);
1130 app_context.enter(|| {
1131 self.any_surface_dirty()
1132 || self.app.has_stale_work_in_context()
1133 || self.app.composition.should_recompose()
1134 || self.app.composition.runtime_handle().has_pending_ui()
1135 || has_pending_semantics_invalidations()
1136 || cranpose_ui::next_cursor_blink_time().is_some_and(|at| at <= Instant::now())
1137 || self
1138 .surfaces
1139 .iter()
1140 .any(|surface| surface.renderer_warmup_due(&self.app))
1141 })
1142 }
1143
1144 pub fn has_pending_ui(&self) -> bool {
1152 let app_context = Rc::clone(&self.app.app_context);
1153 app_context.enter(|| self.app.composition.runtime_handle().has_pending_ui())
1154 }
1155
1156 pub fn run_pending_tasks(&mut self) {
1163 let app_context = Rc::clone(&self.app.app_context);
1164 app_context.enter(|| {
1165 let runtime_handle = self.app.composition.runtime_handle();
1166 if runtime_handle.has_pending_ui() {
1167 runtime_handle.with_deferred_state_releases(|| runtime_handle.drain_ui());
1168 }
1169 });
1170 }
1171
1172 pub fn needs_redraw(&self) -> bool {
1185 let app_context = Rc::clone(&self.app.app_context);
1186 app_context.enter(|| self.surfaces[0].needs_redraw_in_context(&self.app))
1187 }
1188
1189 pub fn mark_dirty(&mut self) {
1191 self.surfaces[0].is_dirty = true;
1192 }
1193
1194 pub fn request_root_render(&mut self) {
1197 self.app.composition.request_root_render();
1198 self.app.request_forced_layout_pass();
1199 let app_context = Rc::clone(&self.app.app_context);
1200 app_context.enter(request_render_invalidation);
1201 self.mark_all_dirty();
1202 }
1203
1204 pub fn set_density(&mut self, density: f32) {
1207 self.surfaces[0].renderer.set_root_scale(density);
1208 let app_context = Rc::clone(&self.app.app_context);
1209 let changed = app_context.enter(|| {
1210 let previous = cranpose_ui::current_density().to_bits();
1211 cranpose_ui::set_density(density);
1212 previous != cranpose_ui::current_density().to_bits()
1213 });
1214 if changed {
1215 self.request_root_render();
1216 }
1217 }
1218
1219 pub fn set_font_scale(&mut self, font_scale: f32) {
1230 self.set_font_scale_curve(cranpose_ui::FontScaleCurve::linear(font_scale));
1231 }
1232
1233 pub fn set_font_scale_curve(&mut self, curve: cranpose_ui::FontScaleCurve) {
1240 let app_context = Rc::clone(&self.app.app_context);
1241 let changed = app_context.enter(|| {
1242 let previous = cranpose_ui::current_font_scale_curve();
1243 cranpose_ui::set_font_scale_curve(curve);
1244 previous != cranpose_ui::current_font_scale_curve()
1245 });
1246 if changed {
1247 self.request_root_render();
1248 }
1249 }
1250
1251 #[cfg(any(test, feature = "test-support"))]
1252 #[doc(hidden)]
1253 pub fn debug_current_density(&self) -> f32 {
1254 let app_context = Rc::clone(&self.app.app_context);
1255 app_context.enter(cranpose_ui::current_density)
1256 }
1257
1258 #[cfg(any(test, feature = "test-support"))]
1259 #[doc(hidden)]
1260 pub fn debug_current_font_scale(&self) -> f32 {
1261 let app_context = Rc::clone(&self.app.app_context);
1262 app_context.enter(cranpose_ui::current_font_scale)
1263 }
1264
1265 #[cfg(any(test, feature = "test-support"))]
1266 #[doc(hidden)]
1267 pub fn debug_current_font_scale_curve(&self) -> cranpose_ui::FontScaleCurve {
1268 let app_context = Rc::clone(&self.app.app_context);
1269 app_context.enter(cranpose_ui::current_font_scale_curve)
1270 }
1271
1272 #[cfg(any(test, feature = "test-support"))]
1273 #[doc(hidden)]
1274 pub fn debug_enter_app_context<T>(&self, block: impl FnOnce() -> T) -> T {
1275 let app_context = Rc::clone(&self.app.app_context);
1276 app_context.enter(block)
1277 }
1278
1279 pub fn has_active_animations(&self) -> bool {
1281 self.app.composition.should_render()
1282 }
1283
1284 pub fn has_transient_frame_callbacks(&self) -> bool {
1287 self.app
1288 .composition
1289 .runtime_handle()
1290 .has_transient_frame_callbacks()
1291 }
1292
1293 pub fn has_active_pointer_gesture(&self) -> bool {
1295 self.surfaces[0].has_active_pointer_gesture()
1296 }
1297
1298 pub fn frame_owed(&self) -> bool {
1300 self.surfaces[0].frame_owed
1301 }
1302
1303 pub fn take_frame_owed(&mut self) -> bool {
1305 std::mem::take(&mut self.surfaces[0].frame_owed)
1306 }
1307
1308 pub fn next_event_time(&self) -> Option<web_time::Instant> {
1311 self.app.next_event_time()
1312 }
1313
1314 fn compute_frame_schedule(&self) -> FrameSchedule {
1315 self.surfaces[0].compute_frame_schedule(&self.app, self.any_surface_dirty())
1316 }
1317
1318 pub fn frame_schedule(&self) -> FrameSchedule {
1321 let schedule = self.compute_frame_schedule();
1322 self.surfaces[0].frame_scheduler.record(schedule);
1323 schedule
1324 }
1325
1326 pub fn schedule_platform_frame<D>(&self, driver: &D) -> FrameSchedule
1329 where
1330 D: PlatformFrameDriver + ?Sized,
1331 {
1332 let schedule = self.compute_frame_schedule();
1333 self.surfaces[0].frame_scheduler.schedule(schedule, driver);
1334 schedule
1335 }
1336
1337 pub fn frame_scheduler_snapshot(&self) -> FrameSchedule {
1340 self.surfaces[0].frame_scheduler.snapshot()
1341 }
1342
1343 pub fn realtime_pointer_event_time(&self, platform_time_ms: Option<i64>) -> PointerEventTime {
1345 self.app.realtime_pointer_event_time(platform_time_ms)
1346 }
1347
1348 pub fn exact_pointer_event_time(&self, platform_time_ms: Option<i64>) -> PointerEventTime {
1350 PointerEventTime {
1351 platform_time_ms,
1352 animation_time_nanos: self.app.last_frame_time_nanos,
1353 }
1354 }
1355
1356 pub fn update_after_frame_interval(
1359 &mut self,
1360 frame_interval: std::time::Duration,
1361 ) -> FrameUpdateResult {
1362 let wall_frame_time = self.app.frame_time_nanos_at(Instant::now());
1363 let base_frame_time = self.app.last_frame_time_nanos.max(wall_frame_time);
1364 let frame_time = base_frame_time
1365 .saturating_add(frame_interval.as_nanos().min(u128::from(u64::MAX)) as u64);
1366 self.update_at_frame_time_nanos(frame_time)
1367 }
1368
1369 pub fn update_after_exact_interval(
1375 &mut self,
1376 frame_interval: std::time::Duration,
1377 ) -> FrameUpdateResult {
1378 let frame_time = self
1379 .app
1380 .last_frame_time_nanos
1381 .saturating_add(frame_interval.as_nanos().min(u128::from(u64::MAX)) as u64);
1382 self.update_at_frame_time_nanos(frame_time)
1383 }
1384
1385 pub fn dispatch_frame_at(&mut self, frame_time: Instant) {
1391 let frame_time = self.app.frame_time_nanos_at(frame_time);
1392 let app_context = Rc::clone(&self.app.app_context);
1393 app_context.enter(|| {
1394 let runtime_handle = self.app.runtime.runtime_handle();
1395 runtime_handle.with_deferred_state_releases(|| {
1396 self.app.last_frame_time_nanos = frame_time.max(self.app.last_frame_time_nanos);
1397 self.app
1398 .runtime
1399 .drain_frame_callbacks(self.app.last_frame_time_nanos);
1400 self.apply_pointer_claim();
1401 runtime_handle.drain_ui();
1402 self.dispatch_requested_mouse_moves(self.app.last_frame_time_nanos);
1403 });
1404 });
1405 }
1406
1407 pub fn update_without_frame(&mut self) -> FrameUpdateResult {
1410 self.update_with_frame_time(None)
1411 }
1412
1413 pub fn update_at_frame_time_nanos(&mut self, frame_time: u64) -> FrameUpdateResult {
1417 self.update_with_frame_time(Some(frame_time))
1418 }
1419
1420 fn update_with_frame_time(&mut self, frame_time: Option<u64>) -> FrameUpdateResult {
1421 let app_context = Rc::clone(&self.app.app_context);
1422 app_context.enter(|| {
1423 let update_started_at = Instant::now();
1424 let runtime_handle = self.app.runtime.runtime_handle();
1425 runtime_handle.with_deferred_state_releases(|| {
1426 if let Some(frame_time) = frame_time {
1427 self.app.last_frame_time_nanos = frame_time.max(self.app.last_frame_time_nanos);
1428 self.app.runtime.drain_frame_callbacks(self.app.last_frame_time_nanos);
1429 self.apply_pointer_claim();
1430 }
1431 let after_frame_callbacks = Instant::now();
1432 runtime_handle.drain_ui();
1433 self.dispatch_requested_mouse_moves(self.app.last_frame_time_nanos);
1434 let after_ui_drain = Instant::now();
1435 let should_render = self.app.composition.should_recompose();
1436 let mut reconcile_attempted = false;
1437 let mut reconcile_changed = false;
1438 if should_render {
1439 log::trace!(
1440 target: "cranpose::input",
1441 "update begin: should_render=true layout_requested={} scene_dirty={} is_dirty={}",
1442 self.app.layout_requested,
1443 self.surfaces[0].scene_dirty,
1444 self.surfaces[0].is_dirty
1445 );
1446 (reconcile_attempted, reconcile_changed) = self.reconcile_in_context();
1447 }
1448 let after_reconcile = Instant::now();
1449 let result = self.process_frame_in_context(reconcile_changed);
1450 let after_process_frame = Instant::now();
1451 log_update_stage_telemetry(UpdateStageTelemetry {
1452 started_at: update_started_at,
1453 after_frame_callbacks,
1454 after_ui_drain,
1455 after_reconcile,
1456 after_process_frame,
1457 should_render,
1458 reconcile_attempted,
1459 reconcile_changed,
1460 });
1461 self.clear_surface_dirt();
1462 result
1463 })
1464 })
1465 }
1466
1467 fn apply_pointer_claim(&mut self) {
1470 let Some(node) = self.app.app_context.take_pointer_claim() else {
1471 return;
1472 };
1473 for index in 0..self.surfaces.len() {
1474 SurfaceMut::new(self, index).give_press_to(node);
1475 }
1476 }
1477
1478 fn dispatch_requested_mouse_moves(&mut self, frame_time: u64) {
1479 use cranpose_ui::mouse_input::MouseInputTarget;
1480 let pending = self.app.app_context.pending_mouse_moves();
1481 for _ in 0..pending {
1482 let Some(request) = self.app.app_context.take_mouse_move() else {
1483 break;
1484 };
1485 let root = match request.target {
1486 MouseInputTarget::Primary => RootId::Primary,
1487 MouseInputTarget::Window(id) => RootId::Window(id),
1488 };
1489 if let Some(mut surface) = self.surface(root) {
1490 surface.set_pointer_source(PointerSource::Mouse);
1491 surface.set_cursor_at_event_time(request.position.x, request.position.y, PointerEventTime {
1492 platform_time_ms: Some((frame_time / 1_000_000).min(i64::MAX as u64) as i64),
1493 animation_time_nanos: frame_time,
1494 });
1495 }
1496 }
1497 }
1498
1499 fn reconcile_in_context(&mut self) -> (bool, bool) {
1500 let Some(root_key) = self.app.composition.root_key() else {
1501 return (false, false);
1502 };
1503 match self
1504 .app
1505 .composition
1506 .reconcile(root_key, &mut *self.app.content)
1507 {
1508 Ok(changed) => {
1509 log::trace!(
1510 target: "cranpose::input",
1511 "reconcile changed={changed}"
1512 );
1513 if changed {
1514 self.app.fps_monitor.record_recomposition();
1515 if self.app.composition_tree_needs_layout() {
1516 self.app.request_layout_pass();
1517 }
1518 request_render_invalidation();
1519 }
1520 (true, changed)
1521 }
1522 Err(NodeError::Missing { id }) => {
1523 log::debug!("Recomposition skipped: node {id} no longer exists");
1524 self.app.request_layout_pass();
1525 request_render_invalidation();
1526 (true, false)
1527 }
1528 Err(err) => {
1529 log::error!("recomposition failed: {err}");
1530 self.app.request_layout_pass();
1531 request_render_invalidation();
1532 (true, false)
1533 }
1534 }
1535 }
1536
1537 pub fn update(&mut self) -> FrameUpdateResult {
1540 let frame_time = self.app.frame_time_nanos_at(Instant::now());
1541 self.update_at_frame_time_nanos(frame_time)
1542 }
1543}
1544
1545impl<R> Drop for AppShell<R>
1546where
1547 R: Renderer,
1548{
1549 fn drop(&mut self) {
1550 self.app.runtime.clear_frame_waker();
1551 }
1552}
1553
1554pub fn default_root_key() -> Key {
1556 location_key(file!(), line!(), column!())
1557}
1558
1559#[cfg(test)]
1560#[path = "tests/app_shell_frame_pacing_tests.rs"]
1561mod frame_pacing_tests;
1562
1563#[cfg(test)]
1564#[path = "tests/app_shell_tests.rs"]
1565mod tests;
1566
1567#[cfg(test)]
1568#[path = "tests/surface_tests.rs"]
1569mod surface_tests;