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
111#[allow(clippy::too_many_arguments)]
112fn log_dirty_diagnostics_for_path(
113 render_dirty: bool,
114 pointer_dirty: bool,
115 scene_dirty: bool,
116 draw_repass_pending: bool,
117 draw_dirty_nodes: &[NodeId],
118 layout_dirty_nodes: &[NodeId],
119 structural_parents: &[NodeId],
120 partial_dirty_nodes: &[NodeId],
121 render_only_dirty: bool,
122 recomposed_this_frame: bool,
123 path: &str,
124) {
125 log_render_phase_dirty_diagnostics(RenderPhaseDirtyDiagnostics {
126 render_dirty,
127 pointer_dirty,
128 scene_dirty,
129 draw_repass_pending,
130 draw_dirty_nodes: draw_dirty_nodes.len(),
131 layout_dirty_nodes: layout_dirty_nodes.len(),
132 structural_dirty_nodes: structural_parents.len(),
133 partial_dirty_nodes: partial_dirty_nodes.len(),
134 dirty_node_ids: render_phase_dirty_diagnostics_enabled().then(|| {
135 format!(
136 "draw={draw_dirty_nodes:?} layout={layout_dirty_nodes:?} structural={structural_parents:?} partial={partial_dirty_nodes:?}",
137 )
138 }),
139 render_only_dirty,
140 recomposed_this_frame,
141 path,
142 });
143}
144
145impl<R> AppShell<R>
146where
147 R: Renderer,
148 R::Error: Debug,
149{
150 pub fn set_semantics_enabled(&mut self, enabled: bool) {
151 if self.semantics_enabled == enabled {
152 return;
153 }
154 self.semantics_enabled = enabled;
155 self.semantics_snapshot_revision = self.semantics_snapshot_revision.wrapping_add(1);
156 if enabled {
157 self.request_forced_layout_pass();
158 self.mark_dirty();
159 } else {
160 self.semantics_tree = None;
161 }
162 }
163
164 pub fn set_frame_rate_preference(&mut self, preference: crate::FrameRatePreference) {
169 self.frame_rate_preference = preference;
170 }
171
172 pub fn frame_rate_preference(&self) -> crate::FrameRatePreference {
176 self.frame_rate_preference
177 }
178
179 pub(crate) fn process_frame(&mut self) -> FrameUpdateResult {
180 let app_context = Rc::clone(&self.app_context);
181 app_context.enter(|| self.process_frame_in_context(false))
182 }
183
184 pub(crate) fn process_frame_in_context(
185 &mut self,
186 recomposed_before_frame: bool,
187 ) -> FrameUpdateResult {
188 let frame_start = Instant::now();
189
190 self.run_layout_phase();
191 let after_initial_layout = Instant::now();
192
193 let recomposed_this_frame = recomposed_before_frame || self.run_post_layout_recomposition();
194
195 let after_layout = Instant::now();
196
197 self.run_dispatch_queues();
198
199 let after_dispatch = Instant::now();
200
201 clear_transient_scroll_motion_contexts();
202
203 let result = self.run_render_phase_with_recomposition_state(recomposed_this_frame);
204 let after_render = Instant::now();
205 log_frame_stage_telemetry(
206 frame_start,
207 after_initial_layout,
208 after_layout,
209 after_dispatch,
210 after_render,
211 );
212 result
213 }
214
215 pub(crate) fn run_layout_phase(&mut self) {
216 let app_context = Rc::clone(&self.app_context);
217 app_context.enter(|| self.run_layout_phase_in_context());
218 }
219
220 fn run_layout_phase_in_context(&mut self) {
221 let has_scoped_repasses = cranpose_ui::has_pending_layout_repasses()
222 || cranpose_ui::has_pending_measure_repasses();
223 let mut scoped_layout_nodes = if cranpose_ui::has_pending_layout_repasses() {
224 cranpose_ui::pending_layout_repass_nodes_snapshot()
225 } else {
226 Vec::new()
227 };
228 if cranpose_ui::has_pending_measure_repasses() {
229 for node in cranpose_ui::pending_measure_repass_nodes_snapshot() {
230 if !scoped_layout_nodes.contains(&node) {
231 scoped_layout_nodes.push(node);
232 }
233 }
234 }
235
236 let invalidation_requested = take_layout_invalidation();
237 let global_layout_invalidation = invalidation_requested && !has_scoped_repasses;
238 let force_layout_pass = self.force_layout_pass;
239
240 if invalidation_requested && !has_scoped_repasses {
241 cranpose_ui::layout::invalidate_all_layout_caches();
242
243 if let Some(root) = self.composition.root() {
244 let mut applier = self.composition.applier_mut();
245 match applier.with_node::<LayoutNode, _>(root, |node| {
246 node.mark_needs_measure();
247 node.mark_needs_layout();
248 }) {
249 Ok(()) | Err(NodeError::Missing { .. }) => {}
250 Err(NodeError::TypeMismatch { .. }) => {
251 let _ = applier.with_node::<SubcomposeLayoutNode, _>(root, |node| {
252 node.mark_needs_measure();
253 node.mark_needs_layout_flag();
254 });
255 }
256 Err(_) => {}
257 }
258 }
259 self.request_forced_layout_pass();
260 } else if invalidation_requested || has_scoped_repasses {
261 self.request_layout_pass();
262 }
263
264 if !self.layout_requested {
265 return;
266 }
267
268 let viewport_size = Size {
269 width: self.viewport.0,
270 height: self.viewport.1,
271 };
272 if let Some(root) = self.composition.root() {
273 let handle = self.composition.runtime_handle();
274 let mut applier = self.composition.applier_mut();
275 applier.set_runtime_handle(handle);
276
277 let tree_needs_layout_check = cranpose_ui::tree_needs_layout(&mut *applier, root)
278 .unwrap_or_else(|err| {
279 log::warn!(
280 "Cannot check layout dirty status for root #{}: {}",
281 root,
282 err
283 );
284 true
285 });
286
287 let needs_layout =
288 self.force_layout_pass || has_scoped_repasses || tree_needs_layout_check;
289
290 if !needs_layout {
291 log::trace!("Skipping layout: tree is clean");
292 self.layout_requested = false;
293 self.force_layout_pass = false;
294 applier.clear_runtime_handle();
295 return;
296 }
297
298 self.layout_requested = false;
299 self.force_layout_pass = false;
300
301 match cranpose_ui::measure_layout_with_options(
302 &mut applier,
303 root,
304 viewport_size,
305 MeasureLayoutOptions {
306 collect_semantics: false,
307 build_layout_tree: false,
308 },
309 ) {
310 Ok(_measurements) => {
311 self.layout_tree = None;
312 self.semantics_snapshot_revision =
313 self.semantics_snapshot_revision.wrapping_add(1);
314 if self.semantics_enabled {
315 self.semantics_tree = None;
316 }
317 if global_layout_invalidation || force_layout_pass {
318 self.scoped_layout_scene_nodes.clear();
319 let _ = cranpose_ui::take_geometry_scene_nodes();
320 } else {
321 let mut seen: HashSet<NodeId> =
322 self.scoped_layout_scene_nodes.iter().copied().collect();
323 if has_scoped_repasses {
324 for node in scoped_layout_nodes {
325 if seen.insert(node) {
326 self.scoped_layout_scene_nodes.push(node);
327 }
328 }
329 }
330 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() {
407 let mut applier = self.composition.applier_mut();
408 process_pointer_repasses(|node_id| {
409 match clear_dispatch_invalidation(
410 &mut applier,
411 node_id,
412 DispatchInvalidationKind::Pointer,
413 ) {
414 Ok(true) => {
415 log::trace!("Cleared pointer repass flag for node #{}", node_id);
416 }
417 Ok(false) => {}
418 Err(err) => {
419 log::debug!(
420 "Could not process pointer repass for node #{}: {}",
421 node_id,
422 err
423 );
424 }
425 }
426 });
427 }
428
429 if has_pending_focus_invalidations() {
430 let mut applier = self.composition.applier_mut();
431 process_focus_invalidations(|node_id| {
432 match clear_dispatch_invalidation(
433 &mut applier,
434 node_id,
435 DispatchInvalidationKind::Focus,
436 ) {
437 Ok(true) => {
438 log::trace!("Cleared focus sync flag for node #{}", node_id);
439 }
440 Ok(false) => {}
441 Err(err) => {
442 log::debug!(
443 "Could not process focus invalidation for node #{}: {}",
444 node_id,
445 err
446 );
447 }
448 }
449 });
450 }
451
452 if has_pending_semantics_invalidations() {
453 let mut applier = self.composition.applier_mut();
454 process_semantics_invalidations(|node_id| {
455 cranpose_core::bubble_semantics_dirty(&mut *applier, node_id);
456 });
457 }
458 }
459
460 fn refresh_draw_repasses(&mut self) -> Vec<NodeId> {
461 let dirty_nodes = take_draw_repass_nodes();
462 if dirty_nodes.is_empty() {
463 return dirty_nodes;
464 }
465 self.refresh_draw_nodes(dirty_nodes)
466 }
467
468 fn refresh_retained_redraw_nodes(&mut self) -> Vec<NodeId> {
469 let Some(root) = self.composition.root() else {
470 return Vec::new();
471 };
472 let mut dirty_nodes = Vec::new();
473 {
474 let mut applier = self.composition.applier_mut();
475 collect_retained_redraw_nodes(&mut applier, root, &mut dirty_nodes);
476 }
477 if dirty_nodes.is_empty() {
478 return dirty_nodes;
479 }
480 self.refresh_draw_nodes(dirty_nodes)
481 }
482
483 fn refresh_draw_nodes(&mut self, dirty_nodes: Vec<NodeId>) -> Vec<NodeId> {
484 let Some(layout_tree) = self.layout_tree.as_mut() else {
485 return dirty_nodes;
486 };
487
488 let dirty_set: HashSet<NodeId> = dirty_nodes.into_iter().collect();
489 let mut applier = self.composition.applier_mut();
490 let refresh_scope = build_draw_refresh_scope(&mut applier, &dirty_set);
491 refresh_layout_box_data(
492 &mut applier,
493 layout_tree.root_mut(),
494 &refresh_scope,
495 &dirty_set,
496 );
497 dirty_set.into_iter().collect()
498 }
499
500 #[cfg(test)]
501 pub(crate) fn run_render_phase(&mut self) -> FrameUpdateResult {
502 let app_context = Rc::clone(&self.app_context);
503 app_context.enter(|| self.run_render_phase_in_context(false))
504 }
505
506 fn run_render_phase_with_recomposition_state(
507 &mut self,
508 recomposed_this_frame: bool,
509 ) -> FrameUpdateResult {
510 let app_context = Rc::clone(&self.app_context);
511 app_context.enter(|| self.run_render_phase_in_context(recomposed_this_frame))
512 }
513
514 fn run_render_phase_in_context(&mut self, recomposed_this_frame: bool) -> FrameUpdateResult {
515 cranpose_ui::tick_cursor_blink();
516 let render_dirty = take_render_invalidation();
517 let pointer_dirty = take_pointer_invalidation();
518 take_focus_invalidation();
519 let draw_repass_pending = cranpose_ui::has_pending_draw_repasses();
520 let mut draw_dirty_nodes = self.refresh_draw_repasses();
521 if render_dirty && !draw_repass_pending && draw_dirty_nodes.is_empty() {
522 draw_dirty_nodes = self.refresh_retained_redraw_nodes();
523 }
524 let layout_dirty_nodes = std::mem::take(&mut self.scoped_layout_scene_nodes);
525 let structural_parents = if cranpose_core::env_flag!("CRANPOSE_DISABLE_STRUCTURAL_DIRT") {
526 Vec::new()
527 } else if let Some(root) = self.composition.root() {
528 self.composition
529 .applier_mut()
530 .take_structural_change_parents_attached_to(root)
531 } else {
532 Vec::new()
533 };
534 let structural_dirty = !structural_parents.is_empty();
535 let mut partial_dirty_nodes = draw_dirty_nodes.clone();
536 partial_dirty_nodes.extend(layout_dirty_nodes.iter().copied());
537 partial_dirty_nodes.extend(structural_parents.iter().copied());
538 partial_dirty_nodes.sort_unstable();
539 partial_dirty_nodes.dedup();
540 let _ = cranpose_ui::has_focused_field();
541
542 let render_only_dirty = render_dirty
543 && partial_dirty_nodes.is_empty()
544 && !draw_repass_pending
545 && !structural_dirty;
546 let scene_dirty = self.scene_dirty;
547 let draw_only_partial_dirty = !draw_dirty_nodes.is_empty()
548 && layout_dirty_nodes.is_empty()
549 && !pointer_dirty
550 && !recomposed_this_frame
551 && !structural_dirty;
552 let scoped_scene_dirty = scene_dirty && !layout_dirty_nodes.is_empty();
553 let full_scene_dirty = scene_dirty && !scoped_scene_dirty && !draw_only_partial_dirty;
554 let partial_scene_dirty = !partial_dirty_nodes.is_empty();
555 let needs_scene_rebuild = full_scene_dirty
556 || scoped_scene_dirty
557 || partial_scene_dirty
558 || draw_repass_pending
559 || structural_dirty;
560
561 if !needs_scene_rebuild {
562 log_dirty_diagnostics_for_path(
563 render_dirty,
564 pointer_dirty,
565 scene_dirty,
566 draw_repass_pending,
567 &draw_dirty_nodes,
568 &layout_dirty_nodes,
569 &structural_parents,
570 &partial_dirty_nodes,
571 render_only_dirty,
572 recomposed_this_frame,
573 if render_only_dirty {
574 "present-retained"
575 } else {
576 "skip"
577 },
578 );
579 self.scoped_layout_scene_nodes = layout_dirty_nodes;
580 if render_only_dirty && self.dev_options.fps_counter {
581 let viewport_size = Size {
582 width: self.viewport.0,
583 height: self.viewport.1,
584 };
585 self.refresh_dev_overlay_text_for_frame_at(viewport_size, Instant::now());
586 let text = self.dev_overlay_text.as_str();
587 self.renderer.draw_dev_overlay(text, viewport_size);
588 }
589 return FrameUpdateResult {
590 visual_changed: render_only_dirty,
591 structure_changed: false,
592 };
593 }
594 self.scene_dirty = false;
595 let viewport_size = Size {
596 width: self.viewport.0,
597 height: self.viewport.1,
598 };
599 let visual_update_only =
600 draw_only_partial_dirty && !partial_dirty_nodes.is_empty() && !full_scene_dirty;
601 let structure_changed = !render_only_dirty && !visual_update_only;
602
603 if let Some(root) = self.composition.root() {
604 let mut applier = self.composition.applier_mut();
605 let use_partial_update =
606 !partial_dirty_nodes.is_empty() && !render_only_dirty && !full_scene_dirty;
607 let use_visual_update = use_partial_update && draw_only_partial_dirty;
608 log_dirty_diagnostics_for_path(
609 render_dirty,
610 pointer_dirty,
611 scene_dirty,
612 draw_repass_pending,
613 &draw_dirty_nodes,
614 &layout_dirty_nodes,
615 &structural_parents,
616 &partial_dirty_nodes,
617 render_only_dirty,
618 recomposed_this_frame,
619 if use_visual_update {
620 "visual-update"
621 } else if use_partial_update {
622 "update"
623 } else {
624 "rebuild"
625 },
626 );
627 let rebuild_result = if use_visual_update {
628 self.renderer.update_visual_scene_from_applier(
629 &mut applier,
630 root,
631 viewport_size,
632 &partial_dirty_nodes,
633 )
634 } else if use_partial_update {
635 self.renderer.update_scene_from_applier(
636 &mut applier,
637 root,
638 viewport_size,
639 &partial_dirty_nodes,
640 )
641 } else {
642 self.renderer
643 .rebuild_scene_from_applier(&mut applier, root, viewport_size)
644 };
645 if let Err(err) = rebuild_result {
646 log::error!("renderer rebuild failed: {err:?}");
647 self.renderer.scene_mut().clear();
648 }
649 } else {
650 self.renderer.scene_mut().clear();
651 }
652 if let Some(retained_nodes) = self.renderer.scene().retained_visual_observation_nodes() {
653 cranpose_ui::prune_draw_observations_to_nodes(&retained_nodes);
654 }
655
656 if self.dev_options.fps_counter {
657 self.refresh_dev_overlay_text_for_frame_at(viewport_size, Instant::now());
658 let renderer = &mut self.renderer;
659 let text = self.dev_overlay_text.as_str();
660 renderer.draw_dev_overlay(text, viewport_size);
661 }
662
663 FrameUpdateResult {
664 visual_changed: true,
665 structure_changed,
666 }
667 }
668
669 fn refresh_dev_overlay_text_for_frame_at(&mut self, viewport_size: Size, now: Instant) {
670 if !self.dev_overlay_text_needs_refresh(viewport_size, now) {
671 return;
672 }
673 self.dev_overlay_text = self.build_dev_overlay_text(viewport_size);
674 self.dev_overlay_last_refresh = Some(now);
675 self.dev_overlay_viewport = Some(viewport_size);
676 }
677
678 fn dev_overlay_text_needs_refresh(&self, viewport_size: Size, now: Instant) -> bool {
679 if self.dev_overlay_text.is_empty() || self.dev_overlay_viewport != Some(viewport_size) {
680 return true;
681 }
682
683 self.dev_overlay_last_refresh
684 .map(|last| {
685 now.checked_duration_since(last).unwrap_or_default() >= DEV_OVERLAY_REFRESH_INTERVAL
686 })
687 .unwrap_or(true)
688 }
689
690 fn build_dev_overlay_text(&mut self, viewport_size: Size) -> String {
691 self.dev_overlay_controls.clear();
692
693 let stats = self.fps_monitor.stats();
694 let mut text = format!(
695 "{:.0} FPS | avg {:.1}ms | p95 {:.1}ms | max {:.1}ms | work {:.1}ms | {} recomp/s",
696 stats.fps,
697 stats.avg_ms,
698 stats.p95_ms,
699 stats.max_ms,
700 stats.work_avg_ms,
701 stats.recomps_per_second
702 );
703
704 if !self.dev_options.frame_pacing_controls {
705 return text;
706 }
707
708 text.push_str(" | ");
709 let mut controls = Vec::with_capacity(FramePacingMode::ALL.len());
710 for (index, mode) in FramePacingMode::ALL.into_iter().enumerate() {
711 if index > 0 {
712 text.push(' ');
713 }
714 let start = text.len();
715 if mode == self.dev_options.frame_pacing_mode {
716 text.push('[');
717 text.push_str(mode.label());
718 text.push(']');
719 } else {
720 text.push_str(mode.label());
721 }
722 controls.push((start, text.len(), mode));
723 }
724
725 let overlay_width = text.len() as f32 * DEV_OVERLAY_CHAR_WIDTH;
726 let overlay_x = (viewport_size.width - overlay_width - DEV_OVERLAY_PADDING * 2.0)
727 .max(DEV_OVERLAY_PADDING);
728 let overlay_y = DEV_OVERLAY_PADDING;
729 let text_x = overlay_x + DEV_OVERLAY_PADDING / 2.0;
730 let text_y = overlay_y + DEV_OVERLAY_PADDING / 4.0;
731 let text_height = DEV_OVERLAY_FONT_SIZE * 1.4;
732
733 self.dev_overlay_controls = controls
734 .into_iter()
735 .map(|(start, end, mode)| DevOverlayControl {
736 bounds: Rect {
737 x: text_x + start as f32 * DEV_OVERLAY_CHAR_WIDTH - 3.0,
738 y: text_y - 3.0,
739 width: (end - start) as f32 * DEV_OVERLAY_CHAR_WIDTH + 6.0,
740 height: text_height + 6.0,
741 },
742 mode,
743 })
744 .collect();
745
746 text
747 }
748}
749
750fn clear_dispatch_invalidation(
751 applier: &mut MemoryApplier,
752 node_id: NodeId,
753 invalidation: DispatchInvalidationKind,
754) -> Result<bool, NodeError> {
755 match invalidation {
756 DispatchInvalidationKind::Pointer => {
757 match applier.with_node::<LayoutNode, _>(node_id, |node| {
758 let needs_pointer_pass = node.needs_pointer_pass();
759 if needs_pointer_pass {
760 node.clear_needs_pointer_pass();
761 }
762 needs_pointer_pass
763 }) {
764 Ok(cleared) => Ok(cleared),
765 Err(NodeError::TypeMismatch { .. }) => applier
766 .with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
767 let needs_pointer_pass = node.needs_pointer_pass();
768 if needs_pointer_pass {
769 node.clear_needs_pointer_pass();
770 }
771 needs_pointer_pass
772 }),
773 Err(err) => Err(err),
774 }
775 }
776 DispatchInvalidationKind::Focus => {
777 match applier.with_node::<LayoutNode, _>(node_id, |node| {
778 let needs_focus_sync = node.needs_focus_sync();
779 if needs_focus_sync {
780 node.clear_needs_focus_sync();
781 }
782 needs_focus_sync
783 }) {
784 Ok(cleared) => Ok(cleared),
785 Err(NodeError::TypeMismatch { .. }) => applier
786 .with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
787 let needs_focus_sync = node.needs_focus_sync();
788 if needs_focus_sync {
789 node.clear_needs_focus_sync();
790 }
791 needs_focus_sync
792 }),
793 Err(err) => Err(err),
794 }
795 }
796 }
797}
798
799pub(crate) fn build_draw_refresh_scope(
800 applier: &mut MemoryApplier,
801 dirty_nodes: &HashSet<NodeId>,
802) -> HashSet<NodeId> {
803 let mut refresh_scope = HashSet::with_capacity(dirty_nodes.len());
804 for &dirty_node in dirty_nodes {
805 let mut current = Some(dirty_node);
806 while let Some(node_id) = current {
807 if !refresh_scope.insert(node_id) {
808 break;
809 }
810 current = applier.get_mut(node_id).ok().and_then(|node| node.parent());
811 }
812 }
813 refresh_scope
814}
815
816fn collect_retained_redraw_nodes(
817 applier: &mut MemoryApplier,
818 node_id: NodeId,
819 dirty_nodes: &mut Vec<NodeId>,
820) {
821 let children = match applier.get_mut(node_id) {
822 Ok(node) => node.children(),
823 Err(_) => return,
824 };
825
826 let redraw = match applier.with_node::<LayoutNode, _>(node_id, |node| {
827 let needs_redraw = node.needs_redraw();
828 if needs_redraw {
829 node.clear_needs_redraw();
830 }
831 needs_redraw
832 }) {
833 Ok(needs_redraw) => needs_redraw,
834 Err(NodeError::TypeMismatch { .. }) => applier
835 .with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
836 let needs_redraw = node.needs_redraw();
837 if needs_redraw {
838 node.clear_needs_redraw();
839 }
840 needs_redraw
841 })
842 .unwrap_or(false),
843 Err(_) => false,
844 };
845 if redraw {
846 dirty_nodes.push(node_id);
847 }
848
849 for child in children {
850 collect_retained_redraw_nodes(applier, child, dirty_nodes);
851 }
852}
853
854fn refresh_layout_box_data(
855 applier: &mut MemoryApplier,
856 layout: &mut cranpose_ui::layout::LayoutBox,
857 refresh_scope: &HashSet<NodeId>,
858 dirty_nodes: &HashSet<NodeId>,
859) {
860 if !refresh_scope.contains(&layout.node_id) {
861 return;
862 }
863
864 if dirty_nodes.contains(&layout.node_id) {
865 if let Ok((modifier, resolved_modifiers, slices)) =
866 applier.with_node::<LayoutNode, _>(layout.node_id, |node| {
867 node.clear_needs_redraw();
868 (
869 node.modifier.clone(),
870 node.resolved_modifiers(),
871 node.modifier_slices_snapshot(),
872 )
873 })
874 {
875 layout.node_data.modifier = modifier;
876 layout.node_data.resolved_modifiers = resolved_modifiers;
877 layout.node_data.modifier_slices = slices;
878 } else if let Ok((modifier, resolved_modifiers)) = applier
879 .with_node::<SubcomposeLayoutNode, _>(layout.node_id, |node| {
880 node.clear_needs_redraw();
881 (node.modifier(), node.resolved_modifiers())
882 })
883 {
884 layout.node_data.modifier = modifier.clone();
885 layout.node_data.resolved_modifiers = resolved_modifiers;
886 layout.node_data.modifier_slices =
887 std::rc::Rc::new(cranpose_ui::collect_slices_from_modifier(&modifier));
888 }
889 }
890
891 for child in &mut layout.children {
892 refresh_layout_box_data(applier, child, refresh_scope, dirty_nodes);
893 }
894}