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