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