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, MeasureLayoutOptions,
30 SemanticsTree, 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
39#[derive(Copy, Clone)]
40enum DispatchInvalidationKind {
41 Pointer,
42 Focus,
43}
44
45pub struct AppShell<R>
46where
47 R: Renderer,
48{
49 runtime: StdRuntime,
50 composition: Composition<MemoryApplier>,
51 renderer: R,
52 cursor: (f32, f32),
53 viewport: (f32, f32),
54 buffer_size: (u32, u32),
55 start_time: Instant,
56 layout_tree: Option<LayoutTree>,
57 semantics_tree: Option<SemanticsTree>,
58 semantics_enabled: bool,
59 layout_dirty: bool,
60 scene_dirty: bool,
61 is_dirty: bool,
62 buttons_pressed: PointerButtons,
64 hit_path_tracker: HitPathTracker,
71 #[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))]
73 clipboard: Option<arboard::Clipboard>,
74 dev_options: DevOptions,
76}
77
78#[derive(Clone, Debug, Default)]
83pub struct DevOptions {
84 pub fps_counter: bool,
86 pub recomposition_counter: bool,
88 pub layout_timing: bool,
90}
91
92fn input_pipeline_debug_enabled() -> bool {
93 static ENABLED: OnceLock<bool> = OnceLock::new();
94 *ENABLED.get_or_init(|| std::env::var_os("CRANPOSE_INPUT_DEBUG").is_some())
95}
96
97impl<R> AppShell<R>
98where
99 R: Renderer,
100 R::Error: Debug,
101{
102 pub fn new(mut renderer: R, root_key: Key, content: impl FnMut() + 'static) -> Self {
103 fps_monitor::init_fps_tracker();
105
106 let runtime = StdRuntime::new();
107 let mut composition = Composition::with_runtime(MemoryApplier::new(), runtime.runtime());
108 let build = content;
109 if let Err(err) = composition.render(root_key, build) {
110 log::error!("initial render failed: {err}");
111 }
112 renderer.scene_mut().clear();
113 let mut shell = Self {
114 runtime,
115 composition,
116 renderer,
117 cursor: (0.0, 0.0),
118 viewport: (800.0, 600.0),
119 buffer_size: (800, 600),
120 start_time: Instant::now(),
121 layout_tree: None,
122 semantics_tree: None,
123 semantics_enabled: false,
124 layout_dirty: true,
125 scene_dirty: true,
126 is_dirty: true,
127 buttons_pressed: PointerButtons::NONE,
128 hit_path_tracker: HitPathTracker::new(),
129 #[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))]
130 clipboard: arboard::Clipboard::new().ok(),
131 dev_options: DevOptions::default(),
132 };
133 shell.process_frame();
134 shell
135 }
136
137 pub fn set_dev_options(&mut self, options: DevOptions) {
142 self.dev_options = options;
143 }
144
145 pub fn dev_options(&self) -> &DevOptions {
147 &self.dev_options
148 }
149
150 pub fn set_viewport(&mut self, width: f32, height: f32) {
151 self.viewport = (width, height);
152 self.layout_dirty = true;
153 self.mark_dirty();
154 self.process_frame();
155 }
156
157 pub fn set_buffer_size(&mut self, width: u32, height: u32) {
158 self.buffer_size = (width, height);
159 }
160
161 pub fn buffer_size(&self) -> (u32, u32) {
162 self.buffer_size
163 }
164
165 pub fn scene(&self) -> &R::Scene {
166 self.renderer.scene()
167 }
168
169 pub fn renderer(&mut self) -> &mut R {
170 &mut self.renderer
171 }
172
173 #[cfg(not(target_arch = "wasm32"))]
174 pub fn set_frame_waker(&mut self, waker: impl Fn() + Send + Sync + 'static) {
175 self.runtime.set_frame_waker(waker);
176 }
177
178 #[cfg(target_arch = "wasm32")]
179 pub fn set_frame_waker(&mut self, waker: impl Fn() + Send + 'static) {
180 self.runtime.set_frame_waker(waker);
181 }
182
183 pub fn clear_frame_waker(&mut self) {
184 self.runtime.clear_frame_waker();
185 }
186
187 pub fn should_render(&self) -> bool {
188 if self.layout_dirty
189 || self.scene_dirty
190 || peek_render_invalidation()
191 || peek_pointer_invalidation()
192 || peek_focus_invalidation()
193 || peek_layout_invalidation()
194 {
195 return true;
196 }
197 self.runtime.take_frame_request() || self.composition.should_render()
198 }
199
200 pub fn needs_redraw(&self) -> bool {
203 if self.is_dirty
204 || self.layout_dirty
205 || self.scene_dirty
206 || peek_render_invalidation()
207 || peek_pointer_invalidation()
208 || peek_focus_invalidation()
209 || peek_layout_invalidation()
210 || cranpose_ui::has_pending_layout_repasses()
211 || cranpose_ui::has_pending_draw_repasses()
212 || has_pending_pointer_repasses()
213 || has_pending_focus_invalidations()
214 {
215 return true;
216 }
217
218 self.composition.should_render()
219 }
220
221 pub fn mark_dirty(&mut self) {
223 self.is_dirty = true;
224 }
225
226 pub fn has_active_animations(&self) -> bool {
228 self.runtime.take_frame_request() || self.composition.should_render()
229 }
230
231 pub fn next_event_time(&self) -> Option<web_time::Instant> {
234 cranpose_ui::next_cursor_blink_time()
235 }
236
237 fn resolve_hit_path(
244 &self,
245 pointer: PointerId,
246 ) -> Vec<<<R as Renderer>::Scene as RenderScene>::HitTarget> {
247 let Some(node_ids) = self.hit_path_tracker.get_path(pointer) else {
248 return Vec::new();
249 };
250
251 let scene = self.renderer.scene();
252 node_ids
253 .iter()
254 .filter_map(|&id| scene.find_target(id))
255 .collect()
256 }
257
258 pub fn update(&mut self) {
259 let now = Instant::now();
260 let frame_time = now
261 .checked_duration_since(self.start_time)
262 .unwrap_or_default()
263 .as_nanos() as u64;
264 self.runtime.drain_frame_callbacks(frame_time);
265 self.runtime.runtime_handle().drain_ui();
266 let should_render = self.composition.should_render();
267 if input_pipeline_debug_enabled() && should_render {
268 eprintln!(
269 "[CRANPOSE_INPUT_DEBUG] update begin: should_render=true layout_dirty={} scene_dirty={} is_dirty={}",
270 self.layout_dirty, self.scene_dirty, self.is_dirty
271 );
272 }
273 if should_render {
274 match self.composition.process_invalid_scopes() {
275 Ok(changed) => {
276 if input_pipeline_debug_enabled() {
277 eprintln!(
278 "[CRANPOSE_INPUT_DEBUG] process_invalid_scopes changed={}",
279 changed
280 );
281 }
282 if changed {
283 fps_monitor::record_recomposition();
284 self.layout_dirty = true;
285 if let Some(root_id) = self.composition.root() {
288 let _ = self.composition.applier_mut().with_node::<LayoutNode, _>(
289 root_id,
290 |node| {
291 node.mark_needs_measure();
292 },
293 );
294 }
295 request_render_invalidation();
296 }
297 }
298 Err(NodeError::Missing { id }) => {
299 log::debug!("Recomposition skipped: node {} no longer exists", id);
302 self.layout_dirty = true;
303 request_render_invalidation();
304 }
305 Err(err) => {
306 log::error!("recomposition failed: {err}");
307 self.layout_dirty = true;
308 request_render_invalidation();
309 }
310 }
311 }
312 self.process_frame();
313 self.is_dirty = false;
315 }
316
317 pub fn set_cursor(&mut self, x: f32, y: f32) -> bool {
318 enter_event_handler();
319 let result = run_in_mutable_snapshot(|| self.set_cursor_inner(x, y)).unwrap_or(false);
320 exit_event_handler();
321 if input_pipeline_debug_enabled() {
322 eprintln!(
323 "[CRANPOSE_INPUT_DEBUG] set_cursor ({:.2},{:.2}) -> {}",
324 x, y, result
325 );
326 }
327 result
328 }
329
330 fn set_cursor_inner(&mut self, x: f32, y: f32) -> bool {
331 self.cursor = (x, y);
332
333 if self.buttons_pressed != PointerButtons::NONE {
337 if self.hit_path_tracker.has_path(PointerId::PRIMARY) {
338 let targets = self.resolve_hit_path(PointerId::PRIMARY);
340
341 if !targets.is_empty() {
342 let event =
343 PointerEvent::new(PointerEventKind::Move, Point { x, y }, Point { x, y })
344 .with_buttons(self.buttons_pressed);
345
346 for hit in targets {
347 hit.dispatch(event.clone());
348 if event.is_consumed() {
349 break;
350 }
351 }
352 return true;
353 }
354
355 let hits = self.renderer.scene().hit_test(x, y);
358 if !hits.is_empty() {
359 let node_ids: Vec<_> = hits.iter().map(|h| h.node_id()).collect();
360 self.hit_path_tracker
361 .add_hit_path(PointerId::PRIMARY, node_ids);
362 let event =
363 PointerEvent::new(PointerEventKind::Move, Point { x, y }, Point { x, y })
364 .with_buttons(self.buttons_pressed);
365 for hit in hits {
366 hit.dispatch(event.clone());
367 if event.is_consumed() {
368 break;
369 }
370 }
371 return true;
372 }
373 return false;
374 }
375
376 return false;
379 }
380
381 let hits = self.renderer.scene().hit_test(x, y);
383 if !hits.is_empty() {
384 let event = PointerEvent::new(PointerEventKind::Move, Point { x, y }, Point { x, y })
385 .with_buttons(self.buttons_pressed); for hit in hits {
387 hit.dispatch(event.clone());
388 if event.is_consumed() {
389 break;
390 }
391 }
392 true
393 } else {
394 false
395 }
396 }
397
398 pub fn pointer_pressed(&mut self) -> bool {
399 enter_event_handler();
400 let result = run_in_mutable_snapshot(|| self.pointer_pressed_inner()).unwrap_or(false);
401 exit_event_handler();
402 if input_pipeline_debug_enabled() {
403 eprintln!("[CRANPOSE_INPUT_DEBUG] pointer_pressed -> {}", result);
404 }
405 result
406 }
407
408 fn pointer_pressed_inner(&mut self) -> bool {
409 self.buttons_pressed.insert(PointerButton::Primary);
411
412 let hits = self.renderer.scene().hit_test(self.cursor.0, self.cursor.1);
420
421 let node_ids: Vec<_> = hits.iter().map(|h| h.node_id()).collect();
423 self.hit_path_tracker
424 .add_hit_path(PointerId::PRIMARY, node_ids);
425
426 if !hits.is_empty() {
427 let event = PointerEvent::new(
428 PointerEventKind::Down,
429 Point {
430 x: self.cursor.0,
431 y: self.cursor.1,
432 },
433 Point {
434 x: self.cursor.0,
435 y: self.cursor.1,
436 },
437 )
438 .with_buttons(self.buttons_pressed);
439
440 for hit in hits {
442 hit.dispatch(event.clone());
443 if event.is_consumed() {
444 break;
445 }
446 }
447 true
448 } else {
449 false
450 }
451 }
452
453 pub fn pointer_released(&mut self) -> bool {
454 enter_event_handler();
455 let result = run_in_mutable_snapshot(|| self.pointer_released_inner()).unwrap_or(false);
456 exit_event_handler();
457 if input_pipeline_debug_enabled() {
458 eprintln!("[CRANPOSE_INPUT_DEBUG] pointer_released -> {}", result);
459 }
460 result
461 }
462
463 fn pointer_released_inner(&mut self) -> bool {
464 self.buttons_pressed.remove(PointerButton::Primary);
467 let corrected_buttons = self.buttons_pressed;
468
469 let targets = self.resolve_hit_path(PointerId::PRIMARY);
471
472 self.hit_path_tracker.remove_path(PointerId::PRIMARY);
474
475 if !targets.is_empty() {
476 let event = PointerEvent::new(
477 PointerEventKind::Up,
478 Point {
479 x: self.cursor.0,
480 y: self.cursor.1,
481 },
482 Point {
483 x: self.cursor.0,
484 y: self.cursor.1,
485 },
486 )
487 .with_buttons(corrected_buttons);
488
489 for hit in targets {
490 hit.dispatch(event.clone());
491 if event.is_consumed() {
492 break;
493 }
494 }
495 true
496 } else {
497 false
498 }
499 }
500
501 pub fn cancel_gesture(&mut self) {
507 enter_event_handler();
508 let _ = run_in_mutable_snapshot(|| {
509 self.cancel_gesture_inner();
510 });
511 exit_event_handler();
512 }
513
514 fn cancel_gesture_inner(&mut self) {
515 let targets = self.resolve_hit_path(PointerId::PRIMARY);
517
518 self.hit_path_tracker.clear();
520 self.buttons_pressed = PointerButtons::NONE;
521
522 if !targets.is_empty() {
523 let event = PointerEvent::new(
524 PointerEventKind::Cancel,
525 Point {
526 x: self.cursor.0,
527 y: self.cursor.1,
528 },
529 Point {
530 x: self.cursor.0,
531 y: self.cursor.1,
532 },
533 );
534
535 for hit in targets {
536 hit.dispatch(event.clone());
537 }
538 }
539 }
540 pub fn on_key_event(&mut self, event: &KeyEvent) -> bool {
547 enter_event_handler();
548 let result = self.on_key_event_inner(event);
549 exit_event_handler();
550 result
551 }
552
553 fn on_key_event_inner(&mut self, event: &KeyEvent) -> bool {
555 use KeyEventType::KeyDown;
556
557 if event.event_type == KeyDown && event.modifiers.command_or_ctrl() {
559 #[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))]
562 {
563 match event.key_code {
564 KeyCode::C => {
566 let text = self.on_copy();
568 if let (Some(text), Some(clipboard)) = (text, self.clipboard.as_mut()) {
569 let _ = clipboard.set_text(&text);
570 return true;
571 }
572 }
573 KeyCode::X => {
575 let text = self.on_cut();
577 if let (Some(text), Some(clipboard)) = (text, self.clipboard.as_mut()) {
578 let _ = clipboard.set_text(&text);
579 self.mark_dirty();
580 self.layout_dirty = true;
581 return true;
582 }
583 }
584 KeyCode::V => {
586 let text = self.clipboard.as_mut().and_then(|cb| cb.get_text().ok());
588 if let Some(text) = text {
589 if self.on_paste(&text) {
590 return true;
591 }
592 }
593 }
594 _ => {}
595 }
596 }
597 }
598
599 if !cranpose_ui::text_field_focus::has_focused_field() {
601 return false;
602 }
603
604 let handled = run_in_mutable_snapshot(|| {
608 cranpose_ui::text_field_focus::dispatch_key_event(event)
611 })
612 .unwrap_or(false);
613
614 if handled {
615 self.mark_dirty();
617 self.layout_dirty = true;
618 }
619
620 handled
621 }
622
623 pub fn on_paste(&mut self, text: &str) -> bool {
627 let handled =
631 run_in_mutable_snapshot(|| cranpose_ui::text_field_focus::dispatch_paste(text))
632 .unwrap_or(false);
633
634 if handled {
635 self.mark_dirty();
636 self.layout_dirty = true;
637 }
638
639 handled
640 }
641
642 pub fn on_copy(&mut self) -> Option<String> {
646 cranpose_ui::text_field_focus::dispatch_copy()
648 }
649
650 pub fn on_cut(&mut self) -> Option<String> {
654 let text = cranpose_ui::text_field_focus::dispatch_cut();
656
657 if text.is_some() {
658 self.mark_dirty();
659 self.layout_dirty = true;
660 }
661
662 text
663 }
664
665 #[cfg(all(target_os = "linux", not(target_arch = "wasm32")))]
669 pub fn set_primary_selection(&mut self, text: &str) {
670 use arboard::{LinuxClipboardKind, SetExtLinux};
671 if let Some(ref mut clipboard) = self.clipboard {
672 let result = clipboard
673 .set()
674 .clipboard(LinuxClipboardKind::Primary)
675 .text(text.to_string());
676 if let Err(e) = result {
677 log::debug!("Primary selection set failed: {:?}", e);
679 }
680 }
681 }
682
683 #[cfg(all(target_os = "linux", not(target_arch = "wasm32")))]
686 pub fn get_primary_selection(&mut self) -> Option<String> {
687 use arboard::{GetExtLinux, LinuxClipboardKind};
688 if let Some(ref mut clipboard) = self.clipboard {
689 clipboard
690 .get()
691 .clipboard(LinuxClipboardKind::Primary)
692 .text()
693 .ok()
694 } else {
695 None
696 }
697 }
698
699 #[cfg(all(not(target_os = "linux"), not(target_arch = "wasm32")))]
700 pub fn get_primary_selection(&mut self) -> Option<String> {
701 None
702 }
703
704 pub fn sync_selection_to_primary(&mut self) {
707 #[cfg(all(target_os = "linux", not(target_arch = "wasm32")))]
708 {
709 if let Some(text) = self.on_copy() {
710 self.set_primary_selection(&text);
711 }
712 }
713 }
714
715 pub fn on_ime_preedit(&mut self, text: &str, cursor: Option<(usize, usize)>) -> bool {
723 let handled = run_in_mutable_snapshot(|| {
725 cranpose_ui::text_field_focus::dispatch_ime_preedit(text, cursor)
726 })
727 .unwrap_or(false);
728
729 if handled {
730 self.mark_dirty();
731 self.layout_dirty = true;
733 }
734
735 handled
736 }
737
738 pub fn on_ime_delete_surrounding(&mut self, before_bytes: usize, after_bytes: usize) -> bool {
741 let handled = run_in_mutable_snapshot(|| {
742 cranpose_ui::text_field_focus::dispatch_delete_surrounding(before_bytes, after_bytes)
743 })
744 .unwrap_or(false);
745
746 if handled {
747 self.mark_dirty();
748 self.layout_dirty = true;
749 }
750
751 handled
752 }
753
754 pub fn log_debug_info(&mut self) {
755 println!("\n\n");
756 println!("════════════════════════════════════════════════════════");
757 println!(" DEBUG: CURRENT SCREEN STATE");
758 println!("════════════════════════════════════════════════════════");
759
760 if let Some(ref layout_tree) = self.layout_tree {
761 log_layout_tree(layout_tree);
762 let renderer = HeadlessRenderer::new();
763 let render_scene = renderer.render(layout_tree);
764 log_render_scene(&render_scene);
765 log_screen_summary(layout_tree, &render_scene);
766 } else {
767 println!("No layout available");
768 }
769
770 println!("════════════════════════════════════════════════════════");
771 println!("\n\n");
772 }
773
774 pub fn layout_tree(&self) -> Option<&LayoutTree> {
776 self.layout_tree.as_ref()
777 }
778
779 pub fn semantics_tree(&self) -> Option<&SemanticsTree> {
781 self.semantics_tree.as_ref()
782 }
783
784 pub fn set_semantics_enabled(&mut self, enabled: bool) {
785 if self.semantics_enabled == enabled {
786 return;
787 }
788 self.semantics_enabled = enabled;
789 if enabled {
790 self.layout_dirty = true;
791 self.mark_dirty();
792 } else {
793 self.semantics_tree = None;
794 }
795 }
796
797 fn process_frame(&mut self) {
798 fps_monitor::record_frame();
800
801 #[cfg(debug_assertions)]
802 let _frame_start = Instant::now();
803
804 self.run_layout_phase();
805
806 #[cfg(debug_assertions)]
807 let _after_layout = Instant::now();
808
809 self.run_dispatch_queues();
810
811 #[cfg(debug_assertions)]
812 let _after_dispatch = Instant::now();
813
814 self.run_render_phase();
815 }
816
817 fn run_layout_phase(&mut self) {
818 let repass_nodes = cranpose_ui::take_layout_repass_nodes();
825 let had_repass_nodes = !repass_nodes.is_empty();
826 if had_repass_nodes {
827 let root = self.composition.root();
828 let mut applier = self.composition.applier_mut();
829 for node_id in repass_nodes {
830 cranpose_core::bubble_measure_dirty(
833 &mut *applier as &mut dyn cranpose_core::Applier,
834 node_id,
835 );
836 cranpose_core::bubble_layout_dirty(
837 &mut *applier as &mut dyn cranpose_core::Applier,
838 node_id,
839 );
840 }
841
842 if let Some(root) = root {
847 if let Ok(node) = applier.get_mut(root) {
848 node.mark_needs_measure();
849 }
850 }
851
852 drop(applier);
853 self.layout_dirty = true;
854 }
855
856 let invalidation_requested = take_layout_invalidation();
874
875 if invalidation_requested && !had_repass_nodes {
882 cranpose_ui::layout::invalidate_all_layout_caches();
885
886 if let Some(root) = self.composition.root() {
889 let mut applier = self.composition.applier_mut();
890 if let Ok(node) = applier.get_mut(root) {
891 if let Some(layout_node) =
892 node.as_any_mut().downcast_mut::<cranpose_ui::LayoutNode>()
893 {
894 layout_node.mark_needs_measure();
895 layout_node.mark_needs_layout();
896 }
897 }
898 }
899 self.layout_dirty = true;
900 } else if invalidation_requested {
901 self.layout_dirty = true;
904 }
905
906 if !self.layout_dirty {
908 return;
909 }
910
911 let viewport_size = Size {
912 width: self.viewport.0,
913 height: self.viewport.1,
914 };
915 if let Some(root) = self.composition.root() {
916 let handle = self.composition.runtime_handle();
917 let mut applier = self.composition.applier_mut();
918 applier.set_runtime_handle(handle);
919
920 let tree_needs_layout_check = cranpose_ui::tree_needs_layout(&mut *applier, root)
923 .unwrap_or_else(|err| {
924 log::warn!(
925 "Cannot check layout dirty status for root #{}: {}",
926 root,
927 err
928 );
929 true });
931
932 let needs_layout = tree_needs_layout_check || self.layout_dirty;
936
937 if !needs_layout {
938 log::trace!("Skipping layout: tree is clean");
940 self.layout_dirty = false;
941 applier.clear_runtime_handle();
942 return;
943 }
944
945 self.layout_dirty = false;
947
948 match cranpose_ui::measure_layout_with_options(
950 &mut applier,
951 root,
952 viewport_size,
953 MeasureLayoutOptions {
954 collect_semantics: self.semantics_enabled,
955 },
956 ) {
957 Ok(measurements) => {
958 self.semantics_tree = measurements.semantics_tree().cloned();
959 self.layout_tree = Some(measurements.into_layout_tree());
960 self.scene_dirty = true;
961 }
962 Err(err) => {
963 log::error!("failed to compute layout: {err}");
964 self.layout_tree = None;
965 self.semantics_tree = None;
966 self.scene_dirty = true;
967 }
968 }
969 applier.clear_runtime_handle();
970 } else {
971 self.layout_tree = None;
972 self.semantics_tree = None;
973 self.scene_dirty = true;
974 self.layout_dirty = false;
975 }
976 }
977
978 fn run_dispatch_queues(&mut self) {
979 if has_pending_pointer_repasses() {
983 let mut applier = self.composition.applier_mut();
984 process_pointer_repasses(|node_id| {
985 match clear_dispatch_invalidation(
986 &mut applier,
987 node_id,
988 DispatchInvalidationKind::Pointer,
989 ) {
990 Ok(true) => {
991 log::trace!("Cleared pointer repass flag for node #{}", node_id);
992 }
993 Ok(false) => {}
994 Err(err) => {
995 log::debug!(
996 "Could not process pointer repass for node #{}: {}",
997 node_id,
998 err
999 );
1000 }
1001 }
1002 });
1003 }
1004
1005 if has_pending_focus_invalidations() {
1009 let mut applier = self.composition.applier_mut();
1010 process_focus_invalidations(|node_id| {
1011 match clear_dispatch_invalidation(
1012 &mut applier,
1013 node_id,
1014 DispatchInvalidationKind::Focus,
1015 ) {
1016 Ok(true) => {
1017 log::trace!("Cleared focus sync flag for node #{}", node_id);
1018 }
1019 Ok(false) => {}
1020 Err(err) => {
1021 log::debug!(
1022 "Could not process focus invalidation for node #{}: {}",
1023 node_id,
1024 err
1025 );
1026 }
1027 }
1028 });
1029 }
1030 }
1031
1032 fn refresh_draw_repasses(&mut self) {
1033 let dirty_nodes = take_draw_repass_nodes();
1034 if dirty_nodes.is_empty() {
1035 return;
1036 }
1037
1038 let Some(layout_tree) = self.layout_tree.as_mut() else {
1039 return;
1040 };
1041
1042 let dirty_set: HashSet<NodeId> = dirty_nodes.into_iter().collect();
1043 let mut applier = self.composition.applier_mut();
1044 let refresh_scope = build_draw_refresh_scope(&mut applier, &dirty_set);
1045 refresh_layout_box_data(
1046 &mut applier,
1047 layout_tree.root_mut(),
1048 &refresh_scope,
1049 &dirty_set,
1050 );
1051 }
1052
1053 fn run_render_phase(&mut self) {
1054 let render_dirty = take_render_invalidation();
1055 take_pointer_invalidation();
1056 take_focus_invalidation();
1057 let draw_repass_pending = cranpose_ui::has_pending_draw_repasses();
1058 let cursor_blink_dirty = cranpose_ui::tick_cursor_blink();
1060
1061 let render_only_dirty = render_dirty || cursor_blink_dirty;
1062 let needs_scene_rebuild = self.scene_dirty
1065 || draw_repass_pending
1066 || (self.dev_options.fps_counter && render_only_dirty);
1067
1068 if !needs_scene_rebuild {
1069 return;
1070 }
1071 self.scene_dirty = false;
1072 self.refresh_draw_repasses();
1073 let viewport_size = Size {
1074 width: self.viewport.0,
1075 height: self.viewport.1,
1076 };
1077
1078 if let Some(root) = self.composition.root() {
1080 let mut applier = self.composition.applier_mut();
1081 if let Err(err) =
1082 self.renderer
1083 .rebuild_scene_from_applier(&mut applier, root, viewport_size)
1084 {
1085 log::error!("renderer rebuild failed: {err:?}");
1087 self.renderer.scene_mut().clear();
1088 }
1089 } else {
1090 self.renderer.scene_mut().clear();
1091 }
1092
1093 if self.dev_options.fps_counter {
1095 let stats = fps_monitor::fps_stats();
1096 let text = format!(
1097 "{:.0} FPS | {:.1}ms | {} recomp/s",
1098 stats.fps, stats.avg_ms, stats.recomps_per_second
1099 );
1100 self.renderer.draw_dev_overlay(&text, viewport_size);
1101 }
1102 }
1103}
1104
1105fn clear_dispatch_invalidation(
1106 applier: &mut MemoryApplier,
1107 node_id: NodeId,
1108 invalidation: DispatchInvalidationKind,
1109) -> Result<bool, NodeError> {
1110 match invalidation {
1111 DispatchInvalidationKind::Pointer => {
1112 match applier.with_node::<LayoutNode, _>(node_id, |node| {
1113 let needs_pointer_pass = node.needs_pointer_pass();
1114 if needs_pointer_pass {
1115 node.clear_needs_pointer_pass();
1116 }
1117 needs_pointer_pass
1118 }) {
1119 Ok(cleared) => Ok(cleared),
1120 Err(NodeError::TypeMismatch { .. }) => applier
1121 .with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
1122 let needs_pointer_pass = node.needs_pointer_pass();
1123 if needs_pointer_pass {
1124 node.clear_needs_pointer_pass();
1125 }
1126 needs_pointer_pass
1127 }),
1128 Err(err) => Err(err),
1129 }
1130 }
1131 DispatchInvalidationKind::Focus => {
1132 match applier.with_node::<LayoutNode, _>(node_id, |node| {
1133 let needs_focus_sync = node.needs_focus_sync();
1134 if needs_focus_sync {
1135 node.clear_needs_focus_sync();
1136 }
1137 needs_focus_sync
1138 }) {
1139 Ok(cleared) => Ok(cleared),
1140 Err(NodeError::TypeMismatch { .. }) => applier
1141 .with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
1142 let needs_focus_sync = node.needs_focus_sync();
1143 if needs_focus_sync {
1144 node.clear_needs_focus_sync();
1145 }
1146 needs_focus_sync
1147 }),
1148 Err(err) => Err(err),
1149 }
1150 }
1151 }
1152}
1153
1154fn build_draw_refresh_scope(
1155 applier: &mut MemoryApplier,
1156 dirty_nodes: &HashSet<NodeId>,
1157) -> HashSet<NodeId> {
1158 let mut refresh_scope = HashSet::with_capacity(dirty_nodes.len());
1159 for &dirty_node in dirty_nodes {
1160 let mut current = Some(dirty_node);
1161 while let Some(node_id) = current {
1162 if !refresh_scope.insert(node_id) {
1163 break;
1164 }
1165 current = applier.get_mut(node_id).ok().and_then(|node| node.parent());
1166 }
1167 }
1168 refresh_scope
1169}
1170
1171fn refresh_layout_box_data(
1172 applier: &mut MemoryApplier,
1173 layout: &mut cranpose_ui::layout::LayoutBox,
1174 refresh_scope: &HashSet<NodeId>,
1175 dirty_nodes: &HashSet<NodeId>,
1176) {
1177 if !refresh_scope.contains(&layout.node_id) {
1178 return;
1179 }
1180
1181 if dirty_nodes.contains(&layout.node_id) {
1182 if let Ok((modifier, resolved_modifiers, slices)) =
1183 applier.with_node::<LayoutNode, _>(layout.node_id, |node| {
1184 node.clear_needs_redraw();
1185 (
1186 node.modifier.clone(),
1187 node.resolved_modifiers(),
1188 node.modifier_slices_snapshot(),
1189 )
1190 })
1191 {
1192 layout.node_data.modifier = modifier;
1193 layout.node_data.resolved_modifiers = resolved_modifiers;
1194 layout.node_data.modifier_slices = slices;
1195 } else if let Ok((modifier, resolved_modifiers)) = applier
1196 .with_node::<SubcomposeLayoutNode, _>(layout.node_id, |node| {
1197 node.clear_needs_redraw();
1198 (node.modifier(), node.resolved_modifiers())
1199 })
1200 {
1201 layout.node_data.modifier = modifier.clone();
1202 layout.node_data.resolved_modifiers = resolved_modifiers;
1203 layout.node_data.modifier_slices =
1204 std::rc::Rc::new(cranpose_ui::collect_slices_from_modifier(&modifier));
1205 }
1206 }
1207
1208 for child in &mut layout.children {
1209 refresh_layout_box_data(applier, child, refresh_scope, dirty_nodes);
1210 }
1211}
1212
1213impl<R> Drop for AppShell<R>
1214where
1215 R: Renderer,
1216{
1217 fn drop(&mut self) {
1218 self.runtime.clear_frame_waker();
1219 }
1220}
1221
1222pub fn default_root_key() -> Key {
1223 location_key(file!(), line!(), column!())
1224}
1225
1226#[cfg(test)]
1227#[path = "tests/app_shell_tests.rs"]
1228mod tests;