1use super::*;
2
3const DEV_OVERLAY_PADDING: f32 = 8.0;
4const DEV_OVERLAY_FONT_SIZE: f32 = 14.0;
5const DEV_OVERLAY_CHAR_WIDTH: f32 = 7.0;
6const DEV_OVERLAY_REFRESH_INTERVAL: std::time::Duration = std::time::Duration::from_millis(250);
7const DEFAULT_FRAME_STAGE_TELEMETRY_THRESHOLD_MS: f64 = 4.0;
8
9#[derive(Copy, Clone)]
10enum DispatchInvalidationKind {
11 Pointer,
12 Focus,
13}
14
15fn frame_stage_telemetry_threshold_ms() -> Option<f64> {
16 static THRESHOLD_MS: std::sync::OnceLock<Option<f64>> = std::sync::OnceLock::new();
17 *THRESHOLD_MS.get_or_init(|| {
18 let explicit = std::env::var("CRANPOSE_FRAME_STAGE_TELEMETRY_MS")
19 .ok()
20 .and_then(|value| value.parse::<f64>().ok())
21 .filter(|value| value.is_finite() && *value >= 0.0);
22 explicit.or_else(|| {
23 std::env::var_os("CRANPOSE_FRAME_STAGE_TELEMETRY")
24 .is_some()
25 .then_some(DEFAULT_FRAME_STAGE_TELEMETRY_THRESHOLD_MS)
26 })
27 })
28}
29
30fn log_frame_stage_telemetry(
31 frame_start: Instant,
32 after_initial_layout: Instant,
33 after_layout: Instant,
34 after_dispatch: Instant,
35 after_render: Instant,
36) {
37 let Some(threshold_ms) = frame_stage_telemetry_threshold_ms() else {
38 return;
39 };
40
41 let total_ms = after_render.duration_since(frame_start).as_secs_f64() * 1000.0;
42 if total_ms < threshold_ms {
43 return;
44 }
45
46 let layout_ms = after_layout.duration_since(frame_start).as_secs_f64() * 1000.0;
47 let initial_layout_ms = after_initial_layout
48 .duration_since(frame_start)
49 .as_secs_f64()
50 * 1000.0;
51 let post_layout_ms = after_layout
52 .duration_since(after_initial_layout)
53 .as_secs_f64()
54 * 1000.0;
55 let dispatch_ms = after_dispatch.duration_since(after_layout).as_secs_f64() * 1000.0;
56 let scene_ms = after_render.duration_since(after_dispatch).as_secs_f64() * 1000.0;
57 log::warn!(
58 "[frame-stage-telemetry] total_ms={total_ms:.2} layout_ms={layout_ms:.2} initial_layout_ms={initial_layout_ms:.2} post_layout_ms={post_layout_ms:.2} dispatch_ms={dispatch_ms:.2} scene_ms={scene_ms:.2}",
59 );
60}
61
62fn render_phase_dirty_diagnostics_enabled() -> bool {
63 cranpose_core::env_flag!("CRANPOSE_RENDER_PHASE_DIRTY_DIAG")
64}
65
66struct RenderPhaseDirtyDiagnostics<'a> {
67 render_dirty: bool,
68 pointer_dirty: bool,
69 scene_dirty: bool,
70 draw_repass_pending: bool,
71 draw_dirty_nodes: usize,
72 layout_dirty_nodes: usize,
73 structural_dirty_nodes: usize,
74 partial_dirty_nodes: usize,
75 dirty_node_ids: Option<String>,
76 render_only_dirty: bool,
77 recomposed_this_frame: bool,
78 path: &'a str,
79}
80
81fn log_render_phase_dirty_diagnostics(diagnostics: RenderPhaseDirtyDiagnostics<'_>) {
82 if !render_phase_dirty_diagnostics_enabled() {
83 return;
84 }
85
86 let RenderPhaseDirtyDiagnostics {
87 render_dirty,
88 pointer_dirty,
89 scene_dirty,
90 draw_repass_pending,
91 draw_dirty_nodes,
92 layout_dirty_nodes,
93 structural_dirty_nodes,
94 partial_dirty_nodes,
95 dirty_node_ids,
96 render_only_dirty,
97 recomposed_this_frame,
98 path,
99 } = diagnostics;
100 if let Some(dirty_node_ids) = dirty_node_ids {
101 log::warn!(
102 "[render-phase-dirty] path={path} render_dirty={render_dirty} pointer_dirty={pointer_dirty} scene_dirty={scene_dirty} draw_repass_pending={draw_repass_pending} draw_dirty_nodes={draw_dirty_nodes} layout_dirty_nodes={layout_dirty_nodes} structural_dirty_nodes={structural_dirty_nodes} partial_dirty_nodes={partial_dirty_nodes} render_only_dirty={render_only_dirty} recomposed_this_frame={recomposed_this_frame} ids={dirty_node_ids}",
103 );
104 } else {
105 log::warn!(
106 "[render-phase-dirty] path={path} render_dirty={render_dirty} pointer_dirty={pointer_dirty} scene_dirty={scene_dirty} draw_repass_pending={draw_repass_pending} draw_dirty_nodes={draw_dirty_nodes} layout_dirty_nodes={layout_dirty_nodes} structural_dirty_nodes={structural_dirty_nodes} partial_dirty_nodes={partial_dirty_nodes} render_only_dirty={render_only_dirty} recomposed_this_frame={recomposed_this_frame}",
107 );
108 }
109}
110
111impl<R> AppShell<R>
112where
113 R: Renderer,
114 R::Error: Debug,
115{
116 pub fn set_semantics_enabled(&mut self, enabled: bool) {
117 if self.semantics_enabled == enabled {
118 return;
119 }
120 self.semantics_enabled = enabled;
121 self.semantics_snapshot_revision = self.semantics_snapshot_revision.wrapping_add(1);
122 if enabled {
123 self.request_forced_layout_pass();
124 self.mark_dirty();
125 } else {
126 self.semantics_tree = None;
127 }
128 }
129
130 pub fn set_frame_rate_preference(&mut self, preference: crate::FrameRatePreference) {
135 self.frame_rate_preference = preference;
136 }
137
138 pub fn frame_rate_preference(&self) -> crate::FrameRatePreference {
142 self.frame_rate_preference
143 }
144
145 pub(crate) fn process_frame(&mut self) -> FrameUpdateResult {
146 let app_context = Rc::clone(&self.app_context);
147 app_context.enter(|| self.process_frame_in_context(false))
148 }
149
150 pub(crate) fn process_frame_in_context(
151 &mut self,
152 recomposed_before_frame: bool,
153 ) -> FrameUpdateResult {
154 let frame_start = Instant::now();
155
156 self.run_layout_phase();
157 let after_initial_layout = Instant::now();
158
159 let recomposed_this_frame = recomposed_before_frame || self.run_post_layout_recomposition();
160
161 let after_layout = Instant::now();
162
163 self.run_dispatch_queues();
164
165 let after_dispatch = Instant::now();
166
167 clear_transient_scroll_motion_contexts();
168
169 let result = self.run_render_phase_with_recomposition_state(recomposed_this_frame);
170 let after_render = Instant::now();
171 log_frame_stage_telemetry(
172 frame_start,
173 after_initial_layout,
174 after_layout,
175 after_dispatch,
176 after_render,
177 );
178 result
179 }
180
181 pub(crate) fn run_layout_phase(&mut self) {
182 let app_context = Rc::clone(&self.app_context);
183 app_context.enter(|| self.run_layout_phase_in_context());
184 }
185
186 fn run_layout_phase_in_context(&mut self) {
187 let has_scoped_repasses = cranpose_ui::has_pending_layout_repasses()
190 || cranpose_ui::has_pending_measure_repasses();
191 let mut scoped_layout_nodes = if cranpose_ui::has_pending_layout_repasses() {
199 cranpose_ui::pending_layout_repass_nodes_snapshot()
200 } else {
201 Vec::new()
202 };
203 if cranpose_ui::has_pending_measure_repasses() {
204 for node in cranpose_ui::pending_measure_repass_nodes_snapshot() {
205 if !scoped_layout_nodes.contains(&node) {
206 scoped_layout_nodes.push(node);
207 }
208 }
209 }
210
211 let invalidation_requested = take_layout_invalidation();
215 let global_layout_invalidation = invalidation_requested && !has_scoped_repasses;
216 let force_layout_pass = self.force_layout_pass;
217
218 if invalidation_requested && !has_scoped_repasses {
219 cranpose_ui::layout::invalidate_all_layout_caches();
220
221 if let Some(root) = self.composition.root() {
224 let mut applier = self.composition.applier_mut();
225 match applier.with_node::<LayoutNode, _>(root, |node| {
226 node.mark_needs_measure();
227 node.mark_needs_layout();
228 }) {
229 Ok(()) | Err(NodeError::Missing { .. }) => {}
230 Err(NodeError::TypeMismatch { .. }) => {
231 let _ = applier.with_node::<SubcomposeLayoutNode, _>(root, |node| {
232 node.mark_needs_measure();
233 node.mark_needs_layout_flag();
234 });
235 }
236 Err(_) => {}
237 }
238 }
239 self.request_forced_layout_pass();
240 } else if invalidation_requested || has_scoped_repasses {
241 self.request_layout_pass();
242 }
243
244 if !self.layout_requested {
245 return;
246 }
247
248 let viewport_size = Size {
249 width: self.viewport.0,
250 height: self.viewport.1,
251 };
252 if let Some(root) = self.composition.root() {
253 let handle = self.composition.runtime_handle();
254 let mut applier = self.composition.applier_mut();
255 applier.set_runtime_handle(handle);
256
257 let tree_needs_layout_check = cranpose_ui::tree_needs_layout(&mut *applier, root)
258 .unwrap_or_else(|err| {
259 log::warn!(
260 "Cannot check layout dirty status for root #{}: {}",
261 root,
262 err
263 );
264 true });
266
267 let needs_layout =
268 self.force_layout_pass || has_scoped_repasses || tree_needs_layout_check;
269
270 if !needs_layout {
271 log::trace!("Skipping layout: tree is clean");
272 self.layout_requested = false;
273 self.force_layout_pass = false;
274 applier.clear_runtime_handle();
275 return;
276 }
277
278 self.layout_requested = false;
279 self.force_layout_pass = false;
280
281 match cranpose_ui::measure_layout_with_options(
283 &mut applier,
284 root,
285 viewport_size,
286 MeasureLayoutOptions {
287 collect_semantics: false,
288 build_layout_tree: false,
289 },
290 ) {
291 Ok(_measurements) => {
292 self.layout_tree = None;
293 self.semantics_snapshot_revision =
294 self.semantics_snapshot_revision.wrapping_add(1);
295 if self.semantics_enabled {
296 self.semantics_tree = None;
297 }
298 if global_layout_invalidation || force_layout_pass {
307 self.scoped_layout_scene_nodes.clear();
308 let _ = cranpose_ui::take_geometry_scene_nodes();
309 } else {
310 let mut seen: HashSet<NodeId> =
313 self.scoped_layout_scene_nodes.iter().copied().collect();
314 if has_scoped_repasses {
315 for node in scoped_layout_nodes {
316 if seen.insert(node) {
317 self.scoped_layout_scene_nodes.push(node);
318 }
319 }
320 }
321 for node in cranpose_ui::take_geometry_scene_nodes() {
331 let Some(node) = applier.scene_node_attached_to(node, root) else {
332 continue;
333 };
334 if seen.insert(node) {
335 self.scoped_layout_scene_nodes.push(node);
336 }
337 }
338 }
339 self.scene_dirty = true;
340 }
341 Err(err) => {
342 log::error!("failed to compute layout: {err}");
343 self.layout_tree = None;
344 self.semantics_tree = None;
345 self.semantics_snapshot_revision =
346 self.semantics_snapshot_revision.wrapping_add(1);
347 self.scoped_layout_scene_nodes.clear();
348 let _ = cranpose_ui::take_geometry_scene_nodes();
349 self.scene_dirty = true;
350 }
351 }
352 applier.clear_runtime_handle();
353 } else {
354 self.layout_tree = None;
355 self.semantics_tree = None;
356 self.semantics_snapshot_revision = self.semantics_snapshot_revision.wrapping_add(1);
357 self.scoped_layout_scene_nodes.clear();
358 let _ = cranpose_ui::take_geometry_scene_nodes();
359 self.scene_dirty = true;
360 self.layout_requested = false;
361 self.force_layout_pass = false;
362 }
363 }
364
365 fn run_post_layout_recomposition(&mut self) -> bool {
366 if !self.composition.should_recompose() {
367 return false;
368 }
369
370 let Some(root_key) = self.composition.root_key() else {
371 return false;
372 };
373
374 match self.composition.reconcile(root_key, &mut *self.content) {
375 Ok(changed) => {
376 if !changed {
377 return false;
378 }
379 self.fps_monitor.record_recomposition();
380 if self.composition_tree_needs_layout() {
381 self.request_layout_pass();
382 self.run_layout_phase_in_context();
383 }
384 request_render_invalidation();
385 true
386 }
387 Err(NodeError::Missing { id }) => {
388 log::debug!(
389 "Post-layout recomposition skipped: node {} no longer exists",
390 id
391 );
392 self.request_layout_pass();
393 request_render_invalidation();
394 true
395 }
396 Err(err) => {
397 log::error!("post-layout recomposition failed: {err}");
398 self.request_layout_pass();
399 request_render_invalidation();
400 true
401 }
402 }
403 }
404
405 fn run_dispatch_queues(&mut self) {
406 if has_pending_pointer_repasses() {
410 let mut applier = self.composition.applier_mut();
411 process_pointer_repasses(|node_id| {
412 match clear_dispatch_invalidation(
413 &mut applier,
414 node_id,
415 DispatchInvalidationKind::Pointer,
416 ) {
417 Ok(true) => {
418 log::trace!("Cleared pointer repass flag for node #{}", node_id);
419 }
420 Ok(false) => {}
421 Err(err) => {
422 log::debug!(
423 "Could not process pointer repass for node #{}: {}",
424 node_id,
425 err
426 );
427 }
428 }
429 });
430 }
431
432 if has_pending_focus_invalidations() {
436 let mut applier = self.composition.applier_mut();
437 process_focus_invalidations(|node_id| {
438 match clear_dispatch_invalidation(
439 &mut applier,
440 node_id,
441 DispatchInvalidationKind::Focus,
442 ) {
443 Ok(true) => {
444 log::trace!("Cleared focus sync flag for node #{}", node_id);
445 }
446 Ok(false) => {}
447 Err(err) => {
448 log::debug!(
449 "Could not process focus invalidation for node #{}: {}",
450 node_id,
451 err
452 );
453 }
454 }
455 });
456 }
457
458 if has_pending_semantics_invalidations() {
467 let mut applier = self.composition.applier_mut();
468 process_semantics_invalidations(|node_id| {
469 cranpose_core::bubble_semantics_dirty(&mut *applier, node_id);
470 });
471 }
472 }
473
474 fn refresh_draw_repasses(&mut self) -> Vec<NodeId> {
475 let dirty_nodes = take_draw_repass_nodes();
476 if dirty_nodes.is_empty() {
477 return dirty_nodes;
478 }
479 self.refresh_draw_nodes(dirty_nodes)
480 }
481
482 fn refresh_retained_redraw_nodes(&mut self) -> Vec<NodeId> {
483 let Some(root) = self.composition.root() else {
484 return Vec::new();
485 };
486 let mut dirty_nodes = Vec::new();
487 {
488 let mut applier = self.composition.applier_mut();
489 collect_retained_redraw_nodes(&mut applier, root, &mut dirty_nodes);
490 }
491 if dirty_nodes.is_empty() {
492 return dirty_nodes;
493 }
494 self.refresh_draw_nodes(dirty_nodes)
495 }
496
497 fn refresh_draw_nodes(&mut self, dirty_nodes: Vec<NodeId>) -> Vec<NodeId> {
498 let Some(layout_tree) = self.layout_tree.as_mut() else {
499 return dirty_nodes;
500 };
501
502 let dirty_set: HashSet<NodeId> = dirty_nodes.into_iter().collect();
503 let mut applier = self.composition.applier_mut();
504 let refresh_scope = build_draw_refresh_scope(&mut applier, &dirty_set);
505 refresh_layout_box_data(
506 &mut applier,
507 layout_tree.root_mut(),
508 &refresh_scope,
509 &dirty_set,
510 );
511 dirty_set.into_iter().collect()
512 }
513
514 #[cfg(test)]
515 pub(crate) fn run_render_phase(&mut self) -> FrameUpdateResult {
516 let app_context = Rc::clone(&self.app_context);
517 app_context.enter(|| self.run_render_phase_in_context(false))
518 }
519
520 fn run_render_phase_with_recomposition_state(
521 &mut self,
522 recomposed_this_frame: bool,
523 ) -> FrameUpdateResult {
524 let app_context = Rc::clone(&self.app_context);
525 app_context.enter(|| self.run_render_phase_in_context(recomposed_this_frame))
526 }
527
528 fn run_render_phase_in_context(&mut self, recomposed_this_frame: bool) -> FrameUpdateResult {
529 cranpose_ui::tick_cursor_blink();
533 let render_dirty = take_render_invalidation();
534 let pointer_dirty = take_pointer_invalidation();
535 take_focus_invalidation();
536 let draw_repass_pending = cranpose_ui::has_pending_draw_repasses();
537 let mut draw_dirty_nodes = self.refresh_draw_repasses();
538 if render_dirty && !draw_repass_pending && draw_dirty_nodes.is_empty() {
539 draw_dirty_nodes = self.refresh_retained_redraw_nodes();
540 }
541 let layout_dirty_nodes = std::mem::take(&mut self.scoped_layout_scene_nodes);
542 let structural_parents = if cranpose_core::env_flag!("CRANPOSE_DISABLE_STRUCTURAL_DIRT") {
547 Vec::new()
548 } else if let Some(root) = self.composition.root() {
549 self.composition
550 .applier_mut()
551 .take_structural_change_parents_attached_to(root)
552 } else {
553 Vec::new()
554 };
555 let structural_dirty = !structural_parents.is_empty();
556 let mut partial_dirty_nodes = draw_dirty_nodes.clone();
557 partial_dirty_nodes.extend(layout_dirty_nodes.iter().copied());
558 partial_dirty_nodes.extend(structural_parents.iter().copied());
559 partial_dirty_nodes.sort_unstable();
560 partial_dirty_nodes.dedup();
561 let draw_dirty_node_count = draw_dirty_nodes.len();
562 let layout_dirty_node_count = layout_dirty_nodes.len();
563 let structural_dirty_node_count = structural_parents.len();
564 let partial_dirty_node_count = partial_dirty_nodes.len();
565 let _ = cranpose_ui::has_focused_field();
570
571 let render_only_dirty = render_dirty
577 && partial_dirty_nodes.is_empty()
578 && !draw_repass_pending
579 && !structural_dirty;
580 let scene_dirty = self.scene_dirty;
581 let draw_only_partial_dirty = !draw_dirty_nodes.is_empty()
582 && layout_dirty_nodes.is_empty()
583 && !pointer_dirty
584 && !recomposed_this_frame
585 && !structural_dirty;
586 let scoped_scene_dirty = scene_dirty && !layout_dirty_nodes.is_empty();
587 let full_scene_dirty = scene_dirty && !scoped_scene_dirty && !draw_only_partial_dirty;
588 let partial_scene_dirty = !partial_dirty_nodes.is_empty();
589 let needs_scene_rebuild = full_scene_dirty
590 || scoped_scene_dirty
591 || partial_scene_dirty
592 || draw_repass_pending
593 || structural_dirty;
594
595 if !needs_scene_rebuild {
596 log_render_phase_dirty_diagnostics(RenderPhaseDirtyDiagnostics {
597 render_dirty,
598 pointer_dirty,
599 scene_dirty,
600 draw_repass_pending,
601 draw_dirty_nodes: draw_dirty_node_count,
602 layout_dirty_nodes: layout_dirty_node_count,
603 structural_dirty_nodes: structural_dirty_node_count,
604 partial_dirty_nodes: partial_dirty_node_count,
605 dirty_node_ids: render_phase_dirty_diagnostics_enabled().then(|| {
606 format!(
607 "draw={:?} layout={:?} structural={:?} partial={:?}",
608 draw_dirty_nodes,
609 layout_dirty_nodes,
610 structural_parents,
611 partial_dirty_nodes
612 )
613 }),
614 render_only_dirty,
615 recomposed_this_frame,
616 path: if render_only_dirty {
617 "present-retained"
618 } else {
619 "skip"
620 },
621 });
622 self.scoped_layout_scene_nodes = layout_dirty_nodes;
623 if render_only_dirty && self.dev_options.fps_counter {
629 let viewport_size = Size {
630 width: self.viewport.0,
631 height: self.viewport.1,
632 };
633 self.refresh_dev_overlay_text_for_frame_at(viewport_size, Instant::now());
634 let text = self.dev_overlay_text.as_str();
635 self.renderer.draw_dev_overlay(text, viewport_size);
636 }
637 return FrameUpdateResult {
638 visual_changed: render_only_dirty,
639 structure_changed: false,
640 };
641 }
642 self.scene_dirty = false;
643 let viewport_size = Size {
644 width: self.viewport.0,
645 height: self.viewport.1,
646 };
647 let visual_update_only =
648 draw_only_partial_dirty && !partial_dirty_nodes.is_empty() && !full_scene_dirty;
649 let structure_changed = !render_only_dirty && !visual_update_only;
650
651 if let Some(root) = self.composition.root() {
653 let mut applier = self.composition.applier_mut();
654 let use_partial_update =
655 !partial_dirty_nodes.is_empty() && !render_only_dirty && !full_scene_dirty;
656 let use_visual_update = use_partial_update && draw_only_partial_dirty;
657 log_render_phase_dirty_diagnostics(RenderPhaseDirtyDiagnostics {
658 render_dirty,
659 pointer_dirty,
660 scene_dirty,
661 draw_repass_pending,
662 draw_dirty_nodes: draw_dirty_node_count,
663 layout_dirty_nodes: layout_dirty_node_count,
664 structural_dirty_nodes: structural_dirty_node_count,
665 partial_dirty_nodes: partial_dirty_node_count,
666 dirty_node_ids: render_phase_dirty_diagnostics_enabled().then(|| {
667 format!(
668 "draw={:?} layout={:?} structural={:?} partial={:?}",
669 draw_dirty_nodes,
670 layout_dirty_nodes,
671 structural_parents,
672 partial_dirty_nodes
673 )
674 }),
675 render_only_dirty,
676 recomposed_this_frame,
677 path: if use_visual_update {
678 "visual-update"
679 } else if use_partial_update {
680 "update"
681 } else {
682 "rebuild"
683 },
684 });
685 let rebuild_result = if use_visual_update {
686 self.renderer.update_visual_scene_from_applier(
687 &mut applier,
688 root,
689 viewport_size,
690 &partial_dirty_nodes,
691 )
692 } else if use_partial_update {
693 self.renderer.update_scene_from_applier(
694 &mut applier,
695 root,
696 viewport_size,
697 &partial_dirty_nodes,
698 )
699 } else {
700 self.renderer
701 .rebuild_scene_from_applier(&mut applier, root, viewport_size)
702 };
703 if let Err(err) = rebuild_result {
704 log::error!("renderer rebuild failed: {err:?}");
706 self.renderer.scene_mut().clear();
707 }
708 } else {
709 self.renderer.scene_mut().clear();
710 }
711 if let Some(retained_nodes) = self.renderer.scene().retained_visual_observation_nodes() {
712 cranpose_ui::prune_draw_observations_to_nodes(&retained_nodes);
713 }
714
715 if self.dev_options.fps_counter {
717 self.refresh_dev_overlay_text_for_frame_at(viewport_size, Instant::now());
718 let renderer = &mut self.renderer;
719 let text = self.dev_overlay_text.as_str();
720 renderer.draw_dev_overlay(text, viewport_size);
721 }
722
723 FrameUpdateResult {
724 visual_changed: true,
725 structure_changed,
726 }
727 }
728
729 fn refresh_dev_overlay_text_for_frame_at(&mut self, viewport_size: Size, now: Instant) {
730 if !self.dev_overlay_text_needs_refresh(viewport_size, now) {
731 return;
732 }
733 self.dev_overlay_text = self.build_dev_overlay_text(viewport_size);
734 self.dev_overlay_last_refresh = Some(now);
735 self.dev_overlay_viewport = Some(viewport_size);
736 }
737
738 fn dev_overlay_text_needs_refresh(&self, viewport_size: Size, now: Instant) -> bool {
739 if self.dev_overlay_text.is_empty() || self.dev_overlay_viewport != Some(viewport_size) {
740 return true;
741 }
742
743 self.dev_overlay_last_refresh
744 .map(|last| {
745 now.checked_duration_since(last).unwrap_or_default() >= DEV_OVERLAY_REFRESH_INTERVAL
746 })
747 .unwrap_or(true)
748 }
749
750 fn build_dev_overlay_text(&mut self, viewport_size: Size) -> String {
751 self.dev_overlay_controls.clear();
752
753 let stats = self.fps_monitor.stats();
754 let mut text = format!(
755 "{:.0} FPS | avg {:.1}ms | p95 {:.1}ms | max {:.1}ms | work {:.1}ms | {} recomp/s",
756 stats.fps,
757 stats.avg_ms,
758 stats.p95_ms,
759 stats.max_ms,
760 stats.work_avg_ms,
761 stats.recomps_per_second
762 );
763
764 if !self.dev_options.frame_pacing_controls {
765 return text;
766 }
767
768 text.push_str(" | ");
769 let mut controls = Vec::with_capacity(FramePacingMode::ALL.len());
770 for (index, mode) in FramePacingMode::ALL.into_iter().enumerate() {
771 if index > 0 {
772 text.push(' ');
773 }
774 let start = text.len();
775 if mode == self.dev_options.frame_pacing_mode {
776 text.push('[');
777 text.push_str(mode.label());
778 text.push(']');
779 } else {
780 text.push_str(mode.label());
781 }
782 controls.push((start, text.len(), mode));
783 }
784
785 let overlay_width = text.len() as f32 * DEV_OVERLAY_CHAR_WIDTH;
786 let overlay_x = (viewport_size.width - overlay_width - DEV_OVERLAY_PADDING * 2.0)
787 .max(DEV_OVERLAY_PADDING);
788 let overlay_y = DEV_OVERLAY_PADDING;
789 let text_x = overlay_x + DEV_OVERLAY_PADDING / 2.0;
790 let text_y = overlay_y + DEV_OVERLAY_PADDING / 4.0;
791 let text_height = DEV_OVERLAY_FONT_SIZE * 1.4;
792
793 self.dev_overlay_controls = controls
794 .into_iter()
795 .map(|(start, end, mode)| DevOverlayControl {
796 bounds: Rect {
797 x: text_x + start as f32 * DEV_OVERLAY_CHAR_WIDTH - 3.0,
798 y: text_y - 3.0,
799 width: (end - start) as f32 * DEV_OVERLAY_CHAR_WIDTH + 6.0,
800 height: text_height + 6.0,
801 },
802 mode,
803 })
804 .collect();
805
806 text
807 }
808}
809
810fn clear_dispatch_invalidation(
811 applier: &mut MemoryApplier,
812 node_id: NodeId,
813 invalidation: DispatchInvalidationKind,
814) -> Result<bool, NodeError> {
815 match invalidation {
816 DispatchInvalidationKind::Pointer => {
817 match applier.with_node::<LayoutNode, _>(node_id, |node| {
818 let needs_pointer_pass = node.needs_pointer_pass();
819 if needs_pointer_pass {
820 node.clear_needs_pointer_pass();
821 }
822 needs_pointer_pass
823 }) {
824 Ok(cleared) => Ok(cleared),
825 Err(NodeError::TypeMismatch { .. }) => applier
826 .with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
827 let needs_pointer_pass = node.needs_pointer_pass();
828 if needs_pointer_pass {
829 node.clear_needs_pointer_pass();
830 }
831 needs_pointer_pass
832 }),
833 Err(err) => Err(err),
834 }
835 }
836 DispatchInvalidationKind::Focus => {
837 match applier.with_node::<LayoutNode, _>(node_id, |node| {
838 let needs_focus_sync = node.needs_focus_sync();
839 if needs_focus_sync {
840 node.clear_needs_focus_sync();
841 }
842 needs_focus_sync
843 }) {
844 Ok(cleared) => Ok(cleared),
845 Err(NodeError::TypeMismatch { .. }) => applier
846 .with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
847 let needs_focus_sync = node.needs_focus_sync();
848 if needs_focus_sync {
849 node.clear_needs_focus_sync();
850 }
851 needs_focus_sync
852 }),
853 Err(err) => Err(err),
854 }
855 }
856 }
857}
858
859pub(crate) fn build_draw_refresh_scope(
860 applier: &mut MemoryApplier,
861 dirty_nodes: &HashSet<NodeId>,
862) -> HashSet<NodeId> {
863 let mut refresh_scope = HashSet::with_capacity(dirty_nodes.len());
864 for &dirty_node in dirty_nodes {
865 let mut current = Some(dirty_node);
866 while let Some(node_id) = current {
867 if !refresh_scope.insert(node_id) {
868 break;
869 }
870 current = applier.get_mut(node_id).ok().and_then(|node| node.parent());
871 }
872 }
873 refresh_scope
874}
875
876fn collect_retained_redraw_nodes(
877 applier: &mut MemoryApplier,
878 node_id: NodeId,
879 dirty_nodes: &mut Vec<NodeId>,
880) {
881 let children = match applier.get_mut(node_id) {
882 Ok(node) => node.children(),
883 Err(_) => return,
884 };
885
886 let redraw = match applier.with_node::<LayoutNode, _>(node_id, |node| {
887 let needs_redraw = node.needs_redraw();
888 if needs_redraw {
889 node.clear_needs_redraw();
890 }
891 needs_redraw
892 }) {
893 Ok(needs_redraw) => needs_redraw,
894 Err(NodeError::TypeMismatch { .. }) => applier
895 .with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
896 let needs_redraw = node.needs_redraw();
897 if needs_redraw {
898 node.clear_needs_redraw();
899 }
900 needs_redraw
901 })
902 .unwrap_or(false),
903 Err(_) => false,
904 };
905 if redraw {
906 dirty_nodes.push(node_id);
907 }
908
909 for child in children {
910 collect_retained_redraw_nodes(applier, child, dirty_nodes);
911 }
912}
913
914fn refresh_layout_box_data(
915 applier: &mut MemoryApplier,
916 layout: &mut cranpose_ui::layout::LayoutBox,
917 refresh_scope: &HashSet<NodeId>,
918 dirty_nodes: &HashSet<NodeId>,
919) {
920 if !refresh_scope.contains(&layout.node_id) {
921 return;
922 }
923
924 if dirty_nodes.contains(&layout.node_id) {
925 if let Ok((modifier, resolved_modifiers, slices)) =
926 applier.with_node::<LayoutNode, _>(layout.node_id, |node| {
927 node.clear_needs_redraw();
928 (
929 node.modifier.clone(),
930 node.resolved_modifiers(),
931 node.modifier_slices_snapshot(),
932 )
933 })
934 {
935 layout.node_data.modifier = modifier;
936 layout.node_data.resolved_modifiers = resolved_modifiers;
937 layout.node_data.modifier_slices = slices;
938 } else if let Ok((modifier, resolved_modifiers)) = applier
939 .with_node::<SubcomposeLayoutNode, _>(layout.node_id, |node| {
940 node.clear_needs_redraw();
941 (node.modifier(), node.resolved_modifiers())
942 })
943 {
944 layout.node_data.modifier = modifier.clone();
945 layout.node_data.resolved_modifiers = resolved_modifiers;
946 layout.node_data.modifier_slices =
947 std::rc::Rc::new(cranpose_ui::collect_slices_from_modifier(&modifier));
948 }
949 }
950
951 for child in &mut layout.children {
952 refresh_layout_box_data(applier, child, refresh_scope, dirty_nodes);
953 }
954}