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 std::sync::OnceLock;
13use web_time::Instant;
15
16use cranpose_core::{
17 enter_event_handler, exit_event_handler, location_key, run_in_mutable_snapshot, Applier,
18 Composition, Key, MemoryApplier, NodeError, NodeId,
19};
20use cranpose_foundation::{PointerButton, PointerButtons, PointerEvent, PointerEventKind};
21use cranpose_render_common::{HitTestTarget, RenderScene, Renderer};
22use cranpose_runtime_std::StdRuntime;
23use cranpose_ui::{
24 has_pending_focus_invalidations, has_pending_pointer_repasses, log_layout_tree,
25 log_render_scene, log_screen_summary, peek_focus_invalidation, peek_layout_invalidation,
26 peek_pointer_invalidation, peek_render_invalidation, process_focus_invalidations,
27 process_pointer_repasses, request_render_invalidation, take_draw_repass_nodes,
28 take_focus_invalidation, take_layout_invalidation, take_pointer_invalidation,
29 take_render_invalidation, HeadlessRenderer, LayoutNode, LayoutTree, SemanticsTree,
30 SubcomposeLayoutNode,
31};
32use cranpose_ui_graphics::{Point, Size};
33use hit_path_tracker::{HitPathTracker, PointerId};
34use std::collections::HashSet;
35
36pub use cranpose_ui::{KeyCode, KeyEvent, KeyEventType, Modifiers};
38
39pub struct AppShell<R>
40where
41 R: Renderer,
42{
43 runtime: StdRuntime,
44 composition: Composition<MemoryApplier>,
45 renderer: R,
46 cursor: (f32, f32),
47 viewport: (f32, f32),
48 buffer_size: (u32, u32),
49 start_time: Instant,
50 layout_tree: Option<LayoutTree>,
51 semantics_tree: Option<SemanticsTree>,
52 layout_dirty: bool,
53 scene_dirty: bool,
54 is_dirty: bool,
55 buttons_pressed: PointerButtons,
57 hit_path_tracker: HitPathTracker,
64 #[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))]
66 clipboard: Option<arboard::Clipboard>,
67 dev_options: DevOptions,
69}
70
71#[derive(Clone, Debug, Default)]
76pub struct DevOptions {
77 pub fps_counter: bool,
79 pub recomposition_counter: bool,
81 pub layout_timing: bool,
83}
84
85fn input_pipeline_debug_enabled() -> bool {
86 static ENABLED: OnceLock<bool> = OnceLock::new();
87 *ENABLED.get_or_init(|| std::env::var_os("CRANPOSE_INPUT_DEBUG").is_some())
88}
89
90impl<R> AppShell<R>
91where
92 R: Renderer,
93 R::Error: Debug,
94{
95 pub fn new(mut renderer: R, root_key: Key, content: impl FnMut() + 'static) -> Self {
96 fps_monitor::init_fps_tracker();
98
99 let runtime = StdRuntime::new();
100 let mut composition = Composition::with_runtime(MemoryApplier::new(), runtime.runtime());
101 let build = content;
102 if let Err(err) = composition.render(root_key, build) {
103 log::error!("initial render failed: {err}");
104 }
105 renderer.scene_mut().clear();
106 let mut shell = Self {
107 runtime,
108 composition,
109 renderer,
110 cursor: (0.0, 0.0),
111 viewport: (800.0, 600.0),
112 buffer_size: (800, 600),
113 start_time: Instant::now(),
114 layout_tree: None,
115 semantics_tree: None,
116 layout_dirty: true,
117 scene_dirty: true,
118 is_dirty: true,
119 buttons_pressed: PointerButtons::NONE,
120 hit_path_tracker: HitPathTracker::new(),
121 #[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))]
122 clipboard: arboard::Clipboard::new().ok(),
123 dev_options: DevOptions::default(),
124 };
125 shell.process_frame();
126 shell
127 }
128
129 pub fn set_dev_options(&mut self, options: DevOptions) {
134 self.dev_options = options;
135 }
136
137 pub fn dev_options(&self) -> &DevOptions {
139 &self.dev_options
140 }
141
142 pub fn set_viewport(&mut self, width: f32, height: f32) {
143 self.viewport = (width, height);
144 self.layout_dirty = true;
145 self.mark_dirty();
146 self.process_frame();
147 }
148
149 pub fn set_buffer_size(&mut self, width: u32, height: u32) {
150 self.buffer_size = (width, height);
151 }
152
153 pub fn buffer_size(&self) -> (u32, u32) {
154 self.buffer_size
155 }
156
157 pub fn scene(&self) -> &R::Scene {
158 self.renderer.scene()
159 }
160
161 pub fn renderer(&mut self) -> &mut R {
162 &mut self.renderer
163 }
164
165 #[cfg(not(target_arch = "wasm32"))]
166 pub fn set_frame_waker(&mut self, waker: impl Fn() + Send + Sync + 'static) {
167 self.runtime.set_frame_waker(waker);
168 }
169
170 #[cfg(target_arch = "wasm32")]
171 pub fn set_frame_waker(&mut self, waker: impl Fn() + Send + 'static) {
172 self.runtime.set_frame_waker(waker);
173 }
174
175 pub fn clear_frame_waker(&mut self) {
176 self.runtime.clear_frame_waker();
177 }
178
179 pub fn should_render(&self) -> bool {
180 if self.layout_dirty
181 || self.scene_dirty
182 || peek_render_invalidation()
183 || peek_pointer_invalidation()
184 || peek_focus_invalidation()
185 || peek_layout_invalidation()
186 {
187 return true;
188 }
189 self.runtime.take_frame_request() || self.composition.should_render()
190 }
191
192 pub fn needs_redraw(&self) -> bool {
195 if self.is_dirty
196 || self.layout_dirty
197 || self.scene_dirty
198 || peek_render_invalidation()
199 || peek_pointer_invalidation()
200 || peek_focus_invalidation()
201 || peek_layout_invalidation()
202 || cranpose_ui::has_pending_layout_repasses()
203 || cranpose_ui::has_pending_draw_repasses()
204 || has_pending_pointer_repasses()
205 || has_pending_focus_invalidations()
206 {
207 return true;
208 }
209
210 self.composition.should_render()
211 }
212
213 pub fn mark_dirty(&mut self) {
215 self.is_dirty = true;
216 }
217
218 pub fn has_active_animations(&self) -> bool {
220 self.runtime.take_frame_request() || self.composition.should_render()
221 }
222
223 pub fn next_event_time(&self) -> Option<web_time::Instant> {
226 cranpose_ui::next_cursor_blink_time()
227 }
228
229 fn resolve_hit_path(
236 &self,
237 pointer: PointerId,
238 ) -> Vec<<<R as Renderer>::Scene as RenderScene>::HitTarget> {
239 let Some(node_ids) = self.hit_path_tracker.get_path(pointer) else {
240 return Vec::new();
241 };
242
243 let scene = self.renderer.scene();
244 node_ids
245 .iter()
246 .filter_map(|&id| scene.find_target(id))
247 .collect()
248 }
249
250 pub fn update(&mut self) {
251 let now = Instant::now();
252 let frame_time = now
253 .checked_duration_since(self.start_time)
254 .unwrap_or_default()
255 .as_nanos() as u64;
256 self.runtime.drain_frame_callbacks(frame_time);
257 self.runtime.runtime_handle().drain_ui();
258 let should_render = self.composition.should_render();
259 if input_pipeline_debug_enabled() && should_render {
260 eprintln!(
261 "[CRANPOSE_INPUT_DEBUG] update begin: should_render=true layout_dirty={} scene_dirty={} is_dirty={}",
262 self.layout_dirty, self.scene_dirty, self.is_dirty
263 );
264 }
265 if should_render {
266 match self.composition.process_invalid_scopes() {
267 Ok(changed) => {
268 if input_pipeline_debug_enabled() {
269 eprintln!(
270 "[CRANPOSE_INPUT_DEBUG] process_invalid_scopes changed={}",
271 changed
272 );
273 }
274 if changed {
275 fps_monitor::record_recomposition();
276 self.layout_dirty = true;
277 if let Some(root_id) = self.composition.root() {
280 let _ = self.composition.applier_mut().with_node::<LayoutNode, _>(
281 root_id,
282 |node| {
283 node.mark_needs_measure();
284 },
285 );
286 }
287 request_render_invalidation();
288 }
289 }
290 Err(NodeError::Missing { id }) => {
291 log::debug!("Recomposition skipped: node {} no longer exists", id);
294 self.layout_dirty = true;
295 request_render_invalidation();
296 }
297 Err(err) => {
298 log::error!("recomposition failed: {err}");
299 self.layout_dirty = true;
300 request_render_invalidation();
301 }
302 }
303 }
304 self.process_frame();
305 self.is_dirty = false;
307 }
308
309 pub fn set_cursor(&mut self, x: f32, y: f32) -> bool {
310 enter_event_handler();
311 let result = run_in_mutable_snapshot(|| self.set_cursor_inner(x, y)).unwrap_or(false);
312 exit_event_handler();
313 if input_pipeline_debug_enabled() {
314 eprintln!(
315 "[CRANPOSE_INPUT_DEBUG] set_cursor ({:.2},{:.2}) -> {}",
316 x, y, result
317 );
318 }
319 result
320 }
321
322 fn set_cursor_inner(&mut self, x: f32, y: f32) -> bool {
323 self.cursor = (x, y);
324
325 if self.buttons_pressed != PointerButtons::NONE {
329 if self.hit_path_tracker.has_path(PointerId::PRIMARY) {
330 let targets = self.resolve_hit_path(PointerId::PRIMARY);
332
333 if !targets.is_empty() {
334 let event =
335 PointerEvent::new(PointerEventKind::Move, Point { x, y }, Point { x, y })
336 .with_buttons(self.buttons_pressed);
337
338 for hit in targets {
339 hit.dispatch(event.clone());
340 if event.is_consumed() {
341 break;
342 }
343 }
344 return true;
345 }
346
347 let hits = self.renderer.scene().hit_test(x, y);
350 if !hits.is_empty() {
351 let node_ids: Vec<_> = hits.iter().map(|h| h.node_id()).collect();
352 self.hit_path_tracker
353 .add_hit_path(PointerId::PRIMARY, node_ids);
354 let event =
355 PointerEvent::new(PointerEventKind::Move, Point { x, y }, Point { x, y })
356 .with_buttons(self.buttons_pressed);
357 for hit in hits {
358 hit.dispatch(event.clone());
359 if event.is_consumed() {
360 break;
361 }
362 }
363 return true;
364 }
365 return false;
366 }
367
368 return false;
371 }
372
373 let hits = self.renderer.scene().hit_test(x, y);
375 if !hits.is_empty() {
376 let event = PointerEvent::new(PointerEventKind::Move, Point { x, y }, Point { x, y })
377 .with_buttons(self.buttons_pressed); for hit in hits {
379 hit.dispatch(event.clone());
380 if event.is_consumed() {
381 break;
382 }
383 }
384 true
385 } else {
386 false
387 }
388 }
389
390 pub fn pointer_pressed(&mut self) -> bool {
391 enter_event_handler();
392 let result = run_in_mutable_snapshot(|| self.pointer_pressed_inner()).unwrap_or(false);
393 exit_event_handler();
394 if input_pipeline_debug_enabled() {
395 eprintln!("[CRANPOSE_INPUT_DEBUG] pointer_pressed -> {}", result);
396 }
397 result
398 }
399
400 fn pointer_pressed_inner(&mut self) -> bool {
401 self.buttons_pressed.insert(PointerButton::Primary);
403
404 let hits = self.renderer.scene().hit_test(self.cursor.0, self.cursor.1);
412
413 let node_ids: Vec<_> = hits.iter().map(|h| h.node_id()).collect();
415 self.hit_path_tracker
416 .add_hit_path(PointerId::PRIMARY, node_ids);
417
418 if !hits.is_empty() {
419 let event = PointerEvent::new(
420 PointerEventKind::Down,
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(self.buttons_pressed);
431
432 for hit in hits {
434 hit.dispatch(event.clone());
435 if event.is_consumed() {
436 break;
437 }
438 }
439 true
440 } else {
441 false
442 }
443 }
444
445 pub fn pointer_released(&mut self) -> bool {
446 enter_event_handler();
447 let result = run_in_mutable_snapshot(|| self.pointer_released_inner()).unwrap_or(false);
448 exit_event_handler();
449 if input_pipeline_debug_enabled() {
450 eprintln!("[CRANPOSE_INPUT_DEBUG] pointer_released -> {}", result);
451 }
452 result
453 }
454
455 fn pointer_released_inner(&mut self) -> bool {
456 self.buttons_pressed.remove(PointerButton::Primary);
459 let corrected_buttons = self.buttons_pressed;
460
461 let targets = self.resolve_hit_path(PointerId::PRIMARY);
463
464 self.hit_path_tracker.remove_path(PointerId::PRIMARY);
466
467 if !targets.is_empty() {
468 let event = PointerEvent::new(
469 PointerEventKind::Up,
470 Point {
471 x: self.cursor.0,
472 y: self.cursor.1,
473 },
474 Point {
475 x: self.cursor.0,
476 y: self.cursor.1,
477 },
478 )
479 .with_buttons(corrected_buttons);
480
481 for hit in targets {
482 hit.dispatch(event.clone());
483 if event.is_consumed() {
484 break;
485 }
486 }
487 true
488 } else {
489 false
490 }
491 }
492
493 pub fn cancel_gesture(&mut self) {
499 enter_event_handler();
500 let _ = run_in_mutable_snapshot(|| {
501 self.cancel_gesture_inner();
502 });
503 exit_event_handler();
504 }
505
506 fn cancel_gesture_inner(&mut self) {
507 let targets = self.resolve_hit_path(PointerId::PRIMARY);
509
510 self.hit_path_tracker.clear();
512 self.buttons_pressed = PointerButtons::NONE;
513
514 if !targets.is_empty() {
515 let event = PointerEvent::new(
516 PointerEventKind::Cancel,
517 Point {
518 x: self.cursor.0,
519 y: self.cursor.1,
520 },
521 Point {
522 x: self.cursor.0,
523 y: self.cursor.1,
524 },
525 );
526
527 for hit in targets {
528 hit.dispatch(event.clone());
529 }
530 }
531 }
532 pub fn on_key_event(&mut self, event: &KeyEvent) -> bool {
539 enter_event_handler();
540 let result = self.on_key_event_inner(event);
541 exit_event_handler();
542 result
543 }
544
545 fn on_key_event_inner(&mut self, event: &KeyEvent) -> bool {
547 use KeyEventType::KeyDown;
548
549 if event.event_type == KeyDown && event.modifiers.command_or_ctrl() {
551 #[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))]
554 {
555 match event.key_code {
556 KeyCode::C => {
558 let text = self.on_copy();
560 if let (Some(text), Some(clipboard)) = (text, self.clipboard.as_mut()) {
561 let _ = clipboard.set_text(&text);
562 return true;
563 }
564 }
565 KeyCode::X => {
567 let text = self.on_cut();
569 if let (Some(text), Some(clipboard)) = (text, self.clipboard.as_mut()) {
570 let _ = clipboard.set_text(&text);
571 self.mark_dirty();
572 self.layout_dirty = true;
573 return true;
574 }
575 }
576 KeyCode::V => {
578 let text = self.clipboard.as_mut().and_then(|cb| cb.get_text().ok());
580 if let Some(text) = text {
581 if self.on_paste(&text) {
582 return true;
583 }
584 }
585 }
586 _ => {}
587 }
588 }
589 }
590
591 if !cranpose_ui::text_field_focus::has_focused_field() {
593 return false;
594 }
595
596 let handled = run_in_mutable_snapshot(|| {
600 cranpose_ui::text_field_focus::dispatch_key_event(event)
603 })
604 .unwrap_or(false);
605
606 if handled {
607 self.mark_dirty();
609 self.layout_dirty = true;
610 }
611
612 handled
613 }
614
615 pub fn on_paste(&mut self, text: &str) -> bool {
619 let handled =
623 run_in_mutable_snapshot(|| cranpose_ui::text_field_focus::dispatch_paste(text))
624 .unwrap_or(false);
625
626 if handled {
627 self.mark_dirty();
628 self.layout_dirty = true;
629 }
630
631 handled
632 }
633
634 pub fn on_copy(&mut self) -> Option<String> {
638 cranpose_ui::text_field_focus::dispatch_copy()
640 }
641
642 pub fn on_cut(&mut self) -> Option<String> {
646 let text = cranpose_ui::text_field_focus::dispatch_cut();
648
649 if text.is_some() {
650 self.mark_dirty();
651 self.layout_dirty = true;
652 }
653
654 text
655 }
656
657 #[cfg(all(target_os = "linux", not(target_arch = "wasm32")))]
661 pub fn set_primary_selection(&mut self, text: &str) {
662 use arboard::{LinuxClipboardKind, SetExtLinux};
663 if let Some(ref mut clipboard) = self.clipboard {
664 let result = clipboard
665 .set()
666 .clipboard(LinuxClipboardKind::Primary)
667 .text(text.to_string());
668 if let Err(e) = result {
669 log::debug!("Primary selection set failed: {:?}", e);
671 }
672 }
673 }
674
675 #[cfg(all(target_os = "linux", not(target_arch = "wasm32")))]
678 pub fn get_primary_selection(&mut self) -> Option<String> {
679 use arboard::{GetExtLinux, LinuxClipboardKind};
680 if let Some(ref mut clipboard) = self.clipboard {
681 clipboard
682 .get()
683 .clipboard(LinuxClipboardKind::Primary)
684 .text()
685 .ok()
686 } else {
687 None
688 }
689 }
690
691 #[cfg(all(not(target_os = "linux"), not(target_arch = "wasm32")))]
692 pub fn get_primary_selection(&mut self) -> Option<String> {
693 None
694 }
695
696 pub fn sync_selection_to_primary(&mut self) {
699 #[cfg(all(target_os = "linux", not(target_arch = "wasm32")))]
700 {
701 if let Some(text) = self.on_copy() {
702 self.set_primary_selection(&text);
703 }
704 }
705 }
706
707 pub fn on_ime_preedit(&mut self, text: &str, cursor: Option<(usize, usize)>) -> bool {
715 let handled = run_in_mutable_snapshot(|| {
717 cranpose_ui::text_field_focus::dispatch_ime_preedit(text, cursor)
718 })
719 .unwrap_or(false);
720
721 if handled {
722 self.mark_dirty();
723 self.layout_dirty = true;
725 }
726
727 handled
728 }
729
730 pub fn on_ime_delete_surrounding(&mut self, before_bytes: usize, after_bytes: usize) -> bool {
733 let handled = run_in_mutable_snapshot(|| {
734 cranpose_ui::text_field_focus::dispatch_delete_surrounding(before_bytes, after_bytes)
735 })
736 .unwrap_or(false);
737
738 if handled {
739 self.mark_dirty();
740 self.layout_dirty = true;
741 }
742
743 handled
744 }
745
746 pub fn log_debug_info(&mut self) {
747 println!("\n\n");
748 println!("════════════════════════════════════════════════════════");
749 println!(" DEBUG: CURRENT SCREEN STATE");
750 println!("════════════════════════════════════════════════════════");
751
752 if let Some(ref layout_tree) = self.layout_tree {
753 log_layout_tree(layout_tree);
754 let renderer = HeadlessRenderer::new();
755 let render_scene = renderer.render(layout_tree);
756 log_render_scene(&render_scene);
757 log_screen_summary(layout_tree, &render_scene);
758 } else {
759 println!("No layout available");
760 }
761
762 println!("════════════════════════════════════════════════════════");
763 println!("\n\n");
764 }
765
766 pub fn layout_tree(&self) -> Option<&LayoutTree> {
768 self.layout_tree.as_ref()
769 }
770
771 pub fn semantics_tree(&self) -> Option<&SemanticsTree> {
773 self.semantics_tree.as_ref()
774 }
775
776 fn process_frame(&mut self) {
777 fps_monitor::record_frame();
779
780 #[cfg(debug_assertions)]
781 let _frame_start = Instant::now();
782
783 self.run_layout_phase();
784
785 #[cfg(debug_assertions)]
786 let _after_layout = Instant::now();
787
788 self.run_dispatch_queues();
789
790 #[cfg(debug_assertions)]
791 let _after_dispatch = Instant::now();
792
793 self.run_render_phase();
794 }
795
796 fn run_layout_phase(&mut self) {
797 let repass_nodes = cranpose_ui::take_layout_repass_nodes();
804 let had_repass_nodes = !repass_nodes.is_empty();
805 if had_repass_nodes {
806 let root = self.composition.root();
807 let mut applier = self.composition.applier_mut();
808 for node_id in repass_nodes {
809 cranpose_core::bubble_measure_dirty(
812 &mut *applier as &mut dyn cranpose_core::Applier,
813 node_id,
814 );
815 cranpose_core::bubble_layout_dirty(
816 &mut *applier as &mut dyn cranpose_core::Applier,
817 node_id,
818 );
819 }
820
821 if let Some(root) = root {
826 if let Ok(node) = applier.get_mut(root) {
827 node.mark_needs_measure();
828 }
829 }
830
831 drop(applier);
832 self.layout_dirty = true;
833 }
834
835 let invalidation_requested = take_layout_invalidation();
853
854 if invalidation_requested && !had_repass_nodes {
861 cranpose_ui::layout::invalidate_all_layout_caches();
864
865 if let Some(root) = self.composition.root() {
868 let mut applier = self.composition.applier_mut();
869 if let Ok(node) = applier.get_mut(root) {
870 if let Some(layout_node) =
871 node.as_any_mut().downcast_mut::<cranpose_ui::LayoutNode>()
872 {
873 layout_node.mark_needs_measure();
874 layout_node.mark_needs_layout();
875 }
876 }
877 }
878 self.layout_dirty = true;
879 } else if invalidation_requested {
880 self.layout_dirty = true;
883 }
884
885 if !self.layout_dirty {
887 return;
888 }
889
890 let viewport_size = Size {
891 width: self.viewport.0,
892 height: self.viewport.1,
893 };
894 if let Some(root) = self.composition.root() {
895 let handle = self.composition.runtime_handle();
896 let mut applier = self.composition.applier_mut();
897 applier.set_runtime_handle(handle);
898
899 let tree_needs_layout_check = cranpose_ui::tree_needs_layout(&mut *applier, root)
902 .unwrap_or_else(|err| {
903 log::warn!(
904 "Cannot check layout dirty status for root #{}: {}",
905 root,
906 err
907 );
908 true });
910
911 let needs_layout = tree_needs_layout_check || self.layout_dirty;
915
916 if !needs_layout {
917 log::trace!("Skipping layout: tree is clean");
919 self.layout_dirty = false;
920 applier.clear_runtime_handle();
921 return;
922 }
923
924 self.layout_dirty = false;
926
927 match cranpose_ui::measure_layout(&mut applier, root, viewport_size) {
929 Ok(measurements) => {
930 self.semantics_tree = Some(measurements.semantics_tree().clone());
931 self.layout_tree = Some(measurements.into_layout_tree());
932 self.scene_dirty = true;
933 }
934 Err(err) => {
935 log::error!("failed to compute layout: {err}");
936 self.layout_tree = None;
937 self.semantics_tree = None;
938 self.scene_dirty = true;
939 }
940 }
941 applier.clear_runtime_handle();
942 } else {
943 self.layout_tree = None;
944 self.semantics_tree = None;
945 self.scene_dirty = true;
946 self.layout_dirty = false;
947 }
948 }
949
950 fn run_dispatch_queues(&mut self) {
951 if has_pending_pointer_repasses() {
955 let mut applier = self.composition.applier_mut();
956 process_pointer_repasses(|node_id| {
957 let result = applier.with_node::<LayoutNode, _>(node_id, |layout_node| {
959 if layout_node.needs_pointer_pass() {
960 layout_node.clear_needs_pointer_pass();
961 log::trace!("Cleared pointer repass flag for node #{}", node_id);
962 }
963 });
964 if let Err(err) = result {
965 log::debug!(
966 "Could not process pointer repass for node #{}: {}",
967 node_id,
968 err
969 );
970 }
971 });
972 }
973
974 if has_pending_focus_invalidations() {
978 let mut applier = self.composition.applier_mut();
979 process_focus_invalidations(|node_id| {
980 let result = applier.with_node::<LayoutNode, _>(node_id, |layout_node| {
982 if layout_node.needs_focus_sync() {
983 layout_node.clear_needs_focus_sync();
984 log::trace!("Cleared focus sync flag for node #{}", node_id);
985 }
986 });
987 if let Err(err) = result {
988 log::debug!(
989 "Could not process focus invalidation for node #{}: {}",
990 node_id,
991 err
992 );
993 }
994 });
995 }
996 }
997
998 fn refresh_draw_repasses(&mut self) {
999 let dirty_nodes = take_draw_repass_nodes();
1000 if dirty_nodes.is_empty() {
1001 return;
1002 }
1003
1004 let Some(layout_tree) = self.layout_tree.as_mut() else {
1005 return;
1006 };
1007
1008 let dirty_set: HashSet<NodeId> = dirty_nodes.into_iter().collect();
1009 let mut applier = self.composition.applier_mut();
1010 refresh_layout_box_data(&mut applier, layout_tree.root_mut(), &dirty_set);
1011 }
1012
1013 fn run_render_phase(&mut self) {
1014 let render_dirty = take_render_invalidation();
1015 let pointer_dirty = take_pointer_invalidation();
1016 let focus_dirty = take_focus_invalidation();
1017 let draw_repass_pending = cranpose_ui::has_pending_draw_repasses();
1018 let cursor_blink_dirty = cranpose_ui::tick_cursor_blink();
1020 if render_dirty || pointer_dirty || focus_dirty || cursor_blink_dirty || draw_repass_pending
1021 {
1022 self.scene_dirty = true;
1023 }
1024 if !self.scene_dirty {
1025 return;
1026 }
1027 self.scene_dirty = false;
1028 self.refresh_draw_repasses();
1029 let viewport_size = Size {
1030 width: self.viewport.0,
1031 height: self.viewport.1,
1032 };
1033
1034 if let Some(root) = self.composition.root() {
1036 let mut applier = self.composition.applier_mut();
1037 if let Err(err) =
1038 self.renderer
1039 .rebuild_scene_from_applier(&mut applier, root, viewport_size)
1040 {
1041 log::error!("renderer rebuild failed: {err:?}");
1043 self.renderer.scene_mut().clear();
1044 }
1045 } else {
1046 self.renderer.scene_mut().clear();
1047 }
1048
1049 if self.dev_options.fps_counter {
1051 let stats = fps_monitor::fps_stats();
1052 let text = format!(
1053 "{:.0} FPS | {:.1}ms | {} recomp/s",
1054 stats.fps, stats.avg_ms, stats.recomps_per_second
1055 );
1056 self.renderer.draw_dev_overlay(&text, viewport_size);
1057 }
1058 }
1059}
1060
1061fn refresh_layout_box_data(
1062 applier: &mut MemoryApplier,
1063 layout: &mut cranpose_ui::layout::LayoutBox,
1064 dirty_nodes: &HashSet<NodeId>,
1065) {
1066 if dirty_nodes.contains(&layout.node_id) {
1067 if let Ok((modifier, resolved_modifiers, slices)) =
1068 applier.with_node::<LayoutNode, _>(layout.node_id, |node| {
1069 node.clear_needs_redraw();
1070 (
1071 node.modifier.clone(),
1072 node.resolved_modifiers(),
1073 node.modifier_slices_snapshot(),
1074 )
1075 })
1076 {
1077 layout.node_data.modifier = modifier;
1078 layout.node_data.resolved_modifiers = resolved_modifiers;
1079 layout.node_data.modifier_slices = slices;
1080 } else if let Ok((modifier, resolved_modifiers)) = applier
1081 .with_node::<SubcomposeLayoutNode, _>(layout.node_id, |node| {
1082 node.clear_needs_redraw();
1083 (node.modifier(), node.resolved_modifiers())
1084 })
1085 {
1086 layout.node_data.modifier = modifier.clone();
1087 layout.node_data.resolved_modifiers = resolved_modifiers;
1088 layout.node_data.modifier_slices =
1089 std::rc::Rc::new(cranpose_ui::collect_slices_from_modifier(&modifier));
1090 }
1091 }
1092
1093 for child in &mut layout.children {
1094 refresh_layout_box_data(applier, child, dirty_nodes);
1095 }
1096}
1097
1098impl<R> Drop for AppShell<R>
1099where
1100 R: Renderer,
1101{
1102 fn drop(&mut self) {
1103 self.runtime.clear_frame_waker();
1104 }
1105}
1106
1107pub fn default_root_key() -> Key {
1108 location_key(file!(), line!(), column!())
1109}
1110
1111#[cfg(test)]
1112#[path = "tests/app_shell_tests.rs"]
1113mod tests;