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(
73 not(target_arch = "wasm32"),
74 not(target_os = "android"),
75 not(target_os = "ios")
76 ))]
77 clipboard: Option<arboard::Clipboard>,
78 dev_options: DevOptions,
80}
81
82#[derive(Clone, Debug, Default)]
87pub struct DevOptions {
88 pub fps_counter: bool,
90 pub recomposition_counter: bool,
92 pub layout_timing: bool,
94}
95
96fn input_pipeline_debug_enabled() -> bool {
97 static ENABLED: OnceLock<bool> = OnceLock::new();
98 *ENABLED.get_or_init(|| std::env::var_os("CRANPOSE_INPUT_DEBUG").is_some())
99}
100
101impl<R> AppShell<R>
102where
103 R: Renderer,
104 R::Error: Debug,
105{
106 pub fn new(mut renderer: R, root_key: Key, content: impl FnMut() + 'static) -> Self {
107 fps_monitor::init_fps_tracker();
109
110 let runtime = StdRuntime::new();
111 let mut composition = Composition::with_runtime(MemoryApplier::new(), runtime.runtime());
112 let build = content;
113 if let Err(err) = composition.render(root_key, build) {
114 log::error!("initial render failed: {err}");
115 }
116 renderer.scene_mut().clear();
117 let mut shell = Self {
118 runtime,
119 composition,
120 renderer,
121 cursor: (0.0, 0.0),
122 viewport: (800.0, 600.0),
123 buffer_size: (800, 600),
124 start_time: Instant::now(),
125 layout_tree: None,
126 semantics_tree: None,
127 semantics_enabled: false,
128 layout_dirty: true,
129 scene_dirty: true,
130 is_dirty: true,
131 buttons_pressed: PointerButtons::NONE,
132 hit_path_tracker: HitPathTracker::new(),
133 #[cfg(all(
134 not(target_arch = "wasm32"),
135 not(target_os = "android"),
136 not(target_os = "ios")
137 ))]
138 clipboard: arboard::Clipboard::new().ok(),
139 dev_options: DevOptions::default(),
140 };
141 shell.process_frame();
142 shell
143 }
144
145 pub fn set_dev_options(&mut self, options: DevOptions) {
150 self.dev_options = options;
151 }
152
153 pub fn dev_options(&self) -> &DevOptions {
155 &self.dev_options
156 }
157
158 pub fn set_viewport(&mut self, width: f32, height: f32) {
159 self.viewport = (width, height);
160 self.layout_dirty = true;
161 self.mark_dirty();
162 self.process_frame();
163 }
164
165 pub fn set_buffer_size(&mut self, width: u32, height: u32) {
166 self.buffer_size = (width, height);
167 }
168
169 pub fn buffer_size(&self) -> (u32, u32) {
170 self.buffer_size
171 }
172
173 pub fn scene(&self) -> &R::Scene {
174 self.renderer.scene()
175 }
176
177 pub fn renderer(&mut self) -> &mut R {
178 &mut self.renderer
179 }
180
181 #[cfg(not(target_arch = "wasm32"))]
182 pub fn set_frame_waker(&mut self, waker: impl Fn() + Send + Sync + 'static) {
183 self.runtime.set_frame_waker(waker);
184 }
185
186 #[cfg(target_arch = "wasm32")]
187 pub fn set_frame_waker(&mut self, waker: impl Fn() + Send + 'static) {
188 self.runtime.set_frame_waker(waker);
189 }
190
191 pub fn clear_frame_waker(&mut self) {
192 self.runtime.clear_frame_waker();
193 }
194
195 pub fn should_render(&self) -> bool {
196 if self.layout_dirty
197 || self.scene_dirty
198 || peek_render_invalidation()
199 || peek_pointer_invalidation()
200 || peek_focus_invalidation()
201 || peek_layout_invalidation()
202 {
203 return true;
204 }
205 self.runtime.take_frame_request() || self.composition.should_render()
206 }
207
208 pub fn needs_redraw(&self) -> bool {
211 if self.is_dirty
212 || self.layout_dirty
213 || self.scene_dirty
214 || peek_render_invalidation()
215 || peek_pointer_invalidation()
216 || peek_focus_invalidation()
217 || peek_layout_invalidation()
218 || cranpose_ui::has_pending_layout_repasses()
219 || cranpose_ui::has_pending_draw_repasses()
220 || has_pending_pointer_repasses()
221 || has_pending_focus_invalidations()
222 {
223 return true;
224 }
225
226 self.composition.should_render()
227 }
228
229 pub fn mark_dirty(&mut self) {
231 self.is_dirty = true;
232 }
233
234 pub fn has_active_animations(&self) -> bool {
236 self.runtime.take_frame_request() || self.composition.should_render()
237 }
238
239 pub fn next_event_time(&self) -> Option<web_time::Instant> {
242 cranpose_ui::next_cursor_blink_time()
243 }
244
245 fn resolve_hit_path(
252 &self,
253 pointer: PointerId,
254 ) -> Vec<<<R as Renderer>::Scene as RenderScene>::HitTarget> {
255 let Some(node_ids) = self.hit_path_tracker.get_path(pointer) else {
256 return Vec::new();
257 };
258
259 let scene = self.renderer.scene();
260 node_ids
261 .iter()
262 .filter_map(|&id| scene.find_target(id))
263 .collect()
264 }
265
266 pub fn update(&mut self) {
267 let now = Instant::now();
268 let frame_time = now
269 .checked_duration_since(self.start_time)
270 .unwrap_or_default()
271 .as_nanos() as u64;
272 self.runtime.drain_frame_callbacks(frame_time);
273 self.runtime.runtime_handle().drain_ui();
274 let should_render = self.composition.should_render();
275 if input_pipeline_debug_enabled() && should_render {
276 eprintln!(
277 "[CRANPOSE_INPUT_DEBUG] update begin: should_render=true layout_dirty={} scene_dirty={} is_dirty={}",
278 self.layout_dirty, self.scene_dirty, self.is_dirty
279 );
280 }
281 if should_render {
282 match self.composition.process_invalid_scopes() {
283 Ok(changed) => {
284 if input_pipeline_debug_enabled() {
285 eprintln!(
286 "[CRANPOSE_INPUT_DEBUG] process_invalid_scopes changed={}",
287 changed
288 );
289 }
290 if changed {
291 fps_monitor::record_recomposition();
292 self.layout_dirty = true;
293 if let Some(root_id) = self.composition.root() {
296 let _ = self.composition.applier_mut().with_node::<LayoutNode, _>(
297 root_id,
298 |node| {
299 node.mark_needs_measure();
300 },
301 );
302 }
303 request_render_invalidation();
304 }
305 }
306 Err(NodeError::Missing { id }) => {
307 log::debug!("Recomposition skipped: node {} no longer exists", id);
310 self.layout_dirty = true;
311 request_render_invalidation();
312 }
313 Err(err) => {
314 log::error!("recomposition failed: {err}");
315 self.layout_dirty = true;
316 request_render_invalidation();
317 }
318 }
319 }
320 self.process_frame();
321 self.is_dirty = false;
323 }
324
325 pub fn set_cursor(&mut self, x: f32, y: f32) -> bool {
326 enter_event_handler();
327 let result = run_in_mutable_snapshot(|| self.set_cursor_inner(x, y)).unwrap_or(false);
328 exit_event_handler();
329 if input_pipeline_debug_enabled() {
330 eprintln!(
331 "[CRANPOSE_INPUT_DEBUG] set_cursor ({:.2},{:.2}) -> {}",
332 x, y, result
333 );
334 }
335 result
336 }
337
338 fn set_cursor_inner(&mut self, x: f32, y: f32) -> bool {
339 self.cursor = (x, y);
340
341 if self.buttons_pressed != PointerButtons::NONE {
345 if self.hit_path_tracker.has_path(PointerId::PRIMARY) {
346 let targets = self.resolve_hit_path(PointerId::PRIMARY);
348
349 if !targets.is_empty() {
350 let event =
351 PointerEvent::new(PointerEventKind::Move, Point { x, y }, Point { x, y })
352 .with_buttons(self.buttons_pressed);
353
354 for hit in targets {
355 hit.dispatch(event.clone());
356 if event.is_consumed() {
357 break;
358 }
359 }
360 return true;
361 }
362
363 let hits = self.renderer.scene().hit_test(x, y);
366 if !hits.is_empty() {
367 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 let event =
371 PointerEvent::new(PointerEventKind::Move, Point { x, y }, Point { x, y })
372 .with_buttons(self.buttons_pressed);
373 for hit in hits {
374 hit.dispatch(event.clone());
375 if event.is_consumed() {
376 break;
377 }
378 }
379 return true;
380 }
381 return false;
382 }
383
384 return false;
387 }
388
389 let hits = self.renderer.scene().hit_test(x, y);
391 if !hits.is_empty() {
392 let event = PointerEvent::new(PointerEventKind::Move, Point { x, y }, Point { x, y })
393 .with_buttons(self.buttons_pressed); for hit in hits {
395 hit.dispatch(event.clone());
396 if event.is_consumed() {
397 break;
398 }
399 }
400 true
401 } else {
402 false
403 }
404 }
405
406 pub fn pointer_pressed(&mut self) -> bool {
407 enter_event_handler();
408 let result = run_in_mutable_snapshot(|| self.pointer_pressed_inner()).unwrap_or(false);
409 exit_event_handler();
410 if input_pipeline_debug_enabled() {
411 eprintln!("[CRANPOSE_INPUT_DEBUG] pointer_pressed -> {}", result);
412 }
413 result
414 }
415
416 fn pointer_pressed_inner(&mut self) -> bool {
417 self.buttons_pressed.insert(PointerButton::Primary);
419
420 let hits = self.renderer.scene().hit_test(self.cursor.0, self.cursor.1);
428
429 let node_ids: Vec<_> = hits.iter().map(|h| h.node_id()).collect();
431 self.hit_path_tracker
432 .add_hit_path(PointerId::PRIMARY, node_ids);
433
434 if !hits.is_empty() {
435 let event = PointerEvent::new(
436 PointerEventKind::Down,
437 Point {
438 x: self.cursor.0,
439 y: self.cursor.1,
440 },
441 Point {
442 x: self.cursor.0,
443 y: self.cursor.1,
444 },
445 )
446 .with_buttons(self.buttons_pressed);
447
448 for hit in hits {
450 hit.dispatch(event.clone());
451 if event.is_consumed() {
452 break;
453 }
454 }
455 true
456 } else {
457 false
458 }
459 }
460
461 pub fn pointer_released(&mut self) -> bool {
462 enter_event_handler();
463 let result = run_in_mutable_snapshot(|| self.pointer_released_inner()).unwrap_or(false);
464 exit_event_handler();
465 if input_pipeline_debug_enabled() {
466 eprintln!("[CRANPOSE_INPUT_DEBUG] pointer_released -> {}", result);
467 }
468 result
469 }
470
471 fn pointer_released_inner(&mut self) -> bool {
472 self.buttons_pressed.remove(PointerButton::Primary);
475 let corrected_buttons = self.buttons_pressed;
476
477 let targets = self.resolve_hit_path(PointerId::PRIMARY);
479
480 self.hit_path_tracker.remove_path(PointerId::PRIMARY);
482
483 if !targets.is_empty() {
484 let event = PointerEvent::new(
485 PointerEventKind::Up,
486 Point {
487 x: self.cursor.0,
488 y: self.cursor.1,
489 },
490 Point {
491 x: self.cursor.0,
492 y: self.cursor.1,
493 },
494 )
495 .with_buttons(corrected_buttons);
496
497 for hit in targets {
498 hit.dispatch(event.clone());
499 if event.is_consumed() {
500 break;
501 }
502 }
503 true
504 } else {
505 false
506 }
507 }
508
509 pub fn cancel_gesture(&mut self) {
515 enter_event_handler();
516 let _ = run_in_mutable_snapshot(|| {
517 self.cancel_gesture_inner();
518 });
519 exit_event_handler();
520 }
521
522 fn cancel_gesture_inner(&mut self) {
523 let targets = self.resolve_hit_path(PointerId::PRIMARY);
525
526 self.hit_path_tracker.clear();
528 self.buttons_pressed = PointerButtons::NONE;
529
530 if !targets.is_empty() {
531 let event = PointerEvent::new(
532 PointerEventKind::Cancel,
533 Point {
534 x: self.cursor.0,
535 y: self.cursor.1,
536 },
537 Point {
538 x: self.cursor.0,
539 y: self.cursor.1,
540 },
541 );
542
543 for hit in targets {
544 hit.dispatch(event.clone());
545 }
546 }
547 }
548 pub fn on_key_event(&mut self, event: &KeyEvent) -> bool {
555 enter_event_handler();
556 let result = self.on_key_event_inner(event);
557 exit_event_handler();
558 result
559 }
560
561 fn on_key_event_inner(&mut self, event: &KeyEvent) -> bool {
563 use KeyEventType::KeyDown;
564
565 if event.event_type == KeyDown && event.modifiers.command_or_ctrl() {
567 #[cfg(all(
570 not(target_arch = "wasm32"),
571 not(target_os = "android"),
572 not(target_os = "ios")
573 ))]
574 {
575 match event.key_code {
576 KeyCode::C => {
578 let text = self.on_copy();
580 if let (Some(text), Some(clipboard)) = (text, self.clipboard.as_mut()) {
581 let _ = clipboard.set_text(&text);
582 return true;
583 }
584 }
585 KeyCode::X => {
587 let text = self.on_cut();
589 if let (Some(text), Some(clipboard)) = (text, self.clipboard.as_mut()) {
590 let _ = clipboard.set_text(&text);
591 self.mark_dirty();
592 self.layout_dirty = true;
593 return true;
594 }
595 }
596 KeyCode::V => {
598 let text = self.clipboard.as_mut().and_then(|cb| cb.get_text().ok());
600 if let Some(text) = text {
601 if self.on_paste(&text) {
602 return true;
603 }
604 }
605 }
606 _ => {}
607 }
608 }
609 }
610
611 if !cranpose_ui::text_field_focus::has_focused_field() {
613 return false;
614 }
615
616 let handled = run_in_mutable_snapshot(|| {
620 cranpose_ui::text_field_focus::dispatch_key_event(event)
623 })
624 .unwrap_or(false);
625
626 if handled {
627 self.mark_dirty();
629 self.layout_dirty = true;
630 }
631
632 handled
633 }
634
635 pub fn on_paste(&mut self, text: &str) -> bool {
639 let handled =
643 run_in_mutable_snapshot(|| cranpose_ui::text_field_focus::dispatch_paste(text))
644 .unwrap_or(false);
645
646 if handled {
647 self.mark_dirty();
648 self.layout_dirty = true;
649 }
650
651 handled
652 }
653
654 pub fn on_copy(&mut self) -> Option<String> {
658 cranpose_ui::text_field_focus::dispatch_copy()
660 }
661
662 pub fn on_cut(&mut self) -> Option<String> {
666 let text = cranpose_ui::text_field_focus::dispatch_cut();
668
669 if text.is_some() {
670 self.mark_dirty();
671 self.layout_dirty = true;
672 }
673
674 text
675 }
676
677 #[cfg(all(target_os = "linux", not(target_arch = "wasm32")))]
681 pub fn set_primary_selection(&mut self, text: &str) {
682 use arboard::{LinuxClipboardKind, SetExtLinux};
683 if let Some(ref mut clipboard) = self.clipboard {
684 let result = clipboard
685 .set()
686 .clipboard(LinuxClipboardKind::Primary)
687 .text(text.to_string());
688 if let Err(e) = result {
689 log::debug!("Primary selection set failed: {:?}", e);
691 }
692 }
693 }
694
695 #[cfg(all(target_os = "linux", not(target_arch = "wasm32")))]
698 pub fn get_primary_selection(&mut self) -> Option<String> {
699 use arboard::{GetExtLinux, LinuxClipboardKind};
700 if let Some(ref mut clipboard) = self.clipboard {
701 clipboard
702 .get()
703 .clipboard(LinuxClipboardKind::Primary)
704 .text()
705 .ok()
706 } else {
707 None
708 }
709 }
710
711 #[cfg(all(
712 not(target_os = "linux"),
713 not(target_arch = "wasm32"),
714 not(target_os = "ios")
715 ))]
716 pub fn get_primary_selection(&mut self) -> Option<String> {
717 None
718 }
719
720 pub fn sync_selection_to_primary(&mut self) {
723 #[cfg(all(target_os = "linux", not(target_arch = "wasm32")))]
724 {
725 if let Some(text) = self.on_copy() {
726 self.set_primary_selection(&text);
727 }
728 }
729 }
730
731 pub fn on_ime_preedit(&mut self, text: &str, cursor: Option<(usize, usize)>) -> bool {
739 let handled = run_in_mutable_snapshot(|| {
741 cranpose_ui::text_field_focus::dispatch_ime_preedit(text, cursor)
742 })
743 .unwrap_or(false);
744
745 if handled {
746 self.mark_dirty();
747 self.layout_dirty = true;
749 }
750
751 handled
752 }
753
754 pub fn on_ime_delete_surrounding(&mut self, before_bytes: usize, after_bytes: usize) -> bool {
757 let handled = run_in_mutable_snapshot(|| {
758 cranpose_ui::text_field_focus::dispatch_delete_surrounding(before_bytes, after_bytes)
759 })
760 .unwrap_or(false);
761
762 if handled {
763 self.mark_dirty();
764 self.layout_dirty = true;
765 }
766
767 handled
768 }
769
770 pub fn log_debug_info(&mut self) {
771 println!("\n\n");
772 println!("════════════════════════════════════════════════════════");
773 println!(" DEBUG: CURRENT SCREEN STATE");
774 println!("════════════════════════════════════════════════════════");
775
776 if let Some(ref layout_tree) = self.layout_tree {
777 log_layout_tree(layout_tree);
778 let renderer = HeadlessRenderer::new();
779 let render_scene = renderer.render(layout_tree);
780 log_render_scene(&render_scene);
781 log_screen_summary(layout_tree, &render_scene);
782 } else {
783 println!("No layout available");
784 }
785
786 println!("════════════════════════════════════════════════════════");
787 println!("\n\n");
788 }
789
790 pub fn layout_tree(&self) -> Option<&LayoutTree> {
792 self.layout_tree.as_ref()
793 }
794
795 pub fn semantics_tree(&self) -> Option<&SemanticsTree> {
797 self.semantics_tree.as_ref()
798 }
799
800 pub fn set_semantics_enabled(&mut self, enabled: bool) {
801 if self.semantics_enabled == enabled {
802 return;
803 }
804 self.semantics_enabled = enabled;
805 if enabled {
806 self.layout_dirty = true;
807 self.mark_dirty();
808 } else {
809 self.semantics_tree = None;
810 }
811 }
812
813 fn process_frame(&mut self) {
814 fps_monitor::record_frame();
816
817 #[cfg(debug_assertions)]
818 let _frame_start = Instant::now();
819
820 self.run_layout_phase();
821
822 #[cfg(debug_assertions)]
823 let _after_layout = Instant::now();
824
825 self.run_dispatch_queues();
826
827 #[cfg(debug_assertions)]
828 let _after_dispatch = Instant::now();
829
830 self.run_render_phase();
831 }
832
833 fn run_layout_phase(&mut self) {
834 let repass_nodes = cranpose_ui::take_layout_repass_nodes();
841 let had_repass_nodes = !repass_nodes.is_empty();
842 if had_repass_nodes {
843 let root = self.composition.root();
844 let mut applier = self.composition.applier_mut();
845 for node_id in repass_nodes {
846 cranpose_core::bubble_measure_dirty(
849 &mut *applier as &mut dyn cranpose_core::Applier,
850 node_id,
851 );
852 cranpose_core::bubble_layout_dirty(
853 &mut *applier as &mut dyn cranpose_core::Applier,
854 node_id,
855 );
856 }
857
858 if let Some(root) = root {
863 if let Ok(node) = applier.get_mut(root) {
864 node.mark_needs_measure();
865 }
866 }
867
868 drop(applier);
869 self.layout_dirty = true;
870 }
871
872 let invalidation_requested = take_layout_invalidation();
890
891 if invalidation_requested && !had_repass_nodes {
898 cranpose_ui::layout::invalidate_all_layout_caches();
901
902 if let Some(root) = self.composition.root() {
905 let mut applier = self.composition.applier_mut();
906 if let Ok(node) = applier.get_mut(root) {
907 if let Some(layout_node) =
908 node.as_any_mut().downcast_mut::<cranpose_ui::LayoutNode>()
909 {
910 layout_node.mark_needs_measure();
911 layout_node.mark_needs_layout();
912 }
913 }
914 }
915 self.layout_dirty = true;
916 } else if invalidation_requested {
917 self.layout_dirty = true;
920 }
921
922 if !self.layout_dirty {
924 return;
925 }
926
927 let viewport_size = Size {
928 width: self.viewport.0,
929 height: self.viewport.1,
930 };
931 if let Some(root) = self.composition.root() {
932 let handle = self.composition.runtime_handle();
933 let mut applier = self.composition.applier_mut();
934 applier.set_runtime_handle(handle);
935
936 let tree_needs_layout_check = cranpose_ui::tree_needs_layout(&mut *applier, root)
939 .unwrap_or_else(|err| {
940 log::warn!(
941 "Cannot check layout dirty status for root #{}: {}",
942 root,
943 err
944 );
945 true });
947
948 let needs_layout = tree_needs_layout_check || self.layout_dirty;
952
953 if !needs_layout {
954 log::trace!("Skipping layout: tree is clean");
956 self.layout_dirty = false;
957 applier.clear_runtime_handle();
958 return;
959 }
960
961 self.layout_dirty = false;
963
964 match cranpose_ui::measure_layout_with_options(
966 &mut applier,
967 root,
968 viewport_size,
969 MeasureLayoutOptions {
970 collect_semantics: self.semantics_enabled,
971 },
972 ) {
973 Ok(measurements) => {
974 self.semantics_tree = measurements.semantics_tree().cloned();
975 self.layout_tree = Some(measurements.into_layout_tree());
976 self.scene_dirty = true;
977 }
978 Err(err) => {
979 log::error!("failed to compute layout: {err}");
980 self.layout_tree = None;
981 self.semantics_tree = None;
982 self.scene_dirty = true;
983 }
984 }
985 applier.clear_runtime_handle();
986 } else {
987 self.layout_tree = None;
988 self.semantics_tree = None;
989 self.scene_dirty = true;
990 self.layout_dirty = false;
991 }
992 }
993
994 fn run_dispatch_queues(&mut self) {
995 if has_pending_pointer_repasses() {
999 let mut applier = self.composition.applier_mut();
1000 process_pointer_repasses(|node_id| {
1001 match clear_dispatch_invalidation(
1002 &mut applier,
1003 node_id,
1004 DispatchInvalidationKind::Pointer,
1005 ) {
1006 Ok(true) => {
1007 log::trace!("Cleared pointer repass flag for node #{}", node_id);
1008 }
1009 Ok(false) => {}
1010 Err(err) => {
1011 log::debug!(
1012 "Could not process pointer repass for node #{}: {}",
1013 node_id,
1014 err
1015 );
1016 }
1017 }
1018 });
1019 }
1020
1021 if has_pending_focus_invalidations() {
1025 let mut applier = self.composition.applier_mut();
1026 process_focus_invalidations(|node_id| {
1027 match clear_dispatch_invalidation(
1028 &mut applier,
1029 node_id,
1030 DispatchInvalidationKind::Focus,
1031 ) {
1032 Ok(true) => {
1033 log::trace!("Cleared focus sync flag for node #{}", node_id);
1034 }
1035 Ok(false) => {}
1036 Err(err) => {
1037 log::debug!(
1038 "Could not process focus invalidation for node #{}: {}",
1039 node_id,
1040 err
1041 );
1042 }
1043 }
1044 });
1045 }
1046 }
1047
1048 fn refresh_draw_repasses(&mut self) {
1049 let dirty_nodes = take_draw_repass_nodes();
1050 if dirty_nodes.is_empty() {
1051 return;
1052 }
1053
1054 let Some(layout_tree) = self.layout_tree.as_mut() else {
1055 return;
1056 };
1057
1058 let dirty_set: HashSet<NodeId> = dirty_nodes.into_iter().collect();
1059 let mut applier = self.composition.applier_mut();
1060 let refresh_scope = build_draw_refresh_scope(&mut applier, &dirty_set);
1061 refresh_layout_box_data(
1062 &mut applier,
1063 layout_tree.root_mut(),
1064 &refresh_scope,
1065 &dirty_set,
1066 );
1067 }
1068
1069 fn run_render_phase(&mut self) {
1070 let render_dirty = take_render_invalidation();
1071 take_pointer_invalidation();
1072 take_focus_invalidation();
1073 let draw_repass_pending = cranpose_ui::has_pending_draw_repasses();
1074 let cursor_blink_dirty = cranpose_ui::tick_cursor_blink();
1076
1077 let render_only_dirty = render_dirty || cursor_blink_dirty;
1078 let needs_scene_rebuild = self.scene_dirty
1081 || draw_repass_pending
1082 || (self.dev_options.fps_counter && render_only_dirty);
1083
1084 if !needs_scene_rebuild {
1085 return;
1086 }
1087 self.scene_dirty = false;
1088 self.refresh_draw_repasses();
1089 let viewport_size = Size {
1090 width: self.viewport.0,
1091 height: self.viewport.1,
1092 };
1093
1094 if let Some(root) = self.composition.root() {
1096 let mut applier = self.composition.applier_mut();
1097 if let Err(err) =
1098 self.renderer
1099 .rebuild_scene_from_applier(&mut applier, root, viewport_size)
1100 {
1101 log::error!("renderer rebuild failed: {err:?}");
1103 self.renderer.scene_mut().clear();
1104 }
1105 } else {
1106 self.renderer.scene_mut().clear();
1107 }
1108
1109 if self.dev_options.fps_counter {
1111 let stats = fps_monitor::fps_stats();
1112 let text = format!(
1113 "{:.0} FPS | {:.1}ms | {} recomp/s",
1114 stats.fps, stats.avg_ms, stats.recomps_per_second
1115 );
1116 self.renderer.draw_dev_overlay(&text, viewport_size);
1117 }
1118 }
1119}
1120
1121fn clear_dispatch_invalidation(
1122 applier: &mut MemoryApplier,
1123 node_id: NodeId,
1124 invalidation: DispatchInvalidationKind,
1125) -> Result<bool, NodeError> {
1126 match invalidation {
1127 DispatchInvalidationKind::Pointer => {
1128 match applier.with_node::<LayoutNode, _>(node_id, |node| {
1129 let needs_pointer_pass = node.needs_pointer_pass();
1130 if needs_pointer_pass {
1131 node.clear_needs_pointer_pass();
1132 }
1133 needs_pointer_pass
1134 }) {
1135 Ok(cleared) => Ok(cleared),
1136 Err(NodeError::TypeMismatch { .. }) => applier
1137 .with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
1138 let needs_pointer_pass = node.needs_pointer_pass();
1139 if needs_pointer_pass {
1140 node.clear_needs_pointer_pass();
1141 }
1142 needs_pointer_pass
1143 }),
1144 Err(err) => Err(err),
1145 }
1146 }
1147 DispatchInvalidationKind::Focus => {
1148 match applier.with_node::<LayoutNode, _>(node_id, |node| {
1149 let needs_focus_sync = node.needs_focus_sync();
1150 if needs_focus_sync {
1151 node.clear_needs_focus_sync();
1152 }
1153 needs_focus_sync
1154 }) {
1155 Ok(cleared) => Ok(cleared),
1156 Err(NodeError::TypeMismatch { .. }) => applier
1157 .with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
1158 let needs_focus_sync = node.needs_focus_sync();
1159 if needs_focus_sync {
1160 node.clear_needs_focus_sync();
1161 }
1162 needs_focus_sync
1163 }),
1164 Err(err) => Err(err),
1165 }
1166 }
1167 }
1168}
1169
1170fn build_draw_refresh_scope(
1171 applier: &mut MemoryApplier,
1172 dirty_nodes: &HashSet<NodeId>,
1173) -> HashSet<NodeId> {
1174 let mut refresh_scope = HashSet::with_capacity(dirty_nodes.len());
1175 for &dirty_node in dirty_nodes {
1176 let mut current = Some(dirty_node);
1177 while let Some(node_id) = current {
1178 if !refresh_scope.insert(node_id) {
1179 break;
1180 }
1181 current = applier.get_mut(node_id).ok().and_then(|node| node.parent());
1182 }
1183 }
1184 refresh_scope
1185}
1186
1187fn refresh_layout_box_data(
1188 applier: &mut MemoryApplier,
1189 layout: &mut cranpose_ui::layout::LayoutBox,
1190 refresh_scope: &HashSet<NodeId>,
1191 dirty_nodes: &HashSet<NodeId>,
1192) {
1193 if !refresh_scope.contains(&layout.node_id) {
1194 return;
1195 }
1196
1197 if dirty_nodes.contains(&layout.node_id) {
1198 if let Ok((modifier, resolved_modifiers, slices)) =
1199 applier.with_node::<LayoutNode, _>(layout.node_id, |node| {
1200 node.clear_needs_redraw();
1201 (
1202 node.modifier.clone(),
1203 node.resolved_modifiers(),
1204 node.modifier_slices_snapshot(),
1205 )
1206 })
1207 {
1208 layout.node_data.modifier = modifier;
1209 layout.node_data.resolved_modifiers = resolved_modifiers;
1210 layout.node_data.modifier_slices = slices;
1211 } else if let Ok((modifier, resolved_modifiers)) = applier
1212 .with_node::<SubcomposeLayoutNode, _>(layout.node_id, |node| {
1213 node.clear_needs_redraw();
1214 (node.modifier(), node.resolved_modifiers())
1215 })
1216 {
1217 layout.node_data.modifier = modifier.clone();
1218 layout.node_data.resolved_modifiers = resolved_modifiers;
1219 layout.node_data.modifier_slices =
1220 std::rc::Rc::new(cranpose_ui::collect_slices_from_modifier(&modifier));
1221 }
1222 }
1223
1224 for child in &mut layout.children {
1225 refresh_layout_box_data(applier, child, refresh_scope, dirty_nodes);
1226 }
1227}
1228
1229impl<R> Drop for AppShell<R>
1230where
1231 R: Renderer,
1232{
1233 fn drop(&mut self) {
1234 self.runtime.clear_frame_waker();
1235 }
1236}
1237
1238pub fn default_root_key() -> Key {
1239 location_key(file!(), line!(), column!())
1240}
1241
1242#[cfg(test)]
1243#[path = "tests/app_shell_tests.rs"]
1244mod tests;