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 pointer_scrolled(&mut self, delta_x: f32, delta_y: f32) -> bool {
513 enter_event_handler();
514 let result = run_in_mutable_snapshot(|| self.pointer_scrolled_inner(delta_x, delta_y))
515 .unwrap_or(false);
516 exit_event_handler();
517 if input_pipeline_debug_enabled() {
518 eprintln!(
519 "[CRANPOSE_INPUT_DEBUG] pointer_scrolled ({:.2},{:.2}) -> {}",
520 delta_x, delta_y, result
521 );
522 }
523 result
524 }
525
526 fn pointer_scrolled_inner(&mut self, delta_x: f32, delta_y: f32) -> bool {
527 if delta_x.abs() <= f32::EPSILON && delta_y.abs() <= f32::EPSILON {
528 return false;
529 }
530
531 let hits = self.renderer.scene().hit_test(self.cursor.0, self.cursor.1);
532 if hits.is_empty() {
533 return false;
534 }
535
536 let event = PointerEvent::new(
537 PointerEventKind::Scroll,
538 Point {
539 x: self.cursor.0,
540 y: self.cursor.1,
541 },
542 Point {
543 x: self.cursor.0,
544 y: self.cursor.1,
545 },
546 )
547 .with_buttons(self.buttons_pressed)
548 .with_scroll_delta(Point {
549 x: delta_x,
550 y: delta_y,
551 });
552
553 for hit in hits {
554 hit.dispatch(event.clone());
555 if event.is_consumed() {
556 break;
557 }
558 }
559
560 event.is_consumed()
561 }
562
563 pub fn cancel_gesture(&mut self) {
569 enter_event_handler();
570 let _ = run_in_mutable_snapshot(|| {
571 self.cancel_gesture_inner();
572 });
573 exit_event_handler();
574 }
575
576 fn cancel_gesture_inner(&mut self) {
577 let targets = self.resolve_hit_path(PointerId::PRIMARY);
579
580 self.hit_path_tracker.clear();
582 self.buttons_pressed = PointerButtons::NONE;
583
584 if !targets.is_empty() {
585 let event = PointerEvent::new(
586 PointerEventKind::Cancel,
587 Point {
588 x: self.cursor.0,
589 y: self.cursor.1,
590 },
591 Point {
592 x: self.cursor.0,
593 y: self.cursor.1,
594 },
595 );
596
597 for hit in targets {
598 hit.dispatch(event.clone());
599 }
600 }
601 }
602 pub fn on_key_event(&mut self, event: &KeyEvent) -> bool {
609 enter_event_handler();
610 let result = self.on_key_event_inner(event);
611 exit_event_handler();
612 result
613 }
614
615 fn on_key_event_inner(&mut self, event: &KeyEvent) -> bool {
617 use KeyEventType::KeyDown;
618
619 if event.event_type == KeyDown && event.modifiers.command_or_ctrl() {
621 #[cfg(all(
624 not(target_arch = "wasm32"),
625 not(target_os = "android"),
626 not(target_os = "ios")
627 ))]
628 {
629 match event.key_code {
630 KeyCode::C => {
632 let text = self.on_copy();
634 if let (Some(text), Some(clipboard)) = (text, self.clipboard.as_mut()) {
635 let _ = clipboard.set_text(&text);
636 return true;
637 }
638 }
639 KeyCode::X => {
641 let text = self.on_cut();
643 if let (Some(text), Some(clipboard)) = (text, self.clipboard.as_mut()) {
644 let _ = clipboard.set_text(&text);
645 self.mark_dirty();
646 self.layout_dirty = true;
647 return true;
648 }
649 }
650 KeyCode::V => {
652 let text = self.clipboard.as_mut().and_then(|cb| cb.get_text().ok());
654 if let Some(text) = text {
655 if self.on_paste(&text) {
656 return true;
657 }
658 }
659 }
660 _ => {}
661 }
662 }
663 }
664
665 if !cranpose_ui::text_field_focus::has_focused_field() {
667 return false;
668 }
669
670 let handled = run_in_mutable_snapshot(|| {
674 cranpose_ui::text_field_focus::dispatch_key_event(event)
677 })
678 .unwrap_or(false);
679
680 if handled {
681 self.mark_dirty();
683 self.layout_dirty = true;
684 }
685
686 handled
687 }
688
689 pub fn on_paste(&mut self, text: &str) -> bool {
693 let handled =
697 run_in_mutable_snapshot(|| cranpose_ui::text_field_focus::dispatch_paste(text))
698 .unwrap_or(false);
699
700 if handled {
701 self.mark_dirty();
702 self.layout_dirty = true;
703 }
704
705 handled
706 }
707
708 pub fn on_copy(&mut self) -> Option<String> {
712 cranpose_ui::text_field_focus::dispatch_copy()
714 }
715
716 pub fn on_cut(&mut self) -> Option<String> {
720 let text = cranpose_ui::text_field_focus::dispatch_cut();
722
723 if text.is_some() {
724 self.mark_dirty();
725 self.layout_dirty = true;
726 }
727
728 text
729 }
730
731 #[cfg(all(target_os = "linux", not(target_arch = "wasm32")))]
735 pub fn set_primary_selection(&mut self, text: &str) {
736 use arboard::{LinuxClipboardKind, SetExtLinux};
737 if let Some(ref mut clipboard) = self.clipboard {
738 let result = clipboard
739 .set()
740 .clipboard(LinuxClipboardKind::Primary)
741 .text(text.to_string());
742 if let Err(e) = result {
743 log::debug!("Primary selection set failed: {:?}", e);
745 }
746 }
747 }
748
749 #[cfg(all(target_os = "linux", not(target_arch = "wasm32")))]
752 pub fn get_primary_selection(&mut self) -> Option<String> {
753 use arboard::{GetExtLinux, LinuxClipboardKind};
754 if let Some(ref mut clipboard) = self.clipboard {
755 clipboard
756 .get()
757 .clipboard(LinuxClipboardKind::Primary)
758 .text()
759 .ok()
760 } else {
761 None
762 }
763 }
764
765 #[cfg(all(
766 not(target_os = "linux"),
767 not(target_arch = "wasm32"),
768 not(target_os = "ios")
769 ))]
770 pub fn get_primary_selection(&mut self) -> Option<String> {
771 None
772 }
773
774 pub fn sync_selection_to_primary(&mut self) {
777 #[cfg(all(target_os = "linux", not(target_arch = "wasm32")))]
778 {
779 if let Some(text) = self.on_copy() {
780 self.set_primary_selection(&text);
781 }
782 }
783 }
784
785 pub fn on_ime_preedit(&mut self, text: &str, cursor: Option<(usize, usize)>) -> bool {
793 let handled = run_in_mutable_snapshot(|| {
795 cranpose_ui::text_field_focus::dispatch_ime_preedit(text, cursor)
796 })
797 .unwrap_or(false);
798
799 if handled {
800 self.mark_dirty();
801 self.layout_dirty = true;
803 }
804
805 handled
806 }
807
808 pub fn on_ime_delete_surrounding(&mut self, before_bytes: usize, after_bytes: usize) -> bool {
811 let handled = run_in_mutable_snapshot(|| {
812 cranpose_ui::text_field_focus::dispatch_delete_surrounding(before_bytes, after_bytes)
813 })
814 .unwrap_or(false);
815
816 if handled {
817 self.mark_dirty();
818 self.layout_dirty = true;
819 }
820
821 handled
822 }
823
824 pub fn log_debug_info(&mut self) {
825 println!("\n\n");
826 println!("════════════════════════════════════════════════════════");
827 println!(" DEBUG: CURRENT SCREEN STATE");
828 println!("════════════════════════════════════════════════════════");
829
830 if let Some(ref layout_tree) = self.layout_tree {
831 log_layout_tree(layout_tree);
832 let renderer = HeadlessRenderer::new();
833 let render_scene = renderer.render(layout_tree);
834 log_render_scene(&render_scene);
835 log_screen_summary(layout_tree, &render_scene);
836 } else {
837 println!("No layout available");
838 }
839
840 println!("════════════════════════════════════════════════════════");
841 println!("\n\n");
842 }
843
844 pub fn layout_tree(&self) -> Option<&LayoutTree> {
846 self.layout_tree.as_ref()
847 }
848
849 pub fn semantics_tree(&self) -> Option<&SemanticsTree> {
851 self.semantics_tree.as_ref()
852 }
853
854 pub fn set_semantics_enabled(&mut self, enabled: bool) {
855 if self.semantics_enabled == enabled {
856 return;
857 }
858 self.semantics_enabled = enabled;
859 if enabled {
860 self.layout_dirty = true;
861 self.mark_dirty();
862 } else {
863 self.semantics_tree = None;
864 }
865 }
866
867 fn process_frame(&mut self) {
868 fps_monitor::record_frame();
870
871 #[cfg(debug_assertions)]
872 let _frame_start = Instant::now();
873
874 self.run_layout_phase();
875
876 #[cfg(debug_assertions)]
877 let _after_layout = Instant::now();
878
879 self.run_dispatch_queues();
880
881 #[cfg(debug_assertions)]
882 let _after_dispatch = Instant::now();
883
884 self.run_render_phase();
885 }
886
887 fn run_layout_phase(&mut self) {
888 let repass_nodes = cranpose_ui::take_layout_repass_nodes();
895 let had_repass_nodes = !repass_nodes.is_empty();
896 if had_repass_nodes {
897 let root = self.composition.root();
898 let mut applier = self.composition.applier_mut();
899 for node_id in repass_nodes {
900 cranpose_core::bubble_measure_dirty(
903 &mut *applier as &mut dyn cranpose_core::Applier,
904 node_id,
905 );
906 cranpose_core::bubble_layout_dirty(
907 &mut *applier as &mut dyn cranpose_core::Applier,
908 node_id,
909 );
910 }
911
912 if let Some(root) = root {
917 if let Ok(node) = applier.get_mut(root) {
918 node.mark_needs_measure();
919 }
920 }
921
922 drop(applier);
923 self.layout_dirty = true;
924 }
925
926 let invalidation_requested = take_layout_invalidation();
944
945 if invalidation_requested && !had_repass_nodes {
952 cranpose_ui::layout::invalidate_all_layout_caches();
955
956 if let Some(root) = self.composition.root() {
959 let mut applier = self.composition.applier_mut();
960 if let Ok(node) = applier.get_mut(root) {
961 if let Some(layout_node) =
962 node.as_any_mut().downcast_mut::<cranpose_ui::LayoutNode>()
963 {
964 layout_node.mark_needs_measure();
965 layout_node.mark_needs_layout();
966 }
967 }
968 }
969 self.layout_dirty = true;
970 } else if invalidation_requested {
971 self.layout_dirty = true;
974 }
975
976 if !self.layout_dirty {
978 return;
979 }
980
981 let viewport_size = Size {
982 width: self.viewport.0,
983 height: self.viewport.1,
984 };
985 if let Some(root) = self.composition.root() {
986 let handle = self.composition.runtime_handle();
987 let mut applier = self.composition.applier_mut();
988 applier.set_runtime_handle(handle);
989
990 let tree_needs_layout_check = cranpose_ui::tree_needs_layout(&mut *applier, root)
993 .unwrap_or_else(|err| {
994 log::warn!(
995 "Cannot check layout dirty status for root #{}: {}",
996 root,
997 err
998 );
999 true });
1001
1002 let needs_layout = tree_needs_layout_check || self.layout_dirty;
1006
1007 if !needs_layout {
1008 log::trace!("Skipping layout: tree is clean");
1010 self.layout_dirty = false;
1011 applier.clear_runtime_handle();
1012 return;
1013 }
1014
1015 self.layout_dirty = false;
1017
1018 match cranpose_ui::measure_layout_with_options(
1020 &mut applier,
1021 root,
1022 viewport_size,
1023 MeasureLayoutOptions {
1024 collect_semantics: self.semantics_enabled,
1025 },
1026 ) {
1027 Ok(measurements) => {
1028 self.semantics_tree = measurements.semantics_tree().cloned();
1029 self.layout_tree = Some(measurements.into_layout_tree());
1030 self.scene_dirty = true;
1031 }
1032 Err(err) => {
1033 log::error!("failed to compute layout: {err}");
1034 self.layout_tree = None;
1035 self.semantics_tree = None;
1036 self.scene_dirty = true;
1037 }
1038 }
1039 applier.clear_runtime_handle();
1040 } else {
1041 self.layout_tree = None;
1042 self.semantics_tree = None;
1043 self.scene_dirty = true;
1044 self.layout_dirty = false;
1045 }
1046 }
1047
1048 fn run_dispatch_queues(&mut self) {
1049 if has_pending_pointer_repasses() {
1053 let mut applier = self.composition.applier_mut();
1054 process_pointer_repasses(|node_id| {
1055 match clear_dispatch_invalidation(
1056 &mut applier,
1057 node_id,
1058 DispatchInvalidationKind::Pointer,
1059 ) {
1060 Ok(true) => {
1061 log::trace!("Cleared pointer repass flag for node #{}", node_id);
1062 }
1063 Ok(false) => {}
1064 Err(err) => {
1065 log::debug!(
1066 "Could not process pointer repass for node #{}: {}",
1067 node_id,
1068 err
1069 );
1070 }
1071 }
1072 });
1073 }
1074
1075 if has_pending_focus_invalidations() {
1079 let mut applier = self.composition.applier_mut();
1080 process_focus_invalidations(|node_id| {
1081 match clear_dispatch_invalidation(
1082 &mut applier,
1083 node_id,
1084 DispatchInvalidationKind::Focus,
1085 ) {
1086 Ok(true) => {
1087 log::trace!("Cleared focus sync flag for node #{}", node_id);
1088 }
1089 Ok(false) => {}
1090 Err(err) => {
1091 log::debug!(
1092 "Could not process focus invalidation for node #{}: {}",
1093 node_id,
1094 err
1095 );
1096 }
1097 }
1098 });
1099 }
1100 }
1101
1102 fn refresh_draw_repasses(&mut self) {
1103 let dirty_nodes = take_draw_repass_nodes();
1104 if dirty_nodes.is_empty() {
1105 return;
1106 }
1107
1108 let Some(layout_tree) = self.layout_tree.as_mut() else {
1109 return;
1110 };
1111
1112 let dirty_set: HashSet<NodeId> = dirty_nodes.into_iter().collect();
1113 let mut applier = self.composition.applier_mut();
1114 let refresh_scope = build_draw_refresh_scope(&mut applier, &dirty_set);
1115 refresh_layout_box_data(
1116 &mut applier,
1117 layout_tree.root_mut(),
1118 &refresh_scope,
1119 &dirty_set,
1120 );
1121 }
1122
1123 fn run_render_phase(&mut self) {
1124 let render_dirty = take_render_invalidation();
1125 take_pointer_invalidation();
1126 take_focus_invalidation();
1127 let draw_repass_pending = cranpose_ui::has_pending_draw_repasses();
1128 let cursor_blink_dirty = cranpose_ui::tick_cursor_blink();
1130
1131 let render_only_dirty = render_dirty || cursor_blink_dirty;
1132 let needs_scene_rebuild = self.scene_dirty || draw_repass_pending || render_only_dirty;
1135
1136 if !needs_scene_rebuild {
1137 return;
1138 }
1139 self.scene_dirty = false;
1140 self.refresh_draw_repasses();
1141 let viewport_size = Size {
1142 width: self.viewport.0,
1143 height: self.viewport.1,
1144 };
1145
1146 if let Some(root) = self.composition.root() {
1148 let mut applier = self.composition.applier_mut();
1149 if let Err(err) =
1150 self.renderer
1151 .rebuild_scene_from_applier(&mut applier, root, viewport_size)
1152 {
1153 log::error!("renderer rebuild failed: {err:?}");
1155 self.renderer.scene_mut().clear();
1156 }
1157 } else {
1158 self.renderer.scene_mut().clear();
1159 }
1160
1161 if self.dev_options.fps_counter {
1163 let stats = fps_monitor::fps_stats();
1164 let text = format!(
1165 "{:.0} FPS | {:.1}ms | {} recomp/s",
1166 stats.fps, stats.avg_ms, stats.recomps_per_second
1167 );
1168 self.renderer.draw_dev_overlay(&text, viewport_size);
1169 }
1170 }
1171}
1172
1173fn clear_dispatch_invalidation(
1174 applier: &mut MemoryApplier,
1175 node_id: NodeId,
1176 invalidation: DispatchInvalidationKind,
1177) -> Result<bool, NodeError> {
1178 match invalidation {
1179 DispatchInvalidationKind::Pointer => {
1180 match applier.with_node::<LayoutNode, _>(node_id, |node| {
1181 let needs_pointer_pass = node.needs_pointer_pass();
1182 if needs_pointer_pass {
1183 node.clear_needs_pointer_pass();
1184 }
1185 needs_pointer_pass
1186 }) {
1187 Ok(cleared) => Ok(cleared),
1188 Err(NodeError::TypeMismatch { .. }) => applier
1189 .with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
1190 let needs_pointer_pass = node.needs_pointer_pass();
1191 if needs_pointer_pass {
1192 node.clear_needs_pointer_pass();
1193 }
1194 needs_pointer_pass
1195 }),
1196 Err(err) => Err(err),
1197 }
1198 }
1199 DispatchInvalidationKind::Focus => {
1200 match applier.with_node::<LayoutNode, _>(node_id, |node| {
1201 let needs_focus_sync = node.needs_focus_sync();
1202 if needs_focus_sync {
1203 node.clear_needs_focus_sync();
1204 }
1205 needs_focus_sync
1206 }) {
1207 Ok(cleared) => Ok(cleared),
1208 Err(NodeError::TypeMismatch { .. }) => applier
1209 .with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
1210 let needs_focus_sync = node.needs_focus_sync();
1211 if needs_focus_sync {
1212 node.clear_needs_focus_sync();
1213 }
1214 needs_focus_sync
1215 }),
1216 Err(err) => Err(err),
1217 }
1218 }
1219 }
1220}
1221
1222fn build_draw_refresh_scope(
1223 applier: &mut MemoryApplier,
1224 dirty_nodes: &HashSet<NodeId>,
1225) -> HashSet<NodeId> {
1226 let mut refresh_scope = HashSet::with_capacity(dirty_nodes.len());
1227 for &dirty_node in dirty_nodes {
1228 let mut current = Some(dirty_node);
1229 while let Some(node_id) = current {
1230 if !refresh_scope.insert(node_id) {
1231 break;
1232 }
1233 current = applier.get_mut(node_id).ok().and_then(|node| node.parent());
1234 }
1235 }
1236 refresh_scope
1237}
1238
1239fn refresh_layout_box_data(
1240 applier: &mut MemoryApplier,
1241 layout: &mut cranpose_ui::layout::LayoutBox,
1242 refresh_scope: &HashSet<NodeId>,
1243 dirty_nodes: &HashSet<NodeId>,
1244) {
1245 if !refresh_scope.contains(&layout.node_id) {
1246 return;
1247 }
1248
1249 if dirty_nodes.contains(&layout.node_id) {
1250 if let Ok((modifier, resolved_modifiers, slices)) =
1251 applier.with_node::<LayoutNode, _>(layout.node_id, |node| {
1252 node.clear_needs_redraw();
1253 (
1254 node.modifier.clone(),
1255 node.resolved_modifiers(),
1256 node.modifier_slices_snapshot(),
1257 )
1258 })
1259 {
1260 layout.node_data.modifier = modifier;
1261 layout.node_data.resolved_modifiers = resolved_modifiers;
1262 layout.node_data.modifier_slices = slices;
1263 } else if let Ok((modifier, resolved_modifiers)) = applier
1264 .with_node::<SubcomposeLayoutNode, _>(layout.node_id, |node| {
1265 node.clear_needs_redraw();
1266 (node.modifier(), node.resolved_modifiers())
1267 })
1268 {
1269 layout.node_data.modifier = modifier.clone();
1270 layout.node_data.resolved_modifiers = resolved_modifiers;
1271 layout.node_data.modifier_slices =
1272 std::rc::Rc::new(cranpose_ui::collect_slices_from_modifier(&modifier));
1273 }
1274 }
1275
1276 for child in &mut layout.children {
1277 refresh_layout_box_data(applier, child, refresh_scope, dirty_nodes);
1278 }
1279}
1280
1281impl<R> Drop for AppShell<R>
1282where
1283 R: Renderer,
1284{
1285 fn drop(&mut self) {
1286 self.runtime.clear_frame_waker();
1287 }
1288}
1289
1290pub fn default_root_key() -> Key {
1291 location_key(file!(), line!(), column!())
1292}
1293
1294#[cfg(test)]
1295#[path = "tests/app_shell_tests.rs"]
1296mod tests;