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 pub fn sync_selection_to_primary(&mut self) {
639 #[cfg(all(target_os = "linux", not(target_arch = "wasm32")))]
640 {
641 if let Some(text) = self.on_copy() {
642 self.set_primary_selection(&text);
643 }
644 }
645 }
646
647 pub fn on_ime_preedit(&mut self, text: &str, cursor: Option<(usize, usize)>) -> bool {
655 let handled = run_in_mutable_snapshot(|| {
657 cranpose_ui::text_field_focus::dispatch_ime_preedit(text, cursor)
658 })
659 .unwrap_or(false);
660
661 if handled {
662 self.mark_dirty();
663 self.layout_dirty = true;
665 }
666
667 handled
668 }
669
670 pub fn on_ime_delete_surrounding(&mut self, before_bytes: usize, after_bytes: usize) -> bool {
673 let handled = run_in_mutable_snapshot(|| {
674 cranpose_ui::text_field_focus::dispatch_delete_surrounding(before_bytes, after_bytes)
675 })
676 .unwrap_or(false);
677
678 if handled {
679 self.mark_dirty();
680 self.layout_dirty = true;
681 }
682
683 handled
684 }
685
686 pub fn log_debug_info(&mut self) {
687 println!("\n\n");
688 println!("════════════════════════════════════════════════════════");
689 println!(" DEBUG: CURRENT SCREEN STATE");
690 println!("════════════════════════════════════════════════════════");
691
692 if let Some(ref layout_tree) = self.layout_tree {
693 log_layout_tree(layout_tree);
694 let renderer = HeadlessRenderer::new();
695 let render_scene = renderer.render(layout_tree);
696 log_render_scene(&render_scene);
697 log_screen_summary(layout_tree, &render_scene);
698 } else {
699 println!("No layout available");
700 }
701
702 println!("════════════════════════════════════════════════════════");
703 println!("\n\n");
704 }
705
706 pub fn layout_tree(&self) -> Option<&LayoutTree> {
708 self.layout_tree.as_ref()
709 }
710
711 pub fn semantics_tree(&self) -> Option<&SemanticsTree> {
713 self.semantics_tree.as_ref()
714 }
715
716 fn process_frame(&mut self) {
717 fps_monitor::record_frame();
719
720 #[cfg(debug_assertions)]
721 let _frame_start = Instant::now();
722
723 self.run_layout_phase();
724
725 #[cfg(debug_assertions)]
726 let _after_layout = Instant::now();
727
728 self.run_dispatch_queues();
729
730 #[cfg(debug_assertions)]
731 let _after_dispatch = Instant::now();
732
733 self.run_render_phase();
734 }
735
736 fn run_layout_phase(&mut self) {
737 let repass_nodes = cranpose_ui::take_layout_repass_nodes();
744 let had_repass_nodes = !repass_nodes.is_empty();
745 if had_repass_nodes {
746 let root = self.composition.root();
747 let mut applier = self.composition.applier_mut();
748 for node_id in repass_nodes {
749 cranpose_core::bubble_measure_dirty(
752 &mut *applier as &mut dyn cranpose_core::Applier,
753 node_id,
754 );
755 cranpose_core::bubble_layout_dirty(
756 &mut *applier as &mut dyn cranpose_core::Applier,
757 node_id,
758 );
759 }
760
761 if let Some(root) = root {
766 if let Ok(node) = applier.get_mut(root) {
767 node.mark_needs_measure();
768 }
769 }
770
771 drop(applier);
772 self.layout_dirty = true;
773 }
774
775 let invalidation_requested = take_layout_invalidation();
793
794 if invalidation_requested && !had_repass_nodes {
801 cranpose_ui::layout::invalidate_all_layout_caches();
804
805 if let Some(root) = self.composition.root() {
808 let mut applier = self.composition.applier_mut();
809 if let Ok(node) = applier.get_mut(root) {
810 if let Some(layout_node) =
811 node.as_any_mut().downcast_mut::<cranpose_ui::LayoutNode>()
812 {
813 layout_node.mark_needs_measure();
814 layout_node.mark_needs_layout();
815 }
816 }
817 }
818 self.layout_dirty = true;
819 } else if invalidation_requested {
820 self.layout_dirty = true;
823 }
824
825 if !self.layout_dirty {
827 return;
828 }
829
830 let viewport_size = Size {
831 width: self.viewport.0,
832 height: self.viewport.1,
833 };
834 if let Some(root) = self.composition.root() {
835 let handle = self.composition.runtime_handle();
836 let mut applier = self.composition.applier_mut();
837 applier.set_runtime_handle(handle);
838
839 let tree_needs_layout_check = cranpose_ui::tree_needs_layout(&mut *applier, root)
842 .unwrap_or_else(|err| {
843 log::warn!(
844 "Cannot check layout dirty status for root #{}: {}",
845 root,
846 err
847 );
848 true });
850
851 let needs_layout = tree_needs_layout_check || self.layout_dirty;
855
856 if !needs_layout {
857 log::trace!("Skipping layout: tree is clean");
859 self.layout_dirty = false;
860 applier.clear_runtime_handle();
861 return;
862 }
863
864 self.layout_dirty = false;
866
867 match cranpose_ui::measure_layout(&mut applier, root, viewport_size) {
869 Ok(measurements) => {
870 self.semantics_tree = Some(measurements.semantics_tree().clone());
871 self.layout_tree = Some(measurements.into_layout_tree());
872 self.scene_dirty = true;
873 }
874 Err(err) => {
875 log::error!("failed to compute layout: {err}");
876 self.layout_tree = None;
877 self.semantics_tree = None;
878 self.scene_dirty = true;
879 }
880 }
881 applier.clear_runtime_handle();
882 } else {
883 self.layout_tree = None;
884 self.semantics_tree = None;
885 self.scene_dirty = true;
886 self.layout_dirty = false;
887 }
888 }
889
890 fn run_dispatch_queues(&mut self) {
891 if has_pending_pointer_repasses() {
895 let mut applier = self.composition.applier_mut();
896 process_pointer_repasses(|node_id| {
897 let result = applier.with_node::<LayoutNode, _>(node_id, |layout_node| {
899 if layout_node.needs_pointer_pass() {
900 layout_node.clear_needs_pointer_pass();
901 log::trace!("Cleared pointer repass flag for node #{}", node_id);
902 }
903 });
904 if let Err(err) = result {
905 log::debug!(
906 "Could not process pointer repass for node #{}: {}",
907 node_id,
908 err
909 );
910 }
911 });
912 }
913
914 if has_pending_focus_invalidations() {
918 let mut applier = self.composition.applier_mut();
919 process_focus_invalidations(|node_id| {
920 let result = applier.with_node::<LayoutNode, _>(node_id, |layout_node| {
922 if layout_node.needs_focus_sync() {
923 layout_node.clear_needs_focus_sync();
924 log::trace!("Cleared focus sync flag for node #{}", node_id);
925 }
926 });
927 if let Err(err) = result {
928 log::debug!(
929 "Could not process focus invalidation for node #{}: {}",
930 node_id,
931 err
932 );
933 }
934 });
935 }
936 }
937
938 fn refresh_draw_repasses(&mut self) {
939 let dirty_nodes = take_draw_repass_nodes();
940 if dirty_nodes.is_empty() {
941 return;
942 }
943
944 let Some(layout_tree) = self.layout_tree.as_mut() else {
945 return;
946 };
947
948 let dirty_set: HashSet<NodeId> = dirty_nodes.into_iter().collect();
949 let mut applier = self.composition.applier_mut();
950 refresh_layout_box_data(&mut applier, layout_tree.root_mut(), &dirty_set);
951 }
952
953 fn run_render_phase(&mut self) {
954 let render_dirty = take_render_invalidation();
955 let pointer_dirty = take_pointer_invalidation();
956 let focus_dirty = take_focus_invalidation();
957 let cursor_blink_dirty = cranpose_ui::tick_cursor_blink();
959 if render_dirty || pointer_dirty || focus_dirty || cursor_blink_dirty {
960 self.scene_dirty = true;
961 }
962 if !self.scene_dirty {
963 return;
964 }
965 self.scene_dirty = false;
966 self.refresh_draw_repasses();
967 let viewport_size = Size {
968 width: self.viewport.0,
969 height: self.viewport.1,
970 };
971 if let Some(layout_tree) = self.layout_tree.as_ref() {
972 if let Err(err) = self.renderer.rebuild_scene(layout_tree, viewport_size) {
973 log::error!("renderer rebuild failed: {err:?}");
974 }
975 } else {
976 self.renderer.scene_mut().clear();
977 }
978
979 if self.dev_options.fps_counter {
981 let stats = fps_monitor::fps_stats();
982 let text = format!(
983 "{:.0} FPS | {:.1}ms | {} recomp/s",
984 stats.fps, stats.avg_ms, stats.recomps_per_second
985 );
986 self.renderer.draw_dev_overlay(&text, viewport_size);
987 }
988 }
989}
990
991fn refresh_layout_box_data(
992 applier: &mut MemoryApplier,
993 layout: &mut cranpose_ui::layout::LayoutBox,
994 dirty_nodes: &HashSet<NodeId>,
995) {
996 if dirty_nodes.contains(&layout.node_id) {
997 if let Ok((modifier, resolved_modifiers, slices)) =
998 applier.with_node::<LayoutNode, _>(layout.node_id, |node| {
999 node.clear_needs_redraw();
1000 (
1001 node.modifier.clone(),
1002 node.resolved_modifiers(),
1003 node.modifier_slices_snapshot(),
1004 )
1005 })
1006 {
1007 layout.node_data.modifier = modifier;
1008 layout.node_data.resolved_modifiers = resolved_modifiers;
1009 layout.node_data.modifier_slices = slices;
1010 } else if let Ok((modifier, resolved_modifiers)) = applier
1011 .with_node::<SubcomposeLayoutNode, _>(layout.node_id, |node| {
1012 node.clear_needs_redraw();
1013 (node.modifier(), node.resolved_modifiers())
1014 })
1015 {
1016 layout.node_data.modifier = modifier.clone();
1017 layout.node_data.resolved_modifiers = resolved_modifiers;
1018 layout.node_data.modifier_slices = cranpose_ui::collect_slices_from_modifier(&modifier);
1019 }
1020 }
1021
1022 for child in &mut layout.children {
1023 refresh_layout_box_data(applier, child, dirty_nodes);
1024 }
1025}
1026
1027impl<R> Drop for AppShell<R>
1028where
1029 R: Renderer,
1030{
1031 fn drop(&mut self) {
1032 self.runtime.clear_frame_waker();
1033 }
1034}
1035
1036pub fn default_root_key() -> Key {
1037 location_key(file!(), line!(), column!())
1038}
1039
1040#[cfg(test)]
1041#[path = "tests/app_shell_tests.rs"]
1042mod tests;