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 hovered_nodes: Vec<NodeId>,
74 #[cfg(all(
76 not(target_arch = "wasm32"),
77 not(target_os = "android"),
78 not(target_os = "ios")
79 ))]
80 clipboard: Option<arboard::Clipboard>,
81 dev_options: DevOptions,
83}
84
85#[derive(Clone, Debug, Default)]
90pub struct DevOptions {
91 pub fps_counter: bool,
93 pub recomposition_counter: bool,
95 pub layout_timing: bool,
97}
98
99fn input_pipeline_debug_enabled() -> bool {
100 static ENABLED: OnceLock<bool> = OnceLock::new();
101 *ENABLED.get_or_init(|| std::env::var_os("CRANPOSE_INPUT_DEBUG").is_some())
102}
103
104impl<R> AppShell<R>
105where
106 R: Renderer,
107 R::Error: Debug,
108{
109 pub fn new(mut renderer: R, root_key: Key, content: impl FnMut() + 'static) -> Self {
110 fps_monitor::init_fps_tracker();
112
113 let runtime = StdRuntime::new();
114 let mut composition = Composition::with_runtime(MemoryApplier::new(), runtime.runtime());
115 let build = content;
116 if let Err(err) = composition.render(root_key, build) {
117 log::error!("initial render failed: {err}");
118 }
119 renderer.scene_mut().clear();
120 let mut shell = Self {
121 runtime,
122 composition,
123 renderer,
124 cursor: (0.0, 0.0),
125 viewport: (800.0, 600.0),
126 buffer_size: (800, 600),
127 start_time: Instant::now(),
128 layout_tree: None,
129 semantics_tree: None,
130 semantics_enabled: false,
131 layout_dirty: true,
132 scene_dirty: true,
133 is_dirty: true,
134 buttons_pressed: PointerButtons::NONE,
135 hit_path_tracker: HitPathTracker::new(),
136 hovered_nodes: Vec::new(),
137 #[cfg(all(
138 not(target_arch = "wasm32"),
139 not(target_os = "android"),
140 not(target_os = "ios")
141 ))]
142 clipboard: arboard::Clipboard::new().ok(),
143 dev_options: DevOptions::default(),
144 };
145 shell.process_frame();
146 shell
147 }
148
149 pub fn set_dev_options(&mut self, options: DevOptions) {
154 self.dev_options = options;
155 }
156
157 pub fn dev_options(&self) -> &DevOptions {
159 &self.dev_options
160 }
161
162 pub fn set_viewport(&mut self, width: f32, height: f32) {
163 self.viewport = (width, height);
164 self.layout_dirty = true;
165 self.mark_dirty();
166 self.process_frame();
167 }
168
169 pub fn set_buffer_size(&mut self, width: u32, height: u32) {
170 self.buffer_size = (width, height);
171 }
172
173 pub fn buffer_size(&self) -> (u32, u32) {
174 self.buffer_size
175 }
176
177 pub fn scene(&self) -> &R::Scene {
178 self.renderer.scene()
179 }
180
181 pub fn renderer(&mut self) -> &mut R {
182 &mut self.renderer
183 }
184
185 #[cfg(not(target_arch = "wasm32"))]
186 pub fn set_frame_waker(&mut self, waker: impl Fn() + Send + Sync + 'static) {
187 self.runtime.set_frame_waker(waker);
188 }
189
190 #[cfg(target_arch = "wasm32")]
191 pub fn set_frame_waker(&mut self, waker: impl Fn() + Send + 'static) {
192 self.runtime.set_frame_waker(waker);
193 }
194
195 pub fn clear_frame_waker(&mut self) {
196 self.runtime.clear_frame_waker();
197 }
198
199 pub fn should_render(&self) -> bool {
200 if self.layout_dirty
201 || self.scene_dirty
202 || peek_render_invalidation()
203 || peek_pointer_invalidation()
204 || peek_focus_invalidation()
205 || peek_layout_invalidation()
206 {
207 return true;
208 }
209 self.runtime.take_frame_request() || self.composition.should_render()
210 }
211
212 pub fn needs_redraw(&self) -> bool {
215 if self.is_dirty
216 || self.layout_dirty
217 || self.scene_dirty
218 || peek_render_invalidation()
219 || peek_pointer_invalidation()
220 || peek_focus_invalidation()
221 || peek_layout_invalidation()
222 || cranpose_ui::has_pending_layout_repasses()
223 || cranpose_ui::has_pending_draw_repasses()
224 || has_pending_pointer_repasses()
225 || has_pending_focus_invalidations()
226 {
227 return true;
228 }
229
230 self.composition.should_render()
231 }
232
233 pub fn mark_dirty(&mut self) {
235 self.is_dirty = true;
236 }
237
238 pub fn has_active_animations(&self) -> bool {
240 self.runtime.take_frame_request() || self.composition.should_render()
241 }
242
243 pub fn next_event_time(&self) -> Option<web_time::Instant> {
246 cranpose_ui::next_cursor_blink_time()
247 }
248
249 fn resolve_hit_path(
256 &self,
257 pointer: PointerId,
258 ) -> Vec<<<R as Renderer>::Scene as RenderScene>::HitTarget> {
259 let Some(node_ids) = self.hit_path_tracker.get_path(pointer) else {
260 return Vec::new();
261 };
262
263 let scene = self.renderer.scene();
264 node_ids
265 .iter()
266 .filter_map(|&id| scene.find_target(id))
267 .collect()
268 }
269
270 pub fn update(&mut self) {
271 let now = Instant::now();
272 let frame_time = now
273 .checked_duration_since(self.start_time)
274 .unwrap_or_default()
275 .as_nanos() as u64;
276 self.runtime.drain_frame_callbacks(frame_time);
277 self.runtime.runtime_handle().drain_ui();
278 let should_render = self.composition.should_render();
279 if input_pipeline_debug_enabled() && should_render {
280 eprintln!(
281 "[CRANPOSE_INPUT_DEBUG] update begin: should_render=true layout_dirty={} scene_dirty={} is_dirty={}",
282 self.layout_dirty, self.scene_dirty, self.is_dirty
283 );
284 }
285 if should_render {
286 match self.composition.process_invalid_scopes() {
287 Ok(changed) => {
288 if input_pipeline_debug_enabled() {
289 eprintln!(
290 "[CRANPOSE_INPUT_DEBUG] process_invalid_scopes changed={}",
291 changed
292 );
293 }
294 if changed {
295 fps_monitor::record_recomposition();
296 self.layout_dirty = true;
297 if let Some(root_id) = self.composition.root() {
300 let _ = self.composition.applier_mut().with_node::<LayoutNode, _>(
301 root_id,
302 |node| {
303 node.mark_needs_measure();
304 },
305 );
306 }
307 request_render_invalidation();
308 }
309 }
310 Err(NodeError::Missing { id }) => {
311 log::debug!("Recomposition skipped: node {} no longer exists", id);
314 self.layout_dirty = true;
315 request_render_invalidation();
316 }
317 Err(err) => {
318 log::error!("recomposition failed: {err}");
319 self.layout_dirty = true;
320 request_render_invalidation();
321 }
322 }
323 }
324 self.process_frame();
325 self.is_dirty = false;
327 }
328
329 pub fn set_cursor(&mut self, x: f32, y: f32) -> bool {
330 enter_event_handler();
331 let result = run_in_mutable_snapshot(|| self.set_cursor_inner(x, y)).unwrap_or(false);
332 exit_event_handler();
333 if input_pipeline_debug_enabled() {
334 eprintln!(
335 "[CRANPOSE_INPUT_DEBUG] set_cursor ({:.2},{:.2}) -> {}",
336 x, y, result
337 );
338 }
339 result
340 }
341
342 fn set_cursor_inner(&mut self, x: f32, y: f32) -> bool {
343 self.cursor = (x, y);
344
345 if self.buttons_pressed != PointerButtons::NONE {
349 if self.hit_path_tracker.has_path(PointerId::PRIMARY) {
350 let targets = self.resolve_hit_path(PointerId::PRIMARY);
352
353 if !targets.is_empty() {
354 let event =
355 PointerEvent::new(PointerEventKind::Move, Point { x, y }, Point { x, y })
356 .with_buttons(self.buttons_pressed);
357
358 for hit in targets {
359 hit.dispatch(event.clone());
360 if event.is_consumed() {
361 break;
362 }
363 }
364 return true;
365 }
366
367 let hits = self.renderer.scene().hit_test(x, y);
370 if !hits.is_empty() {
371 let node_ids: Vec<_> = hits.iter().map(|h| h.node_id()).collect();
372 self.hit_path_tracker
373 .add_hit_path(PointerId::PRIMARY, node_ids);
374 let event =
375 PointerEvent::new(PointerEventKind::Move, Point { x, y }, Point { x, y })
376 .with_buttons(self.buttons_pressed);
377 for hit in hits {
378 hit.dispatch(event.clone());
379 if event.is_consumed() {
380 break;
381 }
382 }
383 return true;
384 }
385 return false;
386 }
387
388 return false;
391 }
392
393 let hits = self.renderer.scene().hit_test(x, y);
396 let new_ids: Vec<NodeId> = hits.iter().map(|h| h.node_id()).collect();
397
398 let pos = Point { x, y };
400 for &old_id in &self.hovered_nodes {
401 if !new_ids.contains(&old_id) {
402 if let Some(target) = self.renderer.scene().find_target(old_id) {
403 let exit_event = PointerEvent::new(PointerEventKind::Exit, pos, pos)
404 .with_buttons(self.buttons_pressed);
405 target.dispatch(exit_event);
406 }
407 }
408 }
409
410 for hit in &hits {
412 if !self.hovered_nodes.contains(&hit.node_id()) {
413 let enter_event = PointerEvent::new(PointerEventKind::Enter, pos, pos)
414 .with_buttons(self.buttons_pressed);
415 hit.dispatch(enter_event);
416 }
417 }
418
419 self.hovered_nodes = new_ids;
420
421 if !hits.is_empty() {
422 let event = PointerEvent::new(PointerEventKind::Move, pos, pos)
423 .with_buttons(self.buttons_pressed);
424 for hit in hits {
425 hit.dispatch(event.clone());
426 if event.is_consumed() {
427 break;
428 }
429 }
430 true
431 } else {
432 false
433 }
434 }
435
436 pub fn pointer_pressed(&mut self) -> bool {
437 enter_event_handler();
438 let result = run_in_mutable_snapshot(|| self.pointer_pressed_inner()).unwrap_or(false);
439 exit_event_handler();
440 if input_pipeline_debug_enabled() {
441 eprintln!("[CRANPOSE_INPUT_DEBUG] pointer_pressed -> {}", result);
442 }
443 result
444 }
445
446 fn pointer_pressed_inner(&mut self) -> bool {
447 self.buttons_pressed.insert(PointerButton::Primary);
449
450 let hits = self.renderer.scene().hit_test(self.cursor.0, self.cursor.1);
458
459 let node_ids: Vec<_> = hits.iter().map(|h| h.node_id()).collect();
461 self.hit_path_tracker
462 .add_hit_path(PointerId::PRIMARY, node_ids);
463
464 if !hits.is_empty() {
465 let event = PointerEvent::new(
466 PointerEventKind::Down,
467 Point {
468 x: self.cursor.0,
469 y: self.cursor.1,
470 },
471 Point {
472 x: self.cursor.0,
473 y: self.cursor.1,
474 },
475 )
476 .with_buttons(self.buttons_pressed);
477
478 for hit in hits {
480 hit.dispatch(event.clone());
481 if event.is_consumed() {
482 break;
483 }
484 }
485 true
486 } else {
487 false
488 }
489 }
490
491 pub fn pointer_released(&mut self) -> bool {
492 enter_event_handler();
493 let result = run_in_mutable_snapshot(|| self.pointer_released_inner()).unwrap_or(false);
494 exit_event_handler();
495 if input_pipeline_debug_enabled() {
496 eprintln!("[CRANPOSE_INPUT_DEBUG] pointer_released -> {}", result);
497 }
498 result
499 }
500
501 fn pointer_released_inner(&mut self) -> bool {
502 self.buttons_pressed.remove(PointerButton::Primary);
505 let corrected_buttons = self.buttons_pressed;
506
507 let targets = self.resolve_hit_path(PointerId::PRIMARY);
509
510 self.hit_path_tracker.remove_path(PointerId::PRIMARY);
512
513 if !targets.is_empty() {
514 let event = PointerEvent::new(
515 PointerEventKind::Up,
516 Point {
517 x: self.cursor.0,
518 y: self.cursor.1,
519 },
520 Point {
521 x: self.cursor.0,
522 y: self.cursor.1,
523 },
524 )
525 .with_buttons(corrected_buttons);
526
527 for hit in targets {
528 hit.dispatch(event.clone());
529 if event.is_consumed() {
530 break;
531 }
532 }
533 true
534 } else {
535 false
536 }
537 }
538
539 pub fn pointer_scrolled(&mut self, delta_x: f32, delta_y: f32) -> bool {
543 enter_event_handler();
544 let result = run_in_mutable_snapshot(|| self.pointer_scrolled_inner(delta_x, delta_y))
545 .unwrap_or(false);
546 exit_event_handler();
547 if input_pipeline_debug_enabled() {
548 eprintln!(
549 "[CRANPOSE_INPUT_DEBUG] pointer_scrolled ({:.2},{:.2}) -> {}",
550 delta_x, delta_y, result
551 );
552 }
553 result
554 }
555
556 fn pointer_scrolled_inner(&mut self, delta_x: f32, delta_y: f32) -> bool {
557 if delta_x.abs() <= f32::EPSILON && delta_y.abs() <= f32::EPSILON {
558 return false;
559 }
560
561 let hits = self.renderer.scene().hit_test(self.cursor.0, self.cursor.1);
562 if hits.is_empty() {
563 return false;
564 }
565
566 let event = PointerEvent::new(
567 PointerEventKind::Scroll,
568 Point {
569 x: self.cursor.0,
570 y: self.cursor.1,
571 },
572 Point {
573 x: self.cursor.0,
574 y: self.cursor.1,
575 },
576 )
577 .with_buttons(self.buttons_pressed)
578 .with_scroll_delta(Point {
579 x: delta_x,
580 y: delta_y,
581 });
582
583 for hit in hits {
584 hit.dispatch(event.clone());
585 if event.is_consumed() {
586 break;
587 }
588 }
589
590 event.is_consumed()
591 }
592
593 pub fn cancel_gesture(&mut self) {
599 enter_event_handler();
600 let _ = run_in_mutable_snapshot(|| {
601 self.cancel_gesture_inner();
602 });
603 exit_event_handler();
604 }
605
606 fn cancel_gesture_inner(&mut self) {
607 let targets = self.resolve_hit_path(PointerId::PRIMARY);
609
610 self.hit_path_tracker.clear();
612 self.buttons_pressed = PointerButtons::NONE;
613
614 if !targets.is_empty() {
615 let event = PointerEvent::new(
616 PointerEventKind::Cancel,
617 Point {
618 x: self.cursor.0,
619 y: self.cursor.1,
620 },
621 Point {
622 x: self.cursor.0,
623 y: self.cursor.1,
624 },
625 );
626
627 for hit in targets {
628 hit.dispatch(event.clone());
629 }
630 }
631
632 let pos = Point {
634 x: self.cursor.0,
635 y: self.cursor.1,
636 };
637 for &node_id in &self.hovered_nodes {
638 if let Some(target) = self.renderer.scene().find_target(node_id) {
639 let exit_event = PointerEvent::new(PointerEventKind::Exit, pos, pos);
640 target.dispatch(exit_event);
641 }
642 }
643 self.hovered_nodes.clear();
644 }
645 pub fn on_key_event(&mut self, event: &KeyEvent) -> bool {
652 enter_event_handler();
653 let result = self.on_key_event_inner(event);
654 exit_event_handler();
655 result
656 }
657
658 fn on_key_event_inner(&mut self, event: &KeyEvent) -> bool {
660 use KeyEventType::KeyDown;
661
662 if event.event_type == KeyDown && event.modifiers.command_or_ctrl() {
664 #[cfg(all(
667 not(target_arch = "wasm32"),
668 not(target_os = "android"),
669 not(target_os = "ios")
670 ))]
671 {
672 match event.key_code {
673 KeyCode::C => {
675 let text = self.on_copy();
677 if let (Some(text), Some(clipboard)) = (text, self.clipboard.as_mut()) {
678 let _ = clipboard.set_text(&text);
679 return true;
680 }
681 }
682 KeyCode::X => {
684 let text = self.on_cut();
686 if let (Some(text), Some(clipboard)) = (text, self.clipboard.as_mut()) {
687 let _ = clipboard.set_text(&text);
688 self.mark_dirty();
689 self.layout_dirty = true;
690 return true;
691 }
692 }
693 KeyCode::V => {
695 let text = self.clipboard.as_mut().and_then(|cb| cb.get_text().ok());
697 if let Some(text) = text {
698 if self.on_paste(&text) {
699 return true;
700 }
701 }
702 }
703 _ => {}
704 }
705 }
706 }
707
708 if !cranpose_ui::text_field_focus::has_focused_field() {
710 return false;
711 }
712
713 let handled = run_in_mutable_snapshot(|| {
717 cranpose_ui::text_field_focus::dispatch_key_event(event)
720 })
721 .unwrap_or(false);
722
723 if handled {
724 self.mark_dirty();
726 self.layout_dirty = true;
727 }
728
729 handled
730 }
731
732 pub fn on_paste(&mut self, text: &str) -> bool {
736 let handled =
740 run_in_mutable_snapshot(|| cranpose_ui::text_field_focus::dispatch_paste(text))
741 .unwrap_or(false);
742
743 if handled {
744 self.mark_dirty();
745 self.layout_dirty = true;
746 }
747
748 handled
749 }
750
751 pub fn on_copy(&mut self) -> Option<String> {
755 cranpose_ui::text_field_focus::dispatch_copy()
757 }
758
759 pub fn on_cut(&mut self) -> Option<String> {
763 let text = cranpose_ui::text_field_focus::dispatch_cut();
765
766 if text.is_some() {
767 self.mark_dirty();
768 self.layout_dirty = true;
769 }
770
771 text
772 }
773
774 #[cfg(all(target_os = "linux", not(target_arch = "wasm32")))]
778 pub fn set_primary_selection(&mut self, text: &str) {
779 use arboard::{LinuxClipboardKind, SetExtLinux};
780 if let Some(ref mut clipboard) = self.clipboard {
781 let result = clipboard
782 .set()
783 .clipboard(LinuxClipboardKind::Primary)
784 .text(text.to_string());
785 if let Err(e) = result {
786 log::debug!("Primary selection set failed: {:?}", e);
788 }
789 }
790 }
791
792 #[cfg(all(target_os = "linux", not(target_arch = "wasm32")))]
795 pub fn get_primary_selection(&mut self) -> Option<String> {
796 use arboard::{GetExtLinux, LinuxClipboardKind};
797 if let Some(ref mut clipboard) = self.clipboard {
798 clipboard
799 .get()
800 .clipboard(LinuxClipboardKind::Primary)
801 .text()
802 .ok()
803 } else {
804 None
805 }
806 }
807
808 #[cfg(all(
809 not(target_os = "linux"),
810 not(target_arch = "wasm32"),
811 not(target_os = "ios")
812 ))]
813 pub fn get_primary_selection(&mut self) -> Option<String> {
814 None
815 }
816
817 pub fn sync_selection_to_primary(&mut self) {
820 #[cfg(all(target_os = "linux", not(target_arch = "wasm32")))]
821 {
822 if let Some(text) = self.on_copy() {
823 self.set_primary_selection(&text);
824 }
825 }
826 }
827
828 pub fn on_ime_preedit(&mut self, text: &str, cursor: Option<(usize, usize)>) -> bool {
836 let handled = run_in_mutable_snapshot(|| {
838 cranpose_ui::text_field_focus::dispatch_ime_preedit(text, cursor)
839 })
840 .unwrap_or(false);
841
842 if handled {
843 self.mark_dirty();
844 self.layout_dirty = true;
846 }
847
848 handled
849 }
850
851 pub fn on_ime_delete_surrounding(&mut self, before_bytes: usize, after_bytes: usize) -> bool {
854 let handled = run_in_mutable_snapshot(|| {
855 cranpose_ui::text_field_focus::dispatch_delete_surrounding(before_bytes, after_bytes)
856 })
857 .unwrap_or(false);
858
859 if handled {
860 self.mark_dirty();
861 self.layout_dirty = true;
862 }
863
864 handled
865 }
866
867 pub fn log_debug_info(&mut self) {
868 println!("\n\n");
869 println!("════════════════════════════════════════════════════════");
870 println!(" DEBUG: CURRENT SCREEN STATE");
871 println!("════════════════════════════════════════════════════════");
872
873 if let Some(ref layout_tree) = self.layout_tree {
874 log_layout_tree(layout_tree);
875 let renderer = HeadlessRenderer::new();
876 let render_scene = renderer.render(layout_tree);
877 log_render_scene(&render_scene);
878 log_screen_summary(layout_tree, &render_scene);
879 } else {
880 println!("No layout available");
881 }
882
883 println!("════════════════════════════════════════════════════════");
884 println!("\n\n");
885 }
886
887 pub fn layout_tree(&self) -> Option<&LayoutTree> {
889 self.layout_tree.as_ref()
890 }
891
892 pub fn semantics_tree(&self) -> Option<&SemanticsTree> {
894 self.semantics_tree.as_ref()
895 }
896
897 pub fn set_semantics_enabled(&mut self, enabled: bool) {
898 if self.semantics_enabled == enabled {
899 return;
900 }
901 self.semantics_enabled = enabled;
902 if enabled {
903 self.layout_dirty = true;
904 self.mark_dirty();
905 } else {
906 self.semantics_tree = None;
907 }
908 }
909
910 fn process_frame(&mut self) {
911 fps_monitor::record_frame();
913
914 #[cfg(debug_assertions)]
915 let _frame_start = Instant::now();
916
917 self.run_layout_phase();
918
919 #[cfg(debug_assertions)]
920 let _after_layout = Instant::now();
921
922 self.run_dispatch_queues();
923
924 #[cfg(debug_assertions)]
925 let _after_dispatch = Instant::now();
926
927 self.run_render_phase();
928 }
929
930 fn run_layout_phase(&mut self) {
931 let repass_nodes = cranpose_ui::take_layout_repass_nodes();
938 let had_repass_nodes = !repass_nodes.is_empty();
939 if had_repass_nodes {
940 let root = self.composition.root();
941 let mut applier = self.composition.applier_mut();
942 for node_id in repass_nodes {
943 cranpose_core::bubble_measure_dirty(
946 &mut *applier as &mut dyn cranpose_core::Applier,
947 node_id,
948 );
949 cranpose_core::bubble_layout_dirty(
950 &mut *applier as &mut dyn cranpose_core::Applier,
951 node_id,
952 );
953 }
954
955 if let Some(root) = root {
960 if let Ok(node) = applier.get_mut(root) {
961 node.mark_needs_measure();
962 }
963 }
964
965 drop(applier);
966 self.layout_dirty = true;
967 }
968
969 let invalidation_requested = take_layout_invalidation();
987
988 if invalidation_requested && !had_repass_nodes {
995 cranpose_ui::layout::invalidate_all_layout_caches();
998
999 if let Some(root) = self.composition.root() {
1002 let mut applier = self.composition.applier_mut();
1003 if let Ok(node) = applier.get_mut(root) {
1004 if let Some(layout_node) =
1005 node.as_any_mut().downcast_mut::<cranpose_ui::LayoutNode>()
1006 {
1007 layout_node.mark_needs_measure();
1008 layout_node.mark_needs_layout();
1009 }
1010 }
1011 }
1012 self.layout_dirty = true;
1013 } else if invalidation_requested {
1014 self.layout_dirty = true;
1017 }
1018
1019 if !self.layout_dirty {
1021 return;
1022 }
1023
1024 let viewport_size = Size {
1025 width: self.viewport.0,
1026 height: self.viewport.1,
1027 };
1028 if let Some(root) = self.composition.root() {
1029 let handle = self.composition.runtime_handle();
1030 let mut applier = self.composition.applier_mut();
1031 applier.set_runtime_handle(handle);
1032
1033 let tree_needs_layout_check = cranpose_ui::tree_needs_layout(&mut *applier, root)
1036 .unwrap_or_else(|err| {
1037 log::warn!(
1038 "Cannot check layout dirty status for root #{}: {}",
1039 root,
1040 err
1041 );
1042 true });
1044
1045 let needs_layout = tree_needs_layout_check || self.layout_dirty;
1049
1050 if !needs_layout {
1051 log::trace!("Skipping layout: tree is clean");
1053 self.layout_dirty = false;
1054 applier.clear_runtime_handle();
1055 return;
1056 }
1057
1058 self.layout_dirty = false;
1060
1061 match cranpose_ui::measure_layout_with_options(
1063 &mut applier,
1064 root,
1065 viewport_size,
1066 MeasureLayoutOptions {
1067 collect_semantics: self.semantics_enabled,
1068 },
1069 ) {
1070 Ok(measurements) => {
1071 self.semantics_tree = measurements.semantics_tree().cloned();
1072 self.layout_tree = Some(measurements.into_layout_tree());
1073 self.scene_dirty = true;
1074 }
1075 Err(err) => {
1076 log::error!("failed to compute layout: {err}");
1077 self.layout_tree = None;
1078 self.semantics_tree = None;
1079 self.scene_dirty = true;
1080 }
1081 }
1082 applier.clear_runtime_handle();
1083 } else {
1084 self.layout_tree = None;
1085 self.semantics_tree = None;
1086 self.scene_dirty = true;
1087 self.layout_dirty = false;
1088 }
1089 }
1090
1091 fn run_dispatch_queues(&mut self) {
1092 if has_pending_pointer_repasses() {
1096 let mut applier = self.composition.applier_mut();
1097 process_pointer_repasses(|node_id| {
1098 match clear_dispatch_invalidation(
1099 &mut applier,
1100 node_id,
1101 DispatchInvalidationKind::Pointer,
1102 ) {
1103 Ok(true) => {
1104 log::trace!("Cleared pointer repass flag for node #{}", node_id);
1105 }
1106 Ok(false) => {}
1107 Err(err) => {
1108 log::debug!(
1109 "Could not process pointer repass for node #{}: {}",
1110 node_id,
1111 err
1112 );
1113 }
1114 }
1115 });
1116 }
1117
1118 if has_pending_focus_invalidations() {
1122 let mut applier = self.composition.applier_mut();
1123 process_focus_invalidations(|node_id| {
1124 match clear_dispatch_invalidation(
1125 &mut applier,
1126 node_id,
1127 DispatchInvalidationKind::Focus,
1128 ) {
1129 Ok(true) => {
1130 log::trace!("Cleared focus sync flag for node #{}", node_id);
1131 }
1132 Ok(false) => {}
1133 Err(err) => {
1134 log::debug!(
1135 "Could not process focus invalidation for node #{}: {}",
1136 node_id,
1137 err
1138 );
1139 }
1140 }
1141 });
1142 }
1143 }
1144
1145 fn refresh_draw_repasses(&mut self) {
1146 let dirty_nodes = take_draw_repass_nodes();
1147 if dirty_nodes.is_empty() {
1148 return;
1149 }
1150
1151 let Some(layout_tree) = self.layout_tree.as_mut() else {
1152 return;
1153 };
1154
1155 let dirty_set: HashSet<NodeId> = dirty_nodes.into_iter().collect();
1156 let mut applier = self.composition.applier_mut();
1157 let refresh_scope = build_draw_refresh_scope(&mut applier, &dirty_set);
1158 refresh_layout_box_data(
1159 &mut applier,
1160 layout_tree.root_mut(),
1161 &refresh_scope,
1162 &dirty_set,
1163 );
1164 }
1165
1166 fn run_render_phase(&mut self) {
1167 let render_dirty = take_render_invalidation();
1168 take_pointer_invalidation();
1169 take_focus_invalidation();
1170 let draw_repass_pending = cranpose_ui::has_pending_draw_repasses();
1171 let cursor_blink_dirty = cranpose_ui::tick_cursor_blink();
1173
1174 let render_only_dirty = render_dirty || cursor_blink_dirty;
1175 let needs_scene_rebuild = self.scene_dirty || draw_repass_pending || render_only_dirty;
1178
1179 if !needs_scene_rebuild {
1180 return;
1181 }
1182 self.scene_dirty = false;
1183 self.refresh_draw_repasses();
1184 let viewport_size = Size {
1185 width: self.viewport.0,
1186 height: self.viewport.1,
1187 };
1188
1189 if let Some(root) = self.composition.root() {
1191 let mut applier = self.composition.applier_mut();
1192 if let Err(err) =
1193 self.renderer
1194 .rebuild_scene_from_applier(&mut applier, root, viewport_size)
1195 {
1196 log::error!("renderer rebuild failed: {err:?}");
1198 self.renderer.scene_mut().clear();
1199 }
1200 } else {
1201 self.renderer.scene_mut().clear();
1202 }
1203
1204 if self.dev_options.fps_counter {
1206 let stats = fps_monitor::fps_stats();
1207 let text = format!(
1208 "{:.0} FPS | {:.1}ms | {} recomp/s",
1209 stats.fps, stats.avg_ms, stats.recomps_per_second
1210 );
1211 self.renderer.draw_dev_overlay(&text, viewport_size);
1212 }
1213 }
1214}
1215
1216fn clear_dispatch_invalidation(
1217 applier: &mut MemoryApplier,
1218 node_id: NodeId,
1219 invalidation: DispatchInvalidationKind,
1220) -> Result<bool, NodeError> {
1221 match invalidation {
1222 DispatchInvalidationKind::Pointer => {
1223 match applier.with_node::<LayoutNode, _>(node_id, |node| {
1224 let needs_pointer_pass = node.needs_pointer_pass();
1225 if needs_pointer_pass {
1226 node.clear_needs_pointer_pass();
1227 }
1228 needs_pointer_pass
1229 }) {
1230 Ok(cleared) => Ok(cleared),
1231 Err(NodeError::TypeMismatch { .. }) => applier
1232 .with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
1233 let needs_pointer_pass = node.needs_pointer_pass();
1234 if needs_pointer_pass {
1235 node.clear_needs_pointer_pass();
1236 }
1237 needs_pointer_pass
1238 }),
1239 Err(err) => Err(err),
1240 }
1241 }
1242 DispatchInvalidationKind::Focus => {
1243 match applier.with_node::<LayoutNode, _>(node_id, |node| {
1244 let needs_focus_sync = node.needs_focus_sync();
1245 if needs_focus_sync {
1246 node.clear_needs_focus_sync();
1247 }
1248 needs_focus_sync
1249 }) {
1250 Ok(cleared) => Ok(cleared),
1251 Err(NodeError::TypeMismatch { .. }) => applier
1252 .with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
1253 let needs_focus_sync = node.needs_focus_sync();
1254 if needs_focus_sync {
1255 node.clear_needs_focus_sync();
1256 }
1257 needs_focus_sync
1258 }),
1259 Err(err) => Err(err),
1260 }
1261 }
1262 }
1263}
1264
1265fn build_draw_refresh_scope(
1266 applier: &mut MemoryApplier,
1267 dirty_nodes: &HashSet<NodeId>,
1268) -> HashSet<NodeId> {
1269 let mut refresh_scope = HashSet::with_capacity(dirty_nodes.len());
1270 for &dirty_node in dirty_nodes {
1271 let mut current = Some(dirty_node);
1272 while let Some(node_id) = current {
1273 if !refresh_scope.insert(node_id) {
1274 break;
1275 }
1276 current = applier.get_mut(node_id).ok().and_then(|node| node.parent());
1277 }
1278 }
1279 refresh_scope
1280}
1281
1282fn refresh_layout_box_data(
1283 applier: &mut MemoryApplier,
1284 layout: &mut cranpose_ui::layout::LayoutBox,
1285 refresh_scope: &HashSet<NodeId>,
1286 dirty_nodes: &HashSet<NodeId>,
1287) {
1288 if !refresh_scope.contains(&layout.node_id) {
1289 return;
1290 }
1291
1292 if dirty_nodes.contains(&layout.node_id) {
1293 if let Ok((modifier, resolved_modifiers, slices)) =
1294 applier.with_node::<LayoutNode, _>(layout.node_id, |node| {
1295 node.clear_needs_redraw();
1296 (
1297 node.modifier.clone(),
1298 node.resolved_modifiers(),
1299 node.modifier_slices_snapshot(),
1300 )
1301 })
1302 {
1303 layout.node_data.modifier = modifier;
1304 layout.node_data.resolved_modifiers = resolved_modifiers;
1305 layout.node_data.modifier_slices = slices;
1306 } else if let Ok((modifier, resolved_modifiers)) = applier
1307 .with_node::<SubcomposeLayoutNode, _>(layout.node_id, |node| {
1308 node.clear_needs_redraw();
1309 (node.modifier(), node.resolved_modifiers())
1310 })
1311 {
1312 layout.node_data.modifier = modifier.clone();
1313 layout.node_data.resolved_modifiers = resolved_modifiers;
1314 layout.node_data.modifier_slices =
1315 std::rc::Rc::new(cranpose_ui::collect_slices_from_modifier(&modifier));
1316 }
1317 }
1318
1319 for child in &mut layout.children {
1320 refresh_layout_box_data(applier, child, refresh_scope, dirty_nodes);
1321 }
1322}
1323
1324impl<R> Drop for AppShell<R>
1325where
1326 R: Renderer,
1327{
1328 fn drop(&mut self) {
1329 self.runtime.clear_frame_waker();
1330 }
1331}
1332
1333pub fn default_root_key() -> Key {
1334 location_key(file!(), line!(), column!())
1335}
1336
1337#[cfg(test)]
1338#[path = "tests/app_shell_tests.rs"]
1339mod tests;