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))]
67struct ShellClipboard {
68 inner: std::rc::Rc<std::cell::RefCell<Option<arboard::Clipboard>>>,
69}
70
71#[cfg(all(
72 feature = "clipboard-native",
73 not(target_arch = "wasm32"),
74 not(target_os = "android"),
75 not(target_os = "ios")
76))]
77impl cranpose_ui::clipboard_session::PlatformClipboard for ShellClipboard {
78 fn write_text(&self, text: &str) {
79 if let Some(clipboard) = self.inner.borrow_mut().as_mut() {
80 let _ = clipboard.set_text(text);
81 }
82 }
83
84 fn read_text(&self) -> Option<String> {
85 self.inner
86 .borrow_mut()
87 .as_mut()
88 .and_then(|clipboard| clipboard.get_text().ok())
89 }
90}
91#[cfg(any(test, feature = "test-support"))]
92use cranpose_core::{
93 CompositionPassDebugStats, SlotId,
94 runtime::{RuntimeDebugStats, StateArenaDebugStats},
95 snapshot_pinning::{SnapshotPinningDebugStats, debug_snapshot_pinning_stats},
96 snapshot_v2::{SnapshotV2DebugStats, debug_snapshot_v2_stats},
97};
98#[cfg(any(test, feature = "test-support"))]
99use cranpose_core::{
100 MemoryApplierDebugStats, RecomposeScopeRegistryDebugStats, SlotTableDebugStats,
101 debug_recompose_scope_registry_stats,
102};
103pub use cranpose_ui::{ImeEditorState, PlatformTextInputHandler};
104#[cfg(any(test, feature = "test-support"))]
105pub mod accessibility_audit;
106#[cfg(any(test, feature = "test-support"))]
107pub mod placed_semantics;
108
109#[derive(Debug, Clone, Copy, PartialEq, Default)]
119pub enum FrameRatePreference {
120 #[default]
125 Auto,
126 NoPreference,
128 Exact(f32),
131}
132
133impl FrameRatePreference {
134 pub const AUTO_QUIET_RATE_HZ: f32 = 60.0;
141
142 pub fn desired_rate_hz(
156 self,
157 producing_frames: bool,
158 boosted: bool,
159 panel_max_hz: Option<f32>,
160 ) -> f32 {
161 match self {
162 FrameRatePreference::Auto => {
163 if boosted {
164 panel_max_hz
165 .filter(|rate| *rate > 0.0)
166 .unwrap_or(Self::AUTO_QUIET_RATE_HZ)
167 } else if producing_frames {
168 Self::AUTO_QUIET_RATE_HZ
169 } else {
170 0.0
171 }
172 }
173 FrameRatePreference::NoPreference => 0.0,
174 FrameRatePreference::Exact(rate) if rate > 0.0 => rate,
175 FrameRatePreference::Exact(_) => 0.0,
176 }
177 }
178}
179
180pub(crate) struct ShellApp {
181 pub(crate) app_context: Rc<cranpose_ui::AppContext>,
182 pub(crate) runtime: StdRuntime,
183 pub(crate) composition: Composition<MemoryApplier>,
184 pub(crate) content: Box<dyn FnMut()>,
185 pub(crate) start_time: Instant,
186 pub(crate) last_frame_time_nanos: u64,
187 pub(crate) semantics_enabled: bool,
188 pub(crate) semantics_snapshot_revision: u64,
189 pub(crate) revealed_focus: Option<NodeId>,
190 pub(crate) layout_requested: bool,
191 pub(crate) content_moved: bool,
192 pub(crate) force_layout_pass: bool,
193 pub(crate) modifiers: Option<Modifiers>,
194 pub(crate) rotary_scroll_factor: f32,
195 #[cfg(all(feature = "clipboard-native", target_os = "linux"))]
196 pub(crate) clipboard: Option<arboard::Clipboard>,
197 pub(crate) dev_options: DevOptions,
198 pub(crate) inspector_projector: Option<inspector::InspectorProjector>,
199 pub(crate) fps_monitor: fps_monitor::FpsMonitor,
200 pub(crate) text_input_routes: Rc<RefCell<TextInputRoutes>>,
201 pub(crate) text_input_router_installed: bool,
202 pub(crate) window_roots_seen: Option<u64>,
203 pub(crate) rebuilt_frames_since_observation_prune: u32,
205 #[cfg(feature = "installed-fonts")]
207 pub(crate) installed_fonts: usize,
208}
209
210pub struct AppShell<R>
218where
219 R: Renderer,
220{
221 pub(crate) app: ShellApp,
222 pub(crate) surfaces: Vec<RootSurface<R>>,
223 pub(crate) routing_scratch: SurfaceRoutingScratch,
224 pub(crate) pending_dirty_nodes: Vec<NodeId>,
225 pub(crate) pending_layer_property_nodes: Vec<NodeId>,
226 pub(crate) geometry_scene_nodes: cranpose_ui::GeometrySceneNodes,
227}
228
229#[derive(Default)]
230pub(crate) struct SurfaceRoutingScratch {
231 pub(crate) attached: Vec<Option<NodeId>>,
232 pub(crate) owners: Vec<Option<NodeId>>,
233 pub(crate) seen: HashSet<(usize, NodeId)>,
234 pub(crate) attachment: SceneNodeAttachmentScratch,
235 pub(crate) window_roots: cranpose_ui::WindowRootRoutingScratch,
236}
237
238#[derive(Clone, Copy, Debug, PartialEq, Eq)]
239pub struct PointerEventTime {
241 pub platform_time_ms: Option<i64>,
243 pub animation_time_nanos: u64,
245}
246
247fn update_stage_telemetry_threshold_ms() -> Option<f64> {
248 static THRESHOLD_MS: std::sync::OnceLock<Option<f64>> = std::sync::OnceLock::new();
249 *THRESHOLD_MS.get_or_init(|| {
250 std::env::var("CRANPOSE_UPDATE_STAGE_TELEMETRY_MS")
251 .ok()
252 .and_then(|value| value.parse::<f64>().ok())
253 .filter(|value| value.is_finite() && *value >= 0.0)
254 })
255}
256
257#[derive(Clone, Copy, Debug)]
258struct UpdateStageTelemetry {
259 started_at: Instant,
260 after_frame_callbacks: Instant,
261 after_ui_drain: Instant,
262 after_reconcile: Instant,
263 after_process_frame: Instant,
264 should_render: bool,
265 reconcile_attempted: bool,
266 reconcile_changed: bool,
267}
268
269fn log_update_stage_telemetry(telemetry: UpdateStageTelemetry) {
270 let Some(threshold_ms) = update_stage_telemetry_threshold_ms() else {
271 return;
272 };
273 let total_ms = telemetry
274 .after_process_frame
275 .duration_since(telemetry.started_at)
276 .as_secs_f64()
277 * 1000.0;
278 if total_ms < threshold_ms {
279 return;
280 }
281
282 let frame_callbacks_ms = telemetry
283 .after_frame_callbacks
284 .duration_since(telemetry.started_at)
285 .as_secs_f64()
286 * 1000.0;
287 let ui_drain_ms = telemetry
288 .after_ui_drain
289 .duration_since(telemetry.after_frame_callbacks)
290 .as_secs_f64()
291 * 1000.0;
292 let reconcile_ms = telemetry
293 .after_reconcile
294 .duration_since(telemetry.after_ui_drain)
295 .as_secs_f64()
296 * 1000.0;
297 let process_frame_ms = telemetry
298 .after_process_frame
299 .duration_since(telemetry.after_reconcile)
300 .as_secs_f64()
301 * 1000.0;
302 eprintln!(
303 "[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={}",
304 telemetry.should_render, telemetry.reconcile_attempted, telemetry.reconcile_changed
305 );
306}
307
308#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
309pub enum FramePacingMode {
310 #[default]
313 Vsync,
314 Hard60,
315 Hard120,
316 NoVsync,
319}
320
321impl FramePacingMode {
322 pub const ALL: [Self; 4] = [Self::Vsync, Self::Hard60, Self::Hard120, Self::NoVsync];
323
324 pub fn label(self) -> &'static str {
325 match self {
326 Self::Vsync => "VSync",
327 Self::Hard60 => "60fps",
328 Self::Hard120 => "120fps",
329 Self::NoVsync => "NoVSync",
330 }
331 }
332
333 pub fn target_fps(self) -> Option<u32> {
334 match self {
335 Self::Hard60 => Some(60),
336 Self::Hard120 => Some(120),
337 Self::Vsync | Self::NoVsync => None,
338 }
339 }
340}
341
342#[derive(Clone, Copy, Debug, PartialEq)]
343pub struct FrameSchedule {
344 pub needs_update: bool,
345 pub needs_frame: bool,
346 pub next_deadline: Option<web_time::Instant>,
347}
348
349#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
350pub struct FrameUpdateResult {
351 pub visual_changed: bool,
352 pub structure_changed: bool,
353 pub content_moved: bool,
358 pub content_redrawn: bool,
361}
362
363pub trait PlatformFrameDriver {
364 fn request_frame(&self);
365 fn request_wake_at(&self, deadline: web_time::Instant);
366 fn clear_wake(&self);
367}
368
369#[derive(Debug)]
370pub struct FrameScheduler {
371 update_pending: AtomicBool,
372 frame_pending: AtomicBool,
373 next_deadline: Mutex<Option<web_time::Instant>>,
374}
375
376impl Default for FrameScheduler {
377 fn default() -> Self {
378 Self {
379 update_pending: AtomicBool::new(false),
380 frame_pending: AtomicBool::new(false),
381 next_deadline: Mutex::new(None),
382 }
383 }
384}
385
386impl FrameScheduler {
387 fn lock_deadline(&self) -> MutexGuard<'_, Option<web_time::Instant>> {
388 self.next_deadline
389 .lock()
390 .unwrap_or_else(PoisonError::into_inner)
391 }
392
393 pub fn record(&self, schedule: FrameSchedule) {
394 self.update_pending
395 .store(schedule.needs_update, Ordering::SeqCst);
396 self.frame_pending
397 .store(schedule.needs_frame, Ordering::SeqCst);
398 let mut next_deadline = self.lock_deadline();
399 *next_deadline = if schedule.needs_update {
400 None
401 } else {
402 schedule.next_deadline
403 };
404 }
405
406 pub fn schedule<D>(&self, schedule: FrameSchedule, driver: &D)
407 where
408 D: PlatformFrameDriver + ?Sized,
409 {
410 self.record(schedule);
411 schedule.apply_to(driver);
412 }
413
414 pub fn snapshot(&self) -> FrameSchedule {
415 FrameSchedule {
416 needs_update: self.update_pending.load(Ordering::SeqCst),
417 needs_frame: self.frame_pending.load(Ordering::SeqCst),
418 next_deadline: *self.lock_deadline(),
419 }
420 }
421}
422
423impl FrameSchedule {
424 pub fn apply_to<D>(self, driver: &D)
425 where
426 D: PlatformFrameDriver + ?Sized,
427 {
428 if self.needs_frame {
429 driver.clear_wake();
430 driver.request_frame();
431 } else if self.needs_update {
432 driver.request_wake_at(web_time::Instant::now());
433 } else if let Some(deadline) = self.next_deadline {
434 driver.request_wake_at(deadline);
435 } else {
436 driver.clear_wake();
437 }
438 }
439}
440
441#[derive(Clone, Copy, Debug)]
442pub(crate) struct DevOverlayControl {
443 bounds: Rect,
444 mode: FramePacingMode,
445}
446
447#[derive(Clone, Debug, Default)]
452pub struct DevOptions {
453 pub fps_counter: bool,
455 pub recomposition_counter: bool,
457 pub layout_timing: bool,
459 pub frame_pacing_controls: bool,
460 pub frame_pacing_mode: FramePacingMode,
461}
462
463#[cfg(any(test, feature = "test-support"))]
464#[doc(hidden)]
465#[derive(Clone, Copy, Debug)]
466pub struct RuntimeLeakDebugStats {
467 pub applier_stats: MemoryApplierDebugStats,
468 pub live_node_heap_bytes: usize,
469 pub recycled_node_heap_bytes: usize,
470 pub slot_table_heap_bytes: usize,
471 pub pass_stats: CompositionPassDebugStats,
472 pub slot_stats: SlotTableDebugStats,
473 pub runtime_stats: RuntimeDebugStats,
474 pub state_arena_stats: StateArenaDebugStats,
475 pub recompose_scope_stats: RecomposeScopeRegistryDebugStats,
476 pub snapshot_v2_stats: SnapshotV2DebugStats,
477 pub snapshot_pinning_stats: SnapshotPinningDebugStats,
478}
479
480impl ShellApp {
481 pub(crate) fn flush_semantics_invalidations(&mut self) {
482 if has_pending_semantics_invalidations() {
483 let mut applier = self.composition.applier_mut();
484 process_semantics_invalidations(|node_id| {
485 cranpose_core::bubble_semantics_dirty(&mut *applier, node_id);
486 });
487 }
488 }
489
490 pub(crate) fn request_layout_pass(&mut self) {
491 self.layout_requested = true;
492 }
493
494 pub(crate) fn request_forced_layout_pass(&mut self) {
495 self.layout_requested = true;
496 self.force_layout_pass = true;
497 }
498
499 pub(crate) fn composition_tree_needs_layout(&mut self) -> bool {
500 let Some(root) = self.composition.root() else {
501 return true;
502 };
503 let mut applier = self.composition.applier_mut();
504 cranpose_ui::tree_needs_layout(&mut *applier, root).unwrap_or_else(|err| {
505 log::warn!("Cannot check layout dirty status for root #{root}: {err}");
506 true
507 })
508 }
509
510 pub(crate) fn has_stale_work_in_context(&self) -> bool {
511 self.layout_requested
512 || peek_render_invalidation()
513 || peek_pointer_invalidation()
514 || peek_focus_invalidation()
515 || peek_layout_invalidation()
516 || cranpose_ui::has_pending_layout_repasses()
517 || cranpose_ui::has_pending_measure_repasses()
518 || cranpose_ui::has_pending_draw_repasses()
519 || cranpose_ui::has_pending_layer_property_repasses()
520 || has_pending_pointer_repasses()
521 || has_pending_focus_invalidations()
522 }
523
524 pub(crate) fn wants_frame_in_context(&self) -> bool {
525 self.layout_requested
526 || peek_render_invalidation()
527 || peek_pointer_invalidation()
528 || peek_focus_invalidation()
529 || peek_layout_invalidation()
530 || cranpose_ui::has_pending_layout_repasses()
531 || cranpose_ui::has_pending_measure_repasses()
532 || cranpose_ui::has_pending_layer_property_repasses()
533 || self.composition.should_render()
534 }
535
536 pub(crate) fn needs_ui_update_in_context(&self, surfaces_dirty: bool) -> bool {
537 surfaces_dirty
538 || self.has_stale_work_in_context()
539 || self.composition.runtime_handle().has_pending_ui()
540 || has_pending_semantics_invalidations()
541 || self.composition.should_render()
542 }
543
544 pub(crate) fn next_event_time(&self) -> Option<web_time::Instant> {
545 let app_context = Rc::clone(&self.app_context);
546 app_context.enter(cranpose_ui::next_cursor_blink_time)
547 }
548
549 pub(crate) fn frame_time_nanos_at(&self, now: Instant) -> u64 {
550 now.checked_duration_since(self.start_time)
551 .unwrap_or_default()
552 .as_nanos()
553 .min(u128::from(u64::MAX)) as u64
554 }
555
556 pub(crate) fn realtime_pointer_event_time(
557 &self,
558 platform_time_ms: Option<i64>,
559 ) -> PointerEventTime {
560 PointerEventTime {
561 platform_time_ms,
562 animation_time_nanos: self
563 .frame_time_nanos_at(Instant::now())
564 .max(self.last_frame_time_nanos),
565 }
566 }
567
568 pub(crate) fn install_text_input_router(&mut self) {
569 if self.text_input_router_installed {
570 return;
571 }
572 let router = Rc::new(TextInputRouter {
573 routes: Rc::clone(&self.text_input_routes),
574 });
575 let app_context = Rc::clone(&self.app_context);
576 app_context
577 .enter(|| cranpose_ui::text_input_session::set_platform_text_input_handler(router));
578 self.text_input_router_installed = true;
579 }
580}
581
582impl<R> AppShell<R>
583where
584 R: Renderer,
585 R::Error: Debug,
586{
587 pub fn new(renderer: R, root_key: Key, content: impl FnMut() + 'static) -> Self {
588 Self::new_with_size(renderer, root_key, content, (800, 600), (800.0, 600.0))
589 }
590
591 pub fn new_with_size(
592 renderer: R,
593 root_key: Key,
594 content: impl FnMut() + 'static,
595 buffer_size: (u32, u32),
596 viewport: (f32, f32),
597 ) -> Self {
598 Self::new_with_size_and_density(renderer, root_key, content, buffer_size, viewport, 1.0)
599 }
600
601 pub fn new_with_size_and_density(
604 mut renderer: R,
605 root_key: Key,
606 content: impl FnMut() + 'static,
607 buffer_size: (u32, u32),
608 viewport: (f32, f32),
609 density: f32,
610 ) -> Self {
611 renderer.set_root_scale(density);
612 let app_context = cranpose_ui::AppContext::new_with_density(density);
613 let runtime = StdRuntime::new();
614 let mut composition = Composition::with_runtime(MemoryApplier::new(), runtime.runtime());
615 let app_content = Rc::new(std::cell::RefCell::new(content));
616 let mut build: Box<dyn FnMut()> = Box::new(move || {
617 let app_content = Rc::clone(&app_content);
618 cranpose_ui::density::ProvideDensity(cranpose_ui::Density::from_host(), move || {
619 cranpose_ui::widgets::PopupHost(move || {
620 (app_content.borrow_mut())();
621 });
622 });
623 });
624 renderer.attach_app_context_services(&app_context);
625 app_context.enter(|| {
626 #[cfg(all(
627 feature = "clipboard-native",
628 not(target_arch = "wasm32"),
629 not(target_os = "android"),
630 not(target_os = "ios")
631 ))]
632 {
633 let clipboard =
634 std::rc::Rc::new(std::cell::RefCell::new(arboard::Clipboard::new().ok()));
635 cranpose_ui::clipboard_session::set_platform_clipboard(std::rc::Rc::new(
636 ShellClipboard { inner: clipboard },
637 ));
638 }
639 if let Err(err) = composition.render_stable(root_key, &mut *build) {
640 log::error!("initial render failed: {err}");
641 }
642 });
643 renderer.scene_mut().clear();
644 let app = ShellApp {
645 app_context,
646 runtime,
647 composition,
648 content: build,
649 start_time: Instant::now(),
650 last_frame_time_nanos: 0,
651 semantics_enabled: false,
652 semantics_snapshot_revision: 0,
653 revealed_focus: None,
654 layout_requested: true,
655 content_moved: false,
656 force_layout_pass: true,
657 modifiers: None,
658 rotary_scroll_factor: DEFAULT_ROTARY_SCROLL_FACTOR_DP,
659 #[cfg(all(feature = "clipboard-native", target_os = "linux"))]
660 clipboard: arboard::Clipboard::new().ok(),
661 dev_options: DevOptions::default(),
662 inspector_projector: None,
663 fps_monitor: fps_monitor::FpsMonitor::new(),
664 text_input_routes: Rc::new(RefCell::new(TextInputRoutes::default())),
665 text_input_router_installed: false,
666 window_roots_seen: None,
667 rebuilt_frames_since_observation_prune: 0,
668 #[cfg(feature = "installed-fonts")]
669 installed_fonts: 0,
670 };
671 let mut shell = Self {
672 app,
673 surfaces: vec![RootSurface::new(
674 RootId::Primary,
675 renderer,
676 buffer_size,
677 viewport,
678 )],
679 routing_scratch: SurfaceRoutingScratch::default(),
680 pending_dirty_nodes: Vec::new(),
681 pending_layer_property_nodes: Vec::new(),
682 geometry_scene_nodes: cranpose_ui::GeometrySceneNodes::default(),
683 };
684 shell.process_frame();
685 shell
686 }
687
688 pub fn app_context(&self) -> &Rc<cranpose_ui::AppContext> {
693 &self.app.app_context
694 }
695
696 pub fn surface(&mut self, root: RootId) -> Option<SurfaceMut<'_, R>> {
699 let index = self.surface_index(root)?;
700 Some(SurfaceMut::new(self, index))
701 }
702
703 pub fn primary(&mut self) -> SurfaceMut<'_, R> {
705 SurfaceMut::new(self, 0)
706 }
707
708 pub fn root_holding_the_press(&mut self) -> Option<RootId> {
717 let pressed = self
718 .surfaces
719 .iter()
720 .find_map(|surface| surface.hit_path_tracker.dispatch_order(PointerId::PRIMARY))?;
721 let primary_root = self.app.composition.root()?;
722 let mut applier = self.app.composition.applier_mut();
723 let holder = pressed.into_iter().find_map(|node| {
724 let node = applier.scene_node_attached_to(node, primary_root)?;
725 Some(cranpose_ui::nearest_window_root(&mut applier, node))
726 })?;
727 drop(applier);
728 self.surfaces
729 .iter()
730 .find(|surface| surface.owns_nodes_under(holder))
731 .map(|surface| surface.id)
732 }
733
734 fn surface_index(&self, root: RootId) -> Option<usize> {
735 self.surfaces.iter().position(|surface| surface.id == root)
736 }
737
738 pub fn add_window_surface(
748 &mut self,
749 window: u64,
750 renderer: R,
751 buffer_size: (u32, u32),
752 viewport: (f32, f32),
753 ) -> Option<R> {
754 let previous = self.remove_window_surface(window);
755 self.surfaces.push(RootSurface::new(
756 RootId::Window(window),
757 renderer,
758 buffer_size,
759 viewport,
760 ));
761 self.app.window_roots_seen = None;
762 self.sync_window_roots();
763 previous
764 }
765
766 pub fn remove_window_surface(&mut self, window: u64) -> Option<R> {
768 let root = RootId::Window(window);
769 let index = self.surface_index(root)?;
770 self.app.text_input_routes.borrow_mut().remove(root);
771 Some(self.surfaces.remove(index).renderer)
772 }
773
774 pub fn surface_ids(&self) -> Vec<RootId> {
776 self.surfaces.iter().map(|surface| surface.id).collect()
777 }
778
779 pub fn window_roots(&self) -> Vec<WindowRootEntry> {
783 let app_context = Rc::clone(&self.app.app_context);
784 app_context.enter(cranpose_ui::window_roots)
785 }
786
787 pub fn window_roots_revision(&self) -> u64 {
791 let app_context = Rc::clone(&self.app.app_context);
792 app_context.enter(cranpose_ui::window_roots_revision)
793 }
794
795 pub fn primary_has_content(&mut self) -> bool {
800 self.read_primary_boxes(cranpose_ui::has_placed_content)
801 .unwrap_or(false)
802 }
803
804 pub fn primary_content_size(&mut self) -> Option<Size> {
810 self.read_primary_boxes(cranpose_ui::placed_content_extent)
811 .flatten()
812 }
813
814 fn read_primary_boxes<T>(
818 &mut self,
819 read: impl FnOnce(&mut MemoryApplier, NodeId) -> Result<T, NodeError>,
820 ) -> Option<T> {
821 let app_context = Rc::clone(&self.app.app_context);
822 app_context.enter(|| {
823 let root = self.surfaces[0].root_node(&self.app)?;
824 let mut applier = self.app.composition.applier_mut();
825 read(&mut applier, root)
826 .inspect_err(|err| log::debug!("failed to read the primary root's boxes: {err}"))
827 .ok()
828 })
829 }
830
831 pub(crate) fn drag_and_drop_target_at(
837 &mut self,
838 point: cranpose_ui::DragAndDropPoint,
839 source: usize,
840 ) -> Option<(NodeId, cranpose_ui::Point)> {
841 let app_context = Rc::clone(&self.app.app_context);
842 let candidates: Vec<(usize, cranpose_ui::Point)> = match point.screen {
843 Some(screen) => (0..self.surfaces.len())
844 .rev()
845 .filter_map(|index| {
846 let local = self.surfaces[index].screen_point_inside(screen)?;
847 Some((index, local))
848 })
849 .collect(),
850 None => vec![(source, point.local)],
851 };
852 candidates.into_iter().find_map(|(index, local)| {
853 self.surfaces[index]
854 .renderer
855 .scene()
856 .hit_test_nodes(local.x, local.y)
857 .into_iter()
858 .find(|node| app_context.drag_and_drop().is_target(*node))
859 .map(|node| (node, local))
860 })
861 }
862
863 pub fn set_active_root(&mut self, root: RootId) {
867 self.app.text_input_routes.borrow_mut().set_active(root);
868 }
869
870 pub fn active_root(&self) -> RootId {
872 self.app.text_input_routes.borrow().active()
873 }
874
875 fn sync_window_roots(&mut self) {
876 let app_context = Rc::clone(&self.app.app_context);
877 app_context.enter(|| self.sync_window_roots_in_context());
878 }
879
880 pub(crate) fn sync_window_roots_in_context(&mut self) {
881 let revision = cranpose_ui::window_roots_revision();
882 if self.app.window_roots_seen == Some(revision) {
883 return;
884 }
885 self.app.window_roots_seen = Some(revision);
886 let entries = cranpose_ui::window_roots();
887 for surface in &mut self.surfaces {
888 let RootId::Window(id) = surface.id else {
889 continue;
890 };
891 let root = entries
892 .iter()
893 .find(|entry| entry.node as u64 == id)
894 .map(|entry| entry.node);
895 surface.set_root(root);
896 }
897 }
898
899 pub(crate) fn any_surface_dirty(&self) -> bool {
900 self.surfaces
901 .iter()
902 .any(|surface| surface.is_dirty || surface.scene_dirty)
903 }
904
905 pub(crate) fn mark_all_dirty(&mut self) {
906 for surface in &mut self.surfaces {
907 surface.is_dirty = true;
908 }
909 }
910
911 fn clear_surface_dirt(&mut self) {
912 for surface in &mut self.surfaces {
913 surface.is_dirty = false;
914 }
915 }
916
917 fn invalidate_dev_overlay_text(&mut self) {
918 for surface in &mut self.surfaces {
919 surface.invalidate_dev_overlay_text();
920 }
921 }
922
923 pub fn set_dev_options(&mut self, options: DevOptions) {
928 self.app.dev_options = options;
929 self.invalidate_dev_overlay_text();
930 let app_context = Rc::clone(&self.app.app_context);
931 app_context.enter(request_render_invalidation);
932 self.mark_all_dirty();
933 }
934
935 pub fn dev_options(&self) -> &DevOptions {
937 &self.app.dev_options
938 }
939
940 pub fn frame_pacing_mode(&self) -> FramePacingMode {
941 self.app.dev_options.frame_pacing_mode
942 }
943
944 pub fn current_fps(&self) -> f32 {
945 self.app.fps_monitor.current_fps()
946 }
947
948 pub fn fps_stats(&self) -> FpsStats {
949 self.app.fps_monitor.stats()
950 }
951
952 pub fn reset_fps_stats(&mut self) {
953 self.app.fps_monitor.reset_stats();
954 self.invalidate_dev_overlay_text();
955 }
956
957 pub fn record_presented_frame(
958 &mut self,
959 frame_started_at: Instant,
960 frame_finished_at: Instant,
961 ) {
962 self.app
963 .fps_monitor
964 .record_frame_work(frame_started_at, frame_finished_at);
965 }
966
967 #[cfg(any(test, feature = "test-support"))]
968 #[doc(hidden)]
969 pub fn record_presented_frame_for_test(
970 &mut self,
971 frame_started_nanos: u64,
972 frame_finished_nanos: u64,
973 ) {
974 let started = self.app.start_time + std::time::Duration::from_nanos(frame_started_nanos);
975 let finished = self.app.start_time + std::time::Duration::from_nanos(frame_finished_nanos);
976 self.record_presented_frame(started, finished);
977 }
978
979 pub fn set_frame_pacing_mode(&mut self, mode: FramePacingMode) {
980 if self.app.dev_options.frame_pacing_mode == mode {
981 return;
982 }
983 self.app.dev_options.frame_pacing_mode = mode;
984 self.invalidate_dev_overlay_text();
985 let app_context = Rc::clone(&self.app.app_context);
986 app_context.enter(request_render_invalidation);
987 self.mark_all_dirty();
988 }
989
990 pub fn dev_overlay_control_center(&self, mode: FramePacingMode) -> Option<(f32, f32)> {
997 self.surfaces[0].dev_overlay_control_center(mode)
998 }
999
1000 pub fn set_viewport(&mut self, width: f32, height: f32) {
1003 if self.surfaces[0].viewport == (width, height) {
1004 return;
1005 }
1006 self.primary().set_viewport(width, height);
1007 self.process_frame();
1008 }
1009
1010 pub fn viewport_size(&self) -> (f32, f32) {
1011 self.surfaces[0].viewport
1012 }
1013
1014 pub fn set_screen_origin(&mut self, origin: Option<cranpose_ui_graphics::Point>) {
1017 self.surfaces[0].screen_origin = origin;
1018 }
1019
1020 pub fn set_buffer_size(&mut self, width: u32, height: u32) {
1021 self.surfaces[0].buffer_size = (width, height);
1022 }
1023
1024 pub fn buffer_size(&self) -> (u32, u32) {
1025 self.surfaces[0].buffer_size
1026 }
1027
1028 pub fn scene(&self) -> &R::Scene {
1029 self.surfaces[0].renderer.scene()
1030 }
1031
1032 pub fn renderer(&mut self) -> &mut R {
1033 &mut self.surfaces[0].renderer
1034 }
1035
1036 #[cfg(not(target_arch = "wasm32"))]
1037 pub fn set_frame_waker(&mut self, waker: impl Fn() + Send + Sync + 'static) {
1038 self.app.runtime.set_frame_waker(waker);
1039 }
1040
1041 #[cfg(target_arch = "wasm32")]
1042 pub fn set_frame_waker(&mut self, waker: impl Fn() + 'static) {
1043 self.app.runtime.set_frame_waker(waker);
1044 }
1045
1046 pub fn clear_frame_waker(&mut self) {
1047 self.app.runtime.clear_frame_waker();
1048 }
1049
1050 pub fn should_render(&self) -> bool {
1051 let app_context = Rc::clone(&self.app.app_context);
1052 app_context.enter(|| {
1053 self.app.wants_frame_in_context()
1054 || self.surfaces.iter().any(|surface| surface.scene_dirty)
1055 })
1056 }
1057
1058 pub fn needs_update(&self) -> bool {
1059 let app_context = Rc::clone(&self.app.app_context);
1060 app_context.enter(|| {
1061 self.app
1062 .needs_ui_update_in_context(self.any_surface_dirty())
1063 })
1064 }
1065
1066 pub fn needs_update_without_frame(&self) -> bool {
1069 let app_context = Rc::clone(&self.app.app_context);
1070 app_context.enter(|| {
1071 self.any_surface_dirty()
1072 || self.app.has_stale_work_in_context()
1073 || self.app.composition.should_recompose()
1074 || self.app.composition.runtime_handle().has_pending_ui()
1075 || has_pending_semantics_invalidations()
1076 || cranpose_ui::next_cursor_blink_time().is_some_and(|at| at <= Instant::now())
1077 || self
1078 .surfaces
1079 .iter()
1080 .any(|surface| surface.renderer_warmup_due(&self.app))
1081 })
1082 }
1083
1084 pub fn has_pending_ui(&self) -> bool {
1092 let app_context = Rc::clone(&self.app.app_context);
1093 app_context.enter(|| self.app.composition.runtime_handle().has_pending_ui())
1094 }
1095
1096 pub fn run_pending_tasks(&mut self) {
1103 let app_context = Rc::clone(&self.app.app_context);
1104 app_context.enter(|| {
1105 let runtime_handle = self.app.composition.runtime_handle();
1106 if runtime_handle.has_pending_ui() {
1107 runtime_handle.with_deferred_state_releases(|| runtime_handle.drain_ui());
1108 }
1109 });
1110 }
1111
1112 pub fn needs_redraw(&self) -> bool {
1125 let app_context = Rc::clone(&self.app.app_context);
1126 app_context.enter(|| self.surfaces[0].needs_redraw_in_context(&self.app))
1127 }
1128
1129 pub fn mark_dirty(&mut self) {
1131 self.surfaces[0].is_dirty = true;
1132 }
1133
1134 pub fn request_root_render(&mut self) {
1135 self.app.composition.request_root_render();
1136 self.app.request_forced_layout_pass();
1137 let app_context = Rc::clone(&self.app.app_context);
1138 app_context.enter(request_render_invalidation);
1139 self.mark_all_dirty();
1140 }
1141
1142 pub fn set_density(&mut self, density: f32) {
1145 self.surfaces[0].renderer.set_root_scale(density);
1146 let app_context = Rc::clone(&self.app.app_context);
1147 let changed = app_context.enter(|| {
1148 let previous = cranpose_ui::current_density().to_bits();
1149 cranpose_ui::set_density(density);
1150 previous != cranpose_ui::current_density().to_bits()
1151 });
1152 if changed {
1153 self.request_root_render();
1154 }
1155 }
1156
1157 pub fn set_font_scale(&mut self, font_scale: f32) {
1168 self.set_font_scale_curve(cranpose_ui::FontScaleCurve::linear(font_scale));
1169 }
1170
1171 pub fn set_font_scale_curve(&mut self, curve: cranpose_ui::FontScaleCurve) {
1178 let app_context = Rc::clone(&self.app.app_context);
1179 let changed = app_context.enter(|| {
1180 let previous = cranpose_ui::current_font_scale_curve();
1181 cranpose_ui::set_font_scale_curve(curve);
1182 previous != cranpose_ui::current_font_scale_curve()
1183 });
1184 if changed {
1185 self.request_root_render();
1186 }
1187 }
1188
1189 #[cfg(any(test, feature = "test-support"))]
1190 #[doc(hidden)]
1191 pub fn debug_current_density(&self) -> f32 {
1192 let app_context = Rc::clone(&self.app.app_context);
1193 app_context.enter(cranpose_ui::current_density)
1194 }
1195
1196 #[cfg(any(test, feature = "test-support"))]
1197 #[doc(hidden)]
1198 pub fn debug_current_font_scale(&self) -> f32 {
1199 let app_context = Rc::clone(&self.app.app_context);
1200 app_context.enter(cranpose_ui::current_font_scale)
1201 }
1202
1203 #[cfg(any(test, feature = "test-support"))]
1204 #[doc(hidden)]
1205 pub fn debug_current_font_scale_curve(&self) -> cranpose_ui::FontScaleCurve {
1206 let app_context = Rc::clone(&self.app.app_context);
1207 app_context.enter(cranpose_ui::current_font_scale_curve)
1208 }
1209
1210 #[cfg(any(test, feature = "test-support"))]
1211 #[doc(hidden)]
1212 pub fn debug_enter_app_context<T>(&self, block: impl FnOnce() -> T) -> T {
1213 let app_context = Rc::clone(&self.app.app_context);
1214 app_context.enter(block)
1215 }
1216
1217 pub fn has_active_animations(&self) -> bool {
1219 self.app.composition.should_render()
1220 }
1221
1222 pub fn has_transient_frame_callbacks(&self) -> bool {
1223 self.app
1224 .composition
1225 .runtime_handle()
1226 .has_transient_frame_callbacks()
1227 }
1228
1229 pub fn has_active_pointer_gesture(&self) -> bool {
1230 self.surfaces[0].has_active_pointer_gesture()
1231 }
1232
1233 pub fn frame_owed(&self) -> bool {
1235 self.surfaces[0].frame_owed
1236 }
1237
1238 pub fn take_frame_owed(&mut self) -> bool {
1240 std::mem::take(&mut self.surfaces[0].frame_owed)
1241 }
1242
1243 pub fn next_event_time(&self) -> Option<web_time::Instant> {
1246 self.app.next_event_time()
1247 }
1248
1249 fn compute_frame_schedule(&self) -> FrameSchedule {
1250 self.surfaces[0].compute_frame_schedule(&self.app, self.any_surface_dirty())
1251 }
1252
1253 pub fn frame_schedule(&self) -> FrameSchedule {
1254 let schedule = self.compute_frame_schedule();
1255 self.surfaces[0].frame_scheduler.record(schedule);
1256 schedule
1257 }
1258
1259 pub fn schedule_platform_frame<D>(&self, driver: &D) -> FrameSchedule
1260 where
1261 D: PlatformFrameDriver + ?Sized,
1262 {
1263 let schedule = self.compute_frame_schedule();
1264 self.surfaces[0].frame_scheduler.schedule(schedule, driver);
1265 schedule
1266 }
1267
1268 pub fn frame_scheduler_snapshot(&self) -> FrameSchedule {
1269 self.surfaces[0].frame_scheduler.snapshot()
1270 }
1271
1272 pub fn realtime_pointer_event_time(&self, platform_time_ms: Option<i64>) -> PointerEventTime {
1274 self.app.realtime_pointer_event_time(platform_time_ms)
1275 }
1276
1277 pub fn exact_pointer_event_time(&self, platform_time_ms: Option<i64>) -> PointerEventTime {
1279 PointerEventTime {
1280 platform_time_ms,
1281 animation_time_nanos: self.app.last_frame_time_nanos,
1282 }
1283 }
1284
1285 pub fn update_after_frame_interval(
1286 &mut self,
1287 frame_interval: std::time::Duration,
1288 ) -> FrameUpdateResult {
1289 let wall_frame_time = self.app.frame_time_nanos_at(Instant::now());
1290 let base_frame_time = self.app.last_frame_time_nanos.max(wall_frame_time);
1291 let frame_time = base_frame_time
1292 .saturating_add(frame_interval.as_nanos().min(u128::from(u64::MAX)) as u64);
1293 self.update_at_frame_time_nanos(frame_time)
1294 }
1295
1296 pub fn update_after_exact_interval(
1302 &mut self,
1303 frame_interval: std::time::Duration,
1304 ) -> FrameUpdateResult {
1305 let frame_time = self
1306 .app
1307 .last_frame_time_nanos
1308 .saturating_add(frame_interval.as_nanos().min(u128::from(u64::MAX)) as u64);
1309 self.update_at_frame_time_nanos(frame_time)
1310 }
1311
1312 pub fn dispatch_frame_at(&mut self, frame_time: Instant) {
1318 let frame_time = self.app.frame_time_nanos_at(frame_time);
1319 let app_context = Rc::clone(&self.app.app_context);
1320 app_context.enter(|| {
1321 let runtime_handle = self.app.runtime.runtime_handle();
1322 runtime_handle.with_deferred_state_releases(|| {
1323 self.app.last_frame_time_nanos = frame_time.max(self.app.last_frame_time_nanos);
1324 self.app
1325 .runtime
1326 .drain_frame_callbacks(self.app.last_frame_time_nanos);
1327 runtime_handle.drain_ui();
1328 self.dispatch_requested_mouse_moves(self.app.last_frame_time_nanos);
1329 });
1330 });
1331 }
1332
1333 pub fn update_without_frame(&mut self) -> FrameUpdateResult {
1336 self.update_with_frame_time(None)
1337 }
1338
1339 pub fn update_at_frame_time_nanos(&mut self, frame_time: u64) -> FrameUpdateResult {
1343 self.update_with_frame_time(Some(frame_time))
1344 }
1345
1346 fn update_with_frame_time(&mut self, frame_time: Option<u64>) -> FrameUpdateResult {
1347 let app_context = Rc::clone(&self.app.app_context);
1348 app_context.enter(|| {
1349 let update_started_at = Instant::now();
1350 let runtime_handle = self.app.runtime.runtime_handle();
1351 runtime_handle.with_deferred_state_releases(|| {
1352 if let Some(frame_time) = frame_time {
1353 self.app.last_frame_time_nanos = frame_time.max(self.app.last_frame_time_nanos);
1354 self.app.runtime.drain_frame_callbacks(self.app.last_frame_time_nanos);
1355 }
1356 let after_frame_callbacks = Instant::now();
1357 runtime_handle.drain_ui();
1358 self.dispatch_requested_mouse_moves(self.app.last_frame_time_nanos);
1359 let after_ui_drain = Instant::now();
1360 let should_render = self.app.composition.should_recompose();
1361 let mut reconcile_attempted = false;
1362 let mut reconcile_changed = false;
1363 if should_render {
1364 log::trace!(
1365 target: "cranpose::input",
1366 "update begin: should_render=true layout_requested={} scene_dirty={} is_dirty={}",
1367 self.app.layout_requested,
1368 self.surfaces[0].scene_dirty,
1369 self.surfaces[0].is_dirty
1370 );
1371 (reconcile_attempted, reconcile_changed) = self.reconcile_in_context();
1372 }
1373 let after_reconcile = Instant::now();
1374 let result = self.process_frame_in_context(reconcile_changed);
1375 let after_process_frame = Instant::now();
1376 log_update_stage_telemetry(UpdateStageTelemetry {
1377 started_at: update_started_at,
1378 after_frame_callbacks,
1379 after_ui_drain,
1380 after_reconcile,
1381 after_process_frame,
1382 should_render,
1383 reconcile_attempted,
1384 reconcile_changed,
1385 });
1386 self.clear_surface_dirt();
1387 result
1388 })
1389 })
1390 }
1391
1392 fn dispatch_requested_mouse_moves(&mut self, frame_time: u64) {
1393 use cranpose_ui::mouse_input::MouseInputTarget;
1394 let pending = self.app.app_context.pending_mouse_moves();
1395 for _ in 0..pending {
1396 let Some(request) = self.app.app_context.take_mouse_move() else {
1397 break;
1398 };
1399 let root = match request.target {
1400 MouseInputTarget::Primary => RootId::Primary,
1401 MouseInputTarget::Window(id) => RootId::Window(id),
1402 };
1403 if let Some(mut surface) = self.surface(root) {
1404 surface.set_pointer_source(PointerSource::Mouse);
1405 surface.set_cursor_at_event_time(request.position.x, request.position.y, PointerEventTime {
1406 platform_time_ms: Some((frame_time / 1_000_000).min(i64::MAX as u64) as i64),
1407 animation_time_nanos: frame_time,
1408 });
1409 }
1410 }
1411 }
1412
1413 fn reconcile_in_context(&mut self) -> (bool, bool) {
1414 let Some(root_key) = self.app.composition.root_key() else {
1415 return (false, false);
1416 };
1417 match self
1418 .app
1419 .composition
1420 .reconcile(root_key, &mut *self.app.content)
1421 {
1422 Ok(changed) => {
1423 log::trace!(
1424 target: "cranpose::input",
1425 "reconcile changed={changed}"
1426 );
1427 if changed {
1428 self.app.fps_monitor.record_recomposition();
1429 if self.app.composition_tree_needs_layout() {
1430 self.app.request_layout_pass();
1431 }
1432 request_render_invalidation();
1433 }
1434 (true, changed)
1435 }
1436 Err(NodeError::Missing { id }) => {
1437 log::debug!("Recomposition skipped: node {id} no longer exists");
1438 self.app.request_layout_pass();
1439 request_render_invalidation();
1440 (true, false)
1441 }
1442 Err(err) => {
1443 log::error!("recomposition failed: {err}");
1444 self.app.request_layout_pass();
1445 request_render_invalidation();
1446 (true, false)
1447 }
1448 }
1449 }
1450
1451 pub fn update(&mut self) -> FrameUpdateResult {
1452 let frame_time = self.app.frame_time_nanos_at(Instant::now());
1453 self.update_at_frame_time_nanos(frame_time)
1454 }
1455}
1456
1457impl<R> Drop for AppShell<R>
1458where
1459 R: Renderer,
1460{
1461 fn drop(&mut self) {
1462 self.app.runtime.clear_frame_waker();
1463 }
1464}
1465
1466pub fn default_root_key() -> Key {
1467 location_key(file!(), line!(), column!())
1468}
1469
1470#[cfg(test)]
1471#[path = "tests/app_shell_frame_pacing_tests.rs"]
1472mod frame_pacing_tests;
1473
1474#[cfg(test)]
1475#[path = "tests/app_shell_tests.rs"]
1476mod tests;
1477
1478#[cfg(test)]
1479#[path = "tests/surface_tests.rs"]
1480mod surface_tests;