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 enter_event_handler();
277 let result = run_in_mutable_snapshot(|| self.set_cursor_inner(x, y)).unwrap_or(false);
278 exit_event_handler();
279 result
280 }
281
282 fn set_cursor_inner(&mut self, x: f32, y: f32) -> bool {
283 self.cursor = (x, y);
284
285 if self.buttons_pressed != PointerButtons::NONE {
289 if self.hit_path_tracker.has_path(PointerId::PRIMARY) {
290 let targets = self.resolve_hit_path(PointerId::PRIMARY);
292
293 if !targets.is_empty() {
294 let event =
295 PointerEvent::new(PointerEventKind::Move, Point { x, y }, Point { x, y })
296 .with_buttons(self.buttons_pressed);
297
298 for hit in targets {
299 hit.dispatch(event.clone());
300 if event.is_consumed() {
301 break;
302 }
303 }
304 self.mark_dirty();
305 return true;
306 }
307
308 let hits = self.renderer.scene().hit_test(x, y);
311 if !hits.is_empty() {
312 let node_ids: Vec<_> = hits.iter().map(|h| h.node_id()).collect();
313 self.hit_path_tracker
314 .add_hit_path(PointerId::PRIMARY, node_ids);
315 let event =
316 PointerEvent::new(PointerEventKind::Move, Point { x, y }, Point { x, y })
317 .with_buttons(self.buttons_pressed);
318 for hit in hits {
319 hit.dispatch(event.clone());
320 if event.is_consumed() {
321 break;
322 }
323 }
324 self.mark_dirty();
325 return true;
326 }
327 return false;
328 }
329
330 return false;
333 }
334
335 let hits = self.renderer.scene().hit_test(x, y);
337 if !hits.is_empty() {
338 let event = PointerEvent::new(PointerEventKind::Move, Point { x, y }, Point { x, y })
339 .with_buttons(self.buttons_pressed); for hit in hits {
341 hit.dispatch(event.clone());
342 if event.is_consumed() {
343 break;
344 }
345 }
346 self.mark_dirty();
347 true
348 } else {
349 false
350 }
351 }
352
353 pub fn pointer_pressed(&mut self) -> bool {
354 enter_event_handler();
355 let result = run_in_mutable_snapshot(|| self.pointer_pressed_inner()).unwrap_or(false);
356 exit_event_handler();
357 result
358 }
359
360 fn pointer_pressed_inner(&mut self) -> bool {
361 self.buttons_pressed.insert(PointerButton::Primary);
363
364 let hits = self.renderer.scene().hit_test(self.cursor.0, self.cursor.1);
372
373 let node_ids: Vec<_> = hits.iter().map(|h| h.node_id()).collect();
375 self.hit_path_tracker
376 .add_hit_path(PointerId::PRIMARY, node_ids);
377
378 if !hits.is_empty() {
379 let event = PointerEvent::new(
380 PointerEventKind::Down,
381 Point {
382 x: self.cursor.0,
383 y: self.cursor.1,
384 },
385 Point {
386 x: self.cursor.0,
387 y: self.cursor.1,
388 },
389 )
390 .with_buttons(self.buttons_pressed);
391
392 for hit in hits {
394 hit.dispatch(event.clone());
395 if event.is_consumed() {
396 break;
397 }
398 }
399 self.mark_dirty();
400 true
401 } else {
402 false
403 }
404 }
405
406 pub fn pointer_released(&mut self) -> bool {
407 enter_event_handler();
408 let result = run_in_mutable_snapshot(|| self.pointer_released_inner()).unwrap_or(false);
409 exit_event_handler();
410 result
411 }
412
413 fn pointer_released_inner(&mut self) -> bool {
414 self.buttons_pressed.remove(PointerButton::Primary);
417 let corrected_buttons = self.buttons_pressed;
418
419 let targets = self.resolve_hit_path(PointerId::PRIMARY);
421
422 self.hit_path_tracker.remove_path(PointerId::PRIMARY);
424
425 if !targets.is_empty() {
426 let event = PointerEvent::new(
427 PointerEventKind::Up,
428 Point {
429 x: self.cursor.0,
430 y: self.cursor.1,
431 },
432 Point {
433 x: self.cursor.0,
434 y: self.cursor.1,
435 },
436 )
437 .with_buttons(corrected_buttons);
438
439 for hit in targets {
440 hit.dispatch(event.clone());
441 if event.is_consumed() {
442 break;
443 }
444 }
445 self.mark_dirty();
446 true
447 } else {
448 false
449 }
450 }
451
452 pub fn cancel_gesture(&mut self) {
458 enter_event_handler();
459 let _ = run_in_mutable_snapshot(|| {
460 self.cancel_gesture_inner();
461 });
462 exit_event_handler();
463 }
464
465 fn cancel_gesture_inner(&mut self) {
466 let targets = self.resolve_hit_path(PointerId::PRIMARY);
468
469 self.hit_path_tracker.clear();
471 self.buttons_pressed = PointerButtons::NONE;
472
473 if !targets.is_empty() {
474 let event = PointerEvent::new(
475 PointerEventKind::Cancel,
476 Point {
477 x: self.cursor.0,
478 y: self.cursor.1,
479 },
480 Point {
481 x: self.cursor.0,
482 y: self.cursor.1,
483 },
484 );
485
486 for hit in targets {
487 hit.dispatch(event.clone());
488 }
489 self.mark_dirty();
490 }
491 }
492 pub fn on_key_event(&mut self, event: &KeyEvent) -> bool {
499 enter_event_handler();
500 let result = self.on_key_event_inner(event);
501 exit_event_handler();
502 result
503 }
504
505 fn on_key_event_inner(&mut self, event: &KeyEvent) -> bool {
507 use KeyEventType::KeyDown;
508
509 if event.event_type == KeyDown && event.modifiers.command_or_ctrl() {
511 #[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))]
514 {
515 match event.key_code {
516 KeyCode::C => {
518 let text = self.on_copy();
520 if let (Some(text), Some(clipboard)) = (text, self.clipboard.as_mut()) {
521 let _ = clipboard.set_text(&text);
522 return true;
523 }
524 }
525 KeyCode::X => {
527 let text = self.on_cut();
529 if let (Some(text), Some(clipboard)) = (text, self.clipboard.as_mut()) {
530 let _ = clipboard.set_text(&text);
531 self.mark_dirty();
532 self.layout_dirty = true;
533 return true;
534 }
535 }
536 KeyCode::V => {
538 let text = self.clipboard.as_mut().and_then(|cb| cb.get_text().ok());
540 if let Some(text) = text {
541 if self.on_paste(&text) {
542 return true;
543 }
544 }
545 }
546 _ => {}
547 }
548 }
549 }
550
551 if !cranpose_ui::text_field_focus::has_focused_field() {
553 return false;
554 }
555
556 let handled = run_in_mutable_snapshot(|| {
560 cranpose_ui::text_field_focus::dispatch_key_event(event)
563 })
564 .unwrap_or(false);
565
566 if handled {
567 self.mark_dirty();
569 self.layout_dirty = true;
570 }
571
572 handled
573 }
574
575 pub fn on_paste(&mut self, text: &str) -> bool {
579 let handled =
583 run_in_mutable_snapshot(|| cranpose_ui::text_field_focus::dispatch_paste(text))
584 .unwrap_or(false);
585
586 if handled {
587 self.mark_dirty();
588 self.layout_dirty = true;
589 }
590
591 handled
592 }
593
594 pub fn on_copy(&mut self) -> Option<String> {
598 cranpose_ui::text_field_focus::dispatch_copy()
600 }
601
602 pub fn on_cut(&mut self) -> Option<String> {
606 let text = cranpose_ui::text_field_focus::dispatch_cut();
608
609 if text.is_some() {
610 self.mark_dirty();
611 self.layout_dirty = true;
612 }
613
614 text
615 }
616
617 #[cfg(all(target_os = "linux", not(target_arch = "wasm32")))]
621 pub fn set_primary_selection(&mut self, text: &str) {
622 use arboard::{LinuxClipboardKind, SetExtLinux};
623 if let Some(ref mut clipboard) = self.clipboard {
624 let result = clipboard
625 .set()
626 .clipboard(LinuxClipboardKind::Primary)
627 .text(text.to_string());
628 if let Err(e) = result {
629 log::debug!("Primary selection set failed: {:?}", e);
631 }
632 }
633 }
634
635 #[cfg(all(target_os = "linux", not(target_arch = "wasm32")))]
638 pub fn get_primary_selection(&mut self) -> Option<String> {
639 use arboard::{GetExtLinux, LinuxClipboardKind};
640 if let Some(ref mut clipboard) = self.clipboard {
641 clipboard
642 .get()
643 .clipboard(LinuxClipboardKind::Primary)
644 .text()
645 .ok()
646 } else {
647 None
648 }
649 }
650
651 #[cfg(all(not(target_os = "linux"), not(target_arch = "wasm32")))]
652 pub fn get_primary_selection(&mut self) -> Option<String> {
653 None
654 }
655
656 pub fn sync_selection_to_primary(&mut self) {
659 #[cfg(all(target_os = "linux", not(target_arch = "wasm32")))]
660 {
661 if let Some(text) = self.on_copy() {
662 self.set_primary_selection(&text);
663 }
664 }
665 }
666
667 pub fn on_ime_preedit(&mut self, text: &str, cursor: Option<(usize, usize)>) -> bool {
675 let handled = run_in_mutable_snapshot(|| {
677 cranpose_ui::text_field_focus::dispatch_ime_preedit(text, cursor)
678 })
679 .unwrap_or(false);
680
681 if handled {
682 self.mark_dirty();
683 self.layout_dirty = true;
685 }
686
687 handled
688 }
689
690 pub fn on_ime_delete_surrounding(&mut self, before_bytes: usize, after_bytes: usize) -> bool {
693 let handled = run_in_mutable_snapshot(|| {
694 cranpose_ui::text_field_focus::dispatch_delete_surrounding(before_bytes, after_bytes)
695 })
696 .unwrap_or(false);
697
698 if handled {
699 self.mark_dirty();
700 self.layout_dirty = true;
701 }
702
703 handled
704 }
705
706 pub fn log_debug_info(&mut self) {
707 println!("\n\n");
708 println!("════════════════════════════════════════════════════════");
709 println!(" DEBUG: CURRENT SCREEN STATE");
710 println!("════════════════════════════════════════════════════════");
711
712 if let Some(ref layout_tree) = self.layout_tree {
713 log_layout_tree(layout_tree);
714 let renderer = HeadlessRenderer::new();
715 let render_scene = renderer.render(layout_tree);
716 log_render_scene(&render_scene);
717 log_screen_summary(layout_tree, &render_scene);
718 } else {
719 println!("No layout available");
720 }
721
722 println!("════════════════════════════════════════════════════════");
723 println!("\n\n");
724 }
725
726 pub fn layout_tree(&self) -> Option<&LayoutTree> {
728 self.layout_tree.as_ref()
729 }
730
731 pub fn semantics_tree(&self) -> Option<&SemanticsTree> {
733 self.semantics_tree.as_ref()
734 }
735
736 fn process_frame(&mut self) {
737 fps_monitor::record_frame();
739
740 #[cfg(debug_assertions)]
741 let _frame_start = Instant::now();
742
743 self.run_layout_phase();
744
745 #[cfg(debug_assertions)]
746 let _after_layout = Instant::now();
747
748 self.run_dispatch_queues();
749
750 #[cfg(debug_assertions)]
751 let _after_dispatch = Instant::now();
752
753 self.run_render_phase();
754 }
755
756 fn run_layout_phase(&mut self) {
757 let repass_nodes = cranpose_ui::take_layout_repass_nodes();
764 let had_repass_nodes = !repass_nodes.is_empty();
765 if had_repass_nodes {
766 let root = self.composition.root();
767 let mut applier = self.composition.applier_mut();
768 for node_id in repass_nodes {
769 cranpose_core::bubble_measure_dirty(
772 &mut *applier as &mut dyn cranpose_core::Applier,
773 node_id,
774 );
775 cranpose_core::bubble_layout_dirty(
776 &mut *applier as &mut dyn cranpose_core::Applier,
777 node_id,
778 );
779 }
780
781 if let Some(root) = root {
786 if let Ok(node) = applier.get_mut(root) {
787 node.mark_needs_measure();
788 }
789 }
790
791 drop(applier);
792 self.layout_dirty = true;
793 }
794
795 let invalidation_requested = take_layout_invalidation();
813
814 if invalidation_requested && !had_repass_nodes {
821 cranpose_ui::layout::invalidate_all_layout_caches();
824
825 if let Some(root) = self.composition.root() {
828 let mut applier = self.composition.applier_mut();
829 if let Ok(node) = applier.get_mut(root) {
830 if let Some(layout_node) =
831 node.as_any_mut().downcast_mut::<cranpose_ui::LayoutNode>()
832 {
833 layout_node.mark_needs_measure();
834 layout_node.mark_needs_layout();
835 }
836 }
837 }
838 self.layout_dirty = true;
839 } else if invalidation_requested {
840 self.layout_dirty = true;
843 }
844
845 if !self.layout_dirty {
847 return;
848 }
849
850 let viewport_size = Size {
851 width: self.viewport.0,
852 height: self.viewport.1,
853 };
854 if let Some(root) = self.composition.root() {
855 let handle = self.composition.runtime_handle();
856 let mut applier = self.composition.applier_mut();
857 applier.set_runtime_handle(handle);
858
859 let tree_needs_layout_check = cranpose_ui::tree_needs_layout(&mut *applier, root)
862 .unwrap_or_else(|err| {
863 log::warn!(
864 "Cannot check layout dirty status for root #{}: {}",
865 root,
866 err
867 );
868 true });
870
871 let needs_layout = tree_needs_layout_check || self.layout_dirty;
875
876 if !needs_layout {
877 log::trace!("Skipping layout: tree is clean");
879 self.layout_dirty = false;
880 applier.clear_runtime_handle();
881 return;
882 }
883
884 self.layout_dirty = false;
886
887 match cranpose_ui::measure_layout(&mut applier, root, viewport_size) {
889 Ok(measurements) => {
890 self.semantics_tree = Some(measurements.semantics_tree().clone());
891 self.layout_tree = Some(measurements.into_layout_tree());
892 self.scene_dirty = true;
893 }
894 Err(err) => {
895 log::error!("failed to compute layout: {err}");
896 self.layout_tree = None;
897 self.semantics_tree = None;
898 self.scene_dirty = true;
899 }
900 }
901 applier.clear_runtime_handle();
902 } else {
903 self.layout_tree = None;
904 self.semantics_tree = None;
905 self.scene_dirty = true;
906 self.layout_dirty = false;
907 }
908 }
909
910 fn run_dispatch_queues(&mut self) {
911 if has_pending_pointer_repasses() {
915 let mut applier = self.composition.applier_mut();
916 process_pointer_repasses(|node_id| {
917 let result = applier.with_node::<LayoutNode, _>(node_id, |layout_node| {
919 if layout_node.needs_pointer_pass() {
920 layout_node.clear_needs_pointer_pass();
921 log::trace!("Cleared pointer repass flag for node #{}", node_id);
922 }
923 });
924 if let Err(err) = result {
925 log::debug!(
926 "Could not process pointer repass for node #{}: {}",
927 node_id,
928 err
929 );
930 }
931 });
932 }
933
934 if has_pending_focus_invalidations() {
938 let mut applier = self.composition.applier_mut();
939 process_focus_invalidations(|node_id| {
940 let result = applier.with_node::<LayoutNode, _>(node_id, |layout_node| {
942 if layout_node.needs_focus_sync() {
943 layout_node.clear_needs_focus_sync();
944 log::trace!("Cleared focus sync flag for node #{}", node_id);
945 }
946 });
947 if let Err(err) = result {
948 log::debug!(
949 "Could not process focus invalidation for node #{}: {}",
950 node_id,
951 err
952 );
953 }
954 });
955 }
956 }
957
958 fn refresh_draw_repasses(&mut self) {
959 let dirty_nodes = take_draw_repass_nodes();
960 if dirty_nodes.is_empty() {
961 return;
962 }
963
964 let Some(layout_tree) = self.layout_tree.as_mut() else {
965 return;
966 };
967
968 let dirty_set: HashSet<NodeId> = dirty_nodes.into_iter().collect();
969 let mut applier = self.composition.applier_mut();
970 refresh_layout_box_data(&mut applier, layout_tree.root_mut(), &dirty_set);
971 }
972
973 fn run_render_phase(&mut self) {
974 let render_dirty = take_render_invalidation();
975 let pointer_dirty = take_pointer_invalidation();
976 let focus_dirty = take_focus_invalidation();
977 let draw_repass_pending = cranpose_ui::has_pending_draw_repasses();
978 let cursor_blink_dirty = cranpose_ui::tick_cursor_blink();
980 if render_dirty || pointer_dirty || focus_dirty || cursor_blink_dirty || draw_repass_pending
981 {
982 self.scene_dirty = true;
983 }
984 if !self.scene_dirty {
985 return;
986 }
987 self.scene_dirty = false;
988 self.refresh_draw_repasses();
989 let viewport_size = Size {
990 width: self.viewport.0,
991 height: self.viewport.1,
992 };
993
994 if let Some(root) = self.composition.root() {
996 let mut applier = self.composition.applier_mut();
997 if let Err(err) =
998 self.renderer
999 .rebuild_scene_from_applier(&mut applier, root, viewport_size)
1000 {
1001 log::error!("renderer rebuild failed: {err:?}");
1003 self.renderer.scene_mut().clear();
1004 }
1005 } else {
1006 self.renderer.scene_mut().clear();
1007 }
1008
1009 if self.dev_options.fps_counter {
1011 let stats = fps_monitor::fps_stats();
1012 let text = format!(
1013 "{:.0} FPS | {:.1}ms | {} recomp/s",
1014 stats.fps, stats.avg_ms, stats.recomps_per_second
1015 );
1016 self.renderer.draw_dev_overlay(&text, viewport_size);
1017 }
1018 }
1019}
1020
1021fn refresh_layout_box_data(
1022 applier: &mut MemoryApplier,
1023 layout: &mut cranpose_ui::layout::LayoutBox,
1024 dirty_nodes: &HashSet<NodeId>,
1025) {
1026 if dirty_nodes.contains(&layout.node_id) {
1027 if let Ok((modifier, resolved_modifiers, slices)) =
1028 applier.with_node::<LayoutNode, _>(layout.node_id, |node| {
1029 node.clear_needs_redraw();
1030 (
1031 node.modifier.clone(),
1032 node.resolved_modifiers(),
1033 node.modifier_slices_snapshot(),
1034 )
1035 })
1036 {
1037 layout.node_data.modifier = modifier;
1038 layout.node_data.resolved_modifiers = resolved_modifiers;
1039 layout.node_data.modifier_slices = slices;
1040 } else if let Ok((modifier, resolved_modifiers)) = applier
1041 .with_node::<SubcomposeLayoutNode, _>(layout.node_id, |node| {
1042 node.clear_needs_redraw();
1043 (node.modifier(), node.resolved_modifiers())
1044 })
1045 {
1046 layout.node_data.modifier = modifier.clone();
1047 layout.node_data.resolved_modifiers = resolved_modifiers;
1048 layout.node_data.modifier_slices =
1049 std::rc::Rc::new(cranpose_ui::collect_slices_from_modifier(&modifier));
1050 }
1051 }
1052
1053 for child in &mut layout.children {
1054 refresh_layout_box_data(applier, child, dirty_nodes);
1055 }
1056}
1057
1058impl<R> Drop for AppShell<R>
1059where
1060 R: Renderer,
1061{
1062 fn drop(&mut self) {
1063 self.runtime.clear_frame_waker();
1064 }
1065}
1066
1067pub fn default_root_key() -> Key {
1068 location_key(file!(), line!(), column!())
1069}
1070
1071#[cfg(test)]
1072#[path = "tests/app_shell_tests.rs"]
1073mod tests;