1#![allow(clippy::type_complexity)]
2
3mod fps_monitor;
4mod hit_path_tracker;
5
6pub use fps_monitor::{
8 current_fps, fps_display, fps_display_detailed, fps_stats, record_recomposition, FpsStats,
9};
10
11use std::fmt::Debug;
12use web_time::Instant;
14
15use cranpose_core::{
16 enter_event_handler, exit_event_handler, location_key, run_in_mutable_snapshot, Applier,
17 Composition, Key, MemoryApplier, NodeError, NodeId,
18};
19use cranpose_foundation::{PointerButton, PointerButtons, PointerEvent, PointerEventKind};
20use cranpose_render_common::{HitTestTarget, RenderScene, Renderer};
21use cranpose_runtime_std::StdRuntime;
22use cranpose_ui::{
23 has_pending_focus_invalidations, has_pending_pointer_repasses, log_layout_tree,
24 log_render_scene, log_screen_summary, peek_focus_invalidation, peek_layout_invalidation,
25 peek_pointer_invalidation, peek_render_invalidation, process_focus_invalidations,
26 process_pointer_repasses, request_render_invalidation, take_draw_repass_nodes,
27 take_focus_invalidation, take_layout_invalidation, take_pointer_invalidation,
28 take_render_invalidation, HeadlessRenderer, LayoutNode, LayoutTree, SemanticsTree,
29 SubcomposeLayoutNode,
30};
31use cranpose_ui_graphics::{Point, Size};
32use hit_path_tracker::{HitPathTracker, PointerId};
33use std::collections::HashSet;
34
35pub use cranpose_ui::{KeyCode, KeyEvent, KeyEventType, Modifiers};
37
38pub struct AppShell<R>
39where
40 R: Renderer,
41{
42 runtime: StdRuntime,
43 composition: Composition<MemoryApplier>,
44 renderer: R,
45 cursor: (f32, f32),
46 viewport: (f32, f32),
47 buffer_size: (u32, u32),
48 start_time: Instant,
49 layout_tree: Option<LayoutTree>,
50 semantics_tree: Option<SemanticsTree>,
51 layout_dirty: bool,
52 scene_dirty: bool,
53 is_dirty: bool,
54 buttons_pressed: PointerButtons,
56 hit_path_tracker: HitPathTracker,
63 #[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))]
65 clipboard: Option<arboard::Clipboard>,
66 dev_options: DevOptions,
68}
69
70#[derive(Clone, Debug, Default)]
75pub struct DevOptions {
76 pub fps_counter: bool,
78 pub recomposition_counter: bool,
80 pub layout_timing: bool,
82}
83
84impl<R> AppShell<R>
85where
86 R: Renderer,
87 R::Error: Debug,
88{
89 pub fn new(mut renderer: R, root_key: Key, content: impl FnMut() + 'static) -> Self {
90 fps_monitor::init_fps_tracker();
92
93 let runtime = StdRuntime::new();
94 let mut composition = Composition::with_runtime(MemoryApplier::new(), runtime.runtime());
95 let build = content;
96 if let Err(err) = composition.render(root_key, build) {
97 log::error!("initial render failed: {err}");
98 }
99 renderer.scene_mut().clear();
100 let mut shell = Self {
101 runtime,
102 composition,
103 renderer,
104 cursor: (0.0, 0.0),
105 viewport: (800.0, 600.0),
106 buffer_size: (800, 600),
107 start_time: Instant::now(),
108 layout_tree: None,
109 semantics_tree: None,
110 layout_dirty: true,
111 scene_dirty: true,
112 is_dirty: true,
113 buttons_pressed: PointerButtons::NONE,
114 hit_path_tracker: HitPathTracker::new(),
115 #[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))]
116 clipboard: arboard::Clipboard::new().ok(),
117 dev_options: DevOptions::default(),
118 };
119 shell.process_frame();
120 shell
121 }
122
123 pub fn set_dev_options(&mut self, options: DevOptions) {
128 self.dev_options = options;
129 }
130
131 pub fn dev_options(&self) -> &DevOptions {
133 &self.dev_options
134 }
135
136 pub fn set_viewport(&mut self, width: f32, height: f32) {
137 self.viewport = (width, height);
138 self.layout_dirty = true;
139 self.mark_dirty();
140 self.process_frame();
141 }
142
143 pub fn set_buffer_size(&mut self, width: u32, height: u32) {
144 self.buffer_size = (width, height);
145 }
146
147 pub fn buffer_size(&self) -> (u32, u32) {
148 self.buffer_size
149 }
150
151 pub fn scene(&self) -> &R::Scene {
152 self.renderer.scene()
153 }
154
155 pub fn renderer(&mut self) -> &mut R {
156 &mut self.renderer
157 }
158
159 #[cfg(not(target_arch = "wasm32"))]
160 pub fn set_frame_waker(&mut self, waker: impl Fn() + Send + Sync + 'static) {
161 self.runtime.set_frame_waker(waker);
162 }
163
164 #[cfg(target_arch = "wasm32")]
165 pub fn set_frame_waker(&mut self, waker: impl Fn() + Send + 'static) {
166 self.runtime.set_frame_waker(waker);
167 }
168
169 pub fn clear_frame_waker(&mut self) {
170 self.runtime.clear_frame_waker();
171 }
172
173 pub fn should_render(&self) -> bool {
174 if self.layout_dirty
175 || self.scene_dirty
176 || peek_render_invalidation()
177 || peek_pointer_invalidation()
178 || peek_focus_invalidation()
179 || peek_layout_invalidation()
180 {
181 return true;
182 }
183 self.runtime.take_frame_request() || self.composition.should_render()
184 }
185
186 pub fn needs_redraw(&self) -> bool {
189 self.is_dirty || self.layout_dirty || self.has_active_animations()
190 }
191
192 pub fn mark_dirty(&mut self) {
194 self.is_dirty = true;
195 }
196
197 pub fn has_active_animations(&self) -> bool {
199 self.runtime.take_frame_request() || self.composition.should_render()
200 }
201
202 pub fn next_event_time(&self) -> Option<web_time::Instant> {
205 cranpose_ui::next_cursor_blink_time()
206 }
207
208 fn resolve_hit_path(
215 &self,
216 pointer: PointerId,
217 ) -> Vec<<<R as Renderer>::Scene as RenderScene>::HitTarget> {
218 let Some(node_ids) = self.hit_path_tracker.get_path(pointer) else {
219 return Vec::new();
220 };
221
222 let scene = self.renderer.scene();
223 node_ids
224 .iter()
225 .filter_map(|&id| scene.find_target(id))
226 .collect()
227 }
228
229 pub fn update(&mut self) {
230 let now = Instant::now();
231 let frame_time = now
232 .checked_duration_since(self.start_time)
233 .unwrap_or_default()
234 .as_nanos() as u64;
235 self.runtime.drain_frame_callbacks(frame_time);
236 self.runtime.runtime_handle().drain_ui();
237 if self.composition.should_render() {
238 match self.composition.process_invalid_scopes() {
239 Ok(changed) => {
240 if changed {
241 fps_monitor::record_recomposition();
242 self.layout_dirty = true;
243 if let Some(root_id) = self.composition.root() {
246 let _ = self.composition.applier_mut().with_node::<LayoutNode, _>(
247 root_id,
248 |node| {
249 node.mark_needs_measure();
250 },
251 );
252 }
253 request_render_invalidation();
254 }
255 }
256 Err(NodeError::Missing { id }) => {
257 log::debug!("Recomposition skipped: node {} no longer exists", id);
260 self.layout_dirty = true;
261 request_render_invalidation();
262 }
263 Err(err) => {
264 log::error!("recomposition failed: {err}");
265 self.layout_dirty = true;
266 request_render_invalidation();
267 }
268 }
269 }
270 self.process_frame();
271 self.is_dirty = false;
273 }
274
275 pub fn set_cursor(&mut self, x: f32, y: f32) -> bool {
276 self.cursor = (x, y);
277
278 if self.buttons_pressed != PointerButtons::NONE {
282 if self.hit_path_tracker.has_path(PointerId::PRIMARY) {
283 let targets = self.resolve_hit_path(PointerId::PRIMARY);
285
286 if !targets.is_empty() {
287 let event =
288 PointerEvent::new(PointerEventKind::Move, Point { x, y }, Point { x, y })
289 .with_buttons(self.buttons_pressed);
290
291 for hit in targets {
292 hit.dispatch(event.clone());
293 if event.is_consumed() {
294 break;
295 }
296 }
297 self.mark_dirty();
298 return true;
299 }
300
301 let hits = self.renderer.scene().hit_test(x, y);
304 if !hits.is_empty() {
305 let node_ids: Vec<_> = hits.iter().map(|h| h.node_id()).collect();
306 self.hit_path_tracker
307 .add_hit_path(PointerId::PRIMARY, node_ids);
308 let event =
309 PointerEvent::new(PointerEventKind::Move, Point { x, y }, Point { x, y })
310 .with_buttons(self.buttons_pressed);
311 for hit in hits {
312 hit.dispatch(event.clone());
313 if event.is_consumed() {
314 break;
315 }
316 }
317 self.mark_dirty();
318 return true;
319 }
320 return false;
321 }
322
323 return false;
326 }
327
328 let hits = self.renderer.scene().hit_test(x, y);
330 if !hits.is_empty() {
331 let event = PointerEvent::new(PointerEventKind::Move, Point { x, y }, Point { x, y })
332 .with_buttons(self.buttons_pressed); for hit in hits {
334 hit.dispatch(event.clone());
335 if event.is_consumed() {
336 break;
337 }
338 }
339 self.mark_dirty();
340 true
341 } else {
342 false
343 }
344 }
345
346 pub fn pointer_pressed(&mut self) -> bool {
347 enter_event_handler();
348 let result = self.pointer_pressed_inner();
349 exit_event_handler();
350 result
351 }
352
353 fn pointer_pressed_inner(&mut self) -> bool {
354 self.buttons_pressed.insert(PointerButton::Primary);
356
357 let hits = self.renderer.scene().hit_test(self.cursor.0, self.cursor.1);
365
366 let node_ids: Vec<_> = hits.iter().map(|h| h.node_id()).collect();
368 self.hit_path_tracker
369 .add_hit_path(PointerId::PRIMARY, node_ids);
370
371 if !hits.is_empty() {
372 let event = PointerEvent::new(
373 PointerEventKind::Down,
374 Point {
375 x: self.cursor.0,
376 y: self.cursor.1,
377 },
378 Point {
379 x: self.cursor.0,
380 y: self.cursor.1,
381 },
382 )
383 .with_buttons(self.buttons_pressed);
384
385 for hit in hits {
387 hit.dispatch(event.clone());
388 if event.is_consumed() {
389 break;
390 }
391 }
392 self.mark_dirty();
393 true
394 } else {
395 false
396 }
397 }
398
399 pub fn pointer_released(&mut self) -> bool {
400 enter_event_handler();
401 let result = self.pointer_released_inner();
402 exit_event_handler();
403 result
404 }
405
406 fn pointer_released_inner(&mut self) -> bool {
407 self.buttons_pressed.remove(PointerButton::Primary);
410 let corrected_buttons = self.buttons_pressed;
411
412 let targets = self.resolve_hit_path(PointerId::PRIMARY);
414
415 self.hit_path_tracker.remove_path(PointerId::PRIMARY);
417
418 if !targets.is_empty() {
419 let event = PointerEvent::new(
420 PointerEventKind::Up,
421 Point {
422 x: self.cursor.0,
423 y: self.cursor.1,
424 },
425 Point {
426 x: self.cursor.0,
427 y: self.cursor.1,
428 },
429 )
430 .with_buttons(corrected_buttons);
431
432 for hit in targets {
433 hit.dispatch(event.clone());
434 if event.is_consumed() {
435 break;
436 }
437 }
438 self.mark_dirty();
439 true
440 } else {
441 false
442 }
443 }
444
445 pub fn cancel_gesture(&mut self) {
451 let targets = self.resolve_hit_path(PointerId::PRIMARY);
453
454 self.hit_path_tracker.clear();
456 self.buttons_pressed = PointerButtons::NONE;
457
458 if !targets.is_empty() {
459 let event = PointerEvent::new(
460 PointerEventKind::Cancel,
461 Point {
462 x: self.cursor.0,
463 y: self.cursor.1,
464 },
465 Point {
466 x: self.cursor.0,
467 y: self.cursor.1,
468 },
469 );
470
471 for hit in targets {
472 hit.dispatch(event.clone());
473 }
474 self.mark_dirty();
475 }
476 }
477 pub fn on_key_event(&mut self, event: &KeyEvent) -> bool {
484 enter_event_handler();
485 let result = self.on_key_event_inner(event);
486 exit_event_handler();
487 result
488 }
489
490 fn on_key_event_inner(&mut self, event: &KeyEvent) -> bool {
492 use KeyEventType::KeyDown;
493
494 if event.event_type == KeyDown && event.modifiers.command_or_ctrl() {
496 #[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))]
499 {
500 match event.key_code {
501 KeyCode::C => {
503 let text = self.on_copy();
505 if let (Some(text), Some(clipboard)) = (text, self.clipboard.as_mut()) {
506 let _ = clipboard.set_text(&text);
507 return true;
508 }
509 }
510 KeyCode::X => {
512 let text = self.on_cut();
514 if let (Some(text), Some(clipboard)) = (text, self.clipboard.as_mut()) {
515 let _ = clipboard.set_text(&text);
516 self.mark_dirty();
517 self.layout_dirty = true;
518 return true;
519 }
520 }
521 KeyCode::V => {
523 let text = self.clipboard.as_mut().and_then(|cb| cb.get_text().ok());
525 if let Some(text) = text {
526 if self.on_paste(&text) {
527 return true;
528 }
529 }
530 }
531 _ => {}
532 }
533 }
534 }
535
536 if !cranpose_ui::text_field_focus::has_focused_field() {
538 return false;
539 }
540
541 let handled = run_in_mutable_snapshot(|| {
545 cranpose_ui::text_field_focus::dispatch_key_event(event)
548 })
549 .unwrap_or(false);
550
551 if handled {
552 self.mark_dirty();
554 self.layout_dirty = true;
555 }
556
557 handled
558 }
559
560 pub fn on_paste(&mut self, text: &str) -> bool {
564 let handled =
568 run_in_mutable_snapshot(|| cranpose_ui::text_field_focus::dispatch_paste(text))
569 .unwrap_or(false);
570
571 if handled {
572 self.mark_dirty();
573 self.layout_dirty = true;
574 }
575
576 handled
577 }
578
579 pub fn on_copy(&mut self) -> Option<String> {
583 cranpose_ui::text_field_focus::dispatch_copy()
585 }
586
587 pub fn on_cut(&mut self) -> Option<String> {
591 let text = cranpose_ui::text_field_focus::dispatch_cut();
593
594 if text.is_some() {
595 self.mark_dirty();
596 self.layout_dirty = true;
597 }
598
599 text
600 }
601
602 #[cfg(all(target_os = "linux", not(target_arch = "wasm32")))]
606 pub fn set_primary_selection(&mut self, text: &str) {
607 use arboard::{LinuxClipboardKind, SetExtLinux};
608 if let Some(ref mut clipboard) = self.clipboard {
609 let result = clipboard
610 .set()
611 .clipboard(LinuxClipboardKind::Primary)
612 .text(text.to_string());
613 if let Err(e) = result {
614 log::debug!("Primary selection set failed: {:?}", e);
616 }
617 }
618 }
619
620 #[cfg(all(target_os = "linux", not(target_arch = "wasm32")))]
623 pub fn get_primary_selection(&mut self) -> Option<String> {
624 use arboard::{GetExtLinux, LinuxClipboardKind};
625 if let Some(ref mut clipboard) = self.clipboard {
626 clipboard
627 .get()
628 .clipboard(LinuxClipboardKind::Primary)
629 .text()
630 .ok()
631 } else {
632 None
633 }
634 }
635
636 #[cfg(all(not(target_os = "linux"), not(target_arch = "wasm32")))]
637 pub fn get_primary_selection(&mut self) -> Option<String> {
638 None
639 }
640
641 pub fn sync_selection_to_primary(&mut self) {
644 #[cfg(all(target_os = "linux", not(target_arch = "wasm32")))]
645 {
646 if let Some(text) = self.on_copy() {
647 self.set_primary_selection(&text);
648 }
649 }
650 }
651
652 pub fn on_ime_preedit(&mut self, text: &str, cursor: Option<(usize, usize)>) -> bool {
660 let handled = run_in_mutable_snapshot(|| {
662 cranpose_ui::text_field_focus::dispatch_ime_preedit(text, cursor)
663 })
664 .unwrap_or(false);
665
666 if handled {
667 self.mark_dirty();
668 self.layout_dirty = true;
670 }
671
672 handled
673 }
674
675 pub fn on_ime_delete_surrounding(&mut self, before_bytes: usize, after_bytes: usize) -> bool {
678 let handled = run_in_mutable_snapshot(|| {
679 cranpose_ui::text_field_focus::dispatch_delete_surrounding(before_bytes, after_bytes)
680 })
681 .unwrap_or(false);
682
683 if handled {
684 self.mark_dirty();
685 self.layout_dirty = true;
686 }
687
688 handled
689 }
690
691 pub fn log_debug_info(&mut self) {
692 println!("\n\n");
693 println!("════════════════════════════════════════════════════════");
694 println!(" DEBUG: CURRENT SCREEN STATE");
695 println!("════════════════════════════════════════════════════════");
696
697 if let Some(ref layout_tree) = self.layout_tree {
698 log_layout_tree(layout_tree);
699 let renderer = HeadlessRenderer::new();
700 let render_scene = renderer.render(layout_tree);
701 log_render_scene(&render_scene);
702 log_screen_summary(layout_tree, &render_scene);
703 } else {
704 println!("No layout available");
705 }
706
707 println!("════════════════════════════════════════════════════════");
708 println!("\n\n");
709 }
710
711 pub fn layout_tree(&self) -> Option<&LayoutTree> {
713 self.layout_tree.as_ref()
714 }
715
716 pub fn semantics_tree(&self) -> Option<&SemanticsTree> {
718 self.semantics_tree.as_ref()
719 }
720
721 fn process_frame(&mut self) {
722 fps_monitor::record_frame();
724
725 #[cfg(debug_assertions)]
726 let _frame_start = Instant::now();
727
728 self.run_layout_phase();
729
730 #[cfg(debug_assertions)]
731 let _after_layout = Instant::now();
732
733 self.run_dispatch_queues();
734
735 #[cfg(debug_assertions)]
736 let _after_dispatch = Instant::now();
737
738 self.run_render_phase();
739 }
740
741 fn run_layout_phase(&mut self) {
742 let repass_nodes = cranpose_ui::take_layout_repass_nodes();
749 let had_repass_nodes = !repass_nodes.is_empty();
750 if had_repass_nodes {
751 let root = self.composition.root();
752 let mut applier = self.composition.applier_mut();
753 for node_id in repass_nodes {
754 cranpose_core::bubble_measure_dirty(
757 &mut *applier as &mut dyn cranpose_core::Applier,
758 node_id,
759 );
760 cranpose_core::bubble_layout_dirty(
761 &mut *applier as &mut dyn cranpose_core::Applier,
762 node_id,
763 );
764 }
765
766 if let Some(root) = root {
771 if let Ok(node) = applier.get_mut(root) {
772 node.mark_needs_measure();
773 }
774 }
775
776 drop(applier);
777 self.layout_dirty = true;
778 }
779
780 let invalidation_requested = take_layout_invalidation();
798
799 if invalidation_requested && !had_repass_nodes {
806 cranpose_ui::layout::invalidate_all_layout_caches();
809
810 if let Some(root) = self.composition.root() {
813 let mut applier = self.composition.applier_mut();
814 if let Ok(node) = applier.get_mut(root) {
815 if let Some(layout_node) =
816 node.as_any_mut().downcast_mut::<cranpose_ui::LayoutNode>()
817 {
818 layout_node.mark_needs_measure();
819 layout_node.mark_needs_layout();
820 }
821 }
822 }
823 self.layout_dirty = true;
824 } else if invalidation_requested {
825 self.layout_dirty = true;
828 }
829
830 if !self.layout_dirty {
832 return;
833 }
834
835 let viewport_size = Size {
836 width: self.viewport.0,
837 height: self.viewport.1,
838 };
839 if let Some(root) = self.composition.root() {
840 let handle = self.composition.runtime_handle();
841 let mut applier = self.composition.applier_mut();
842 applier.set_runtime_handle(handle);
843
844 let tree_needs_layout_check = cranpose_ui::tree_needs_layout(&mut *applier, root)
847 .unwrap_or_else(|err| {
848 log::warn!(
849 "Cannot check layout dirty status for root #{}: {}",
850 root,
851 err
852 );
853 true });
855
856 let needs_layout = tree_needs_layout_check || self.layout_dirty;
860
861 if !needs_layout {
862 log::trace!("Skipping layout: tree is clean");
864 self.layout_dirty = false;
865 applier.clear_runtime_handle();
866 return;
867 }
868
869 self.layout_dirty = false;
871
872 match cranpose_ui::measure_layout(&mut applier, root, viewport_size) {
874 Ok(measurements) => {
875 self.semantics_tree = Some(measurements.semantics_tree().clone());
876 self.layout_tree = Some(measurements.into_layout_tree());
877 self.scene_dirty = true;
878 }
879 Err(err) => {
880 log::error!("failed to compute layout: {err}");
881 self.layout_tree = None;
882 self.semantics_tree = None;
883 self.scene_dirty = true;
884 }
885 }
886 applier.clear_runtime_handle();
887 } else {
888 self.layout_tree = None;
889 self.semantics_tree = None;
890 self.scene_dirty = true;
891 self.layout_dirty = false;
892 }
893 }
894
895 fn run_dispatch_queues(&mut self) {
896 if has_pending_pointer_repasses() {
900 let mut applier = self.composition.applier_mut();
901 process_pointer_repasses(|node_id| {
902 let result = applier.with_node::<LayoutNode, _>(node_id, |layout_node| {
904 if layout_node.needs_pointer_pass() {
905 layout_node.clear_needs_pointer_pass();
906 log::trace!("Cleared pointer repass flag for node #{}", node_id);
907 }
908 });
909 if let Err(err) = result {
910 log::debug!(
911 "Could not process pointer repass for node #{}: {}",
912 node_id,
913 err
914 );
915 }
916 });
917 }
918
919 if has_pending_focus_invalidations() {
923 let mut applier = self.composition.applier_mut();
924 process_focus_invalidations(|node_id| {
925 let result = applier.with_node::<LayoutNode, _>(node_id, |layout_node| {
927 if layout_node.needs_focus_sync() {
928 layout_node.clear_needs_focus_sync();
929 log::trace!("Cleared focus sync flag for node #{}", node_id);
930 }
931 });
932 if let Err(err) = result {
933 log::debug!(
934 "Could not process focus invalidation for node #{}: {}",
935 node_id,
936 err
937 );
938 }
939 });
940 }
941 }
942
943 fn refresh_draw_repasses(&mut self) {
944 let dirty_nodes = take_draw_repass_nodes();
945 if dirty_nodes.is_empty() {
946 return;
947 }
948
949 let Some(layout_tree) = self.layout_tree.as_mut() else {
950 return;
951 };
952
953 let dirty_set: HashSet<NodeId> = dirty_nodes.into_iter().collect();
954 let mut applier = self.composition.applier_mut();
955 refresh_layout_box_data(&mut applier, layout_tree.root_mut(), &dirty_set);
956 }
957
958 fn run_render_phase(&mut self) {
959 let render_dirty = take_render_invalidation();
960 let pointer_dirty = take_pointer_invalidation();
961 let focus_dirty = take_focus_invalidation();
962 let cursor_blink_dirty = cranpose_ui::tick_cursor_blink();
964 if render_dirty || pointer_dirty || focus_dirty || cursor_blink_dirty {
965 self.scene_dirty = true;
966 }
967 if !self.scene_dirty {
968 return;
969 }
970 self.scene_dirty = false;
971 self.refresh_draw_repasses();
972 let viewport_size = Size {
973 width: self.viewport.0,
974 height: self.viewport.1,
975 };
976
977 if let Some(root) = self.composition.root() {
979 let mut applier = self.composition.applier_mut();
980 if let Err(err) =
981 self.renderer
982 .rebuild_scene_from_applier(&mut applier, root, viewport_size)
983 {
984 log::error!("renderer rebuild failed: {err:?}");
986 self.renderer.scene_mut().clear();
987 }
988 } else {
989 self.renderer.scene_mut().clear();
990 }
991
992 if self.dev_options.fps_counter {
994 let stats = fps_monitor::fps_stats();
995 let text = format!(
996 "{:.0} FPS | {:.1}ms | {} recomp/s",
997 stats.fps, stats.avg_ms, stats.recomps_per_second
998 );
999 self.renderer.draw_dev_overlay(&text, viewport_size);
1000 }
1001 }
1002}
1003
1004fn refresh_layout_box_data(
1005 applier: &mut MemoryApplier,
1006 layout: &mut cranpose_ui::layout::LayoutBox,
1007 dirty_nodes: &HashSet<NodeId>,
1008) {
1009 if dirty_nodes.contains(&layout.node_id) {
1010 if let Ok((modifier, resolved_modifiers, slices)) =
1011 applier.with_node::<LayoutNode, _>(layout.node_id, |node| {
1012 node.clear_needs_redraw();
1013 (
1014 node.modifier.clone(),
1015 node.resolved_modifiers(),
1016 node.modifier_slices_snapshot(),
1017 )
1018 })
1019 {
1020 layout.node_data.modifier = modifier;
1021 layout.node_data.resolved_modifiers = resolved_modifiers;
1022 layout.node_data.modifier_slices = slices;
1023 } else if let Ok((modifier, resolved_modifiers)) = applier
1024 .with_node::<SubcomposeLayoutNode, _>(layout.node_id, |node| {
1025 node.clear_needs_redraw();
1026 (node.modifier(), node.resolved_modifiers())
1027 })
1028 {
1029 layout.node_data.modifier = modifier.clone();
1030 layout.node_data.resolved_modifiers = resolved_modifiers;
1031 layout.node_data.modifier_slices =
1032 std::rc::Rc::new(cranpose_ui::collect_slices_from_modifier(&modifier));
1033 }
1034 }
1035
1036 for child in &mut layout.children {
1037 refresh_layout_box_data(applier, child, dirty_nodes);
1038 }
1039}
1040
1041impl<R> Drop for AppShell<R>
1042where
1043 R: Renderer,
1044{
1045 fn drop(&mut self) {
1046 self.runtime.clear_frame_waker();
1047 }
1048}
1049
1050pub fn default_root_key() -> Key {
1051 location_key(file!(), line!(), column!())
1052}
1053
1054#[cfg(test)]
1055#[path = "tests/app_shell_tests.rs"]
1056mod tests;