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 scoped_layout_nodes = if cranpose_ui::has_pending_layout_repasses() {
192 cranpose_ui::pending_layout_repass_nodes_snapshot()
193 } else {
194 Vec::new()
195 };
196
197 let invalidation_requested = take_layout_invalidation();
201 let global_layout_invalidation = invalidation_requested && !has_scoped_repasses;
202 let force_layout_pass = self.force_layout_pass;
203
204 if invalidation_requested && !has_scoped_repasses {
205 cranpose_ui::layout::invalidate_all_layout_caches();
206
207 if let Some(root) = self.composition.root() {
210 let mut applier = self.composition.applier_mut();
211 match applier.with_node::<LayoutNode, _>(root, |node| {
212 node.mark_needs_measure();
213 node.mark_needs_layout();
214 }) {
215 Ok(()) | Err(NodeError::Missing { .. }) => {}
216 Err(NodeError::TypeMismatch { .. }) => {
217 let _ = applier.with_node::<SubcomposeLayoutNode, _>(root, |node| {
218 node.mark_needs_measure();
219 node.mark_needs_layout_flag();
220 });
221 }
222 Err(_) => {}
223 }
224 }
225 self.request_forced_layout_pass();
226 } else if invalidation_requested || has_scoped_repasses {
227 self.request_layout_pass();
228 }
229
230 if !self.layout_requested {
231 return;
232 }
233
234 let viewport_size = Size {
235 width: self.viewport.0,
236 height: self.viewport.1,
237 };
238 if let Some(root) = self.composition.root() {
239 let handle = self.composition.runtime_handle();
240 let mut applier = self.composition.applier_mut();
241 applier.set_runtime_handle(handle);
242
243 let tree_needs_layout_check = cranpose_ui::tree_needs_layout(&mut *applier, root)
244 .unwrap_or_else(|err| {
245 log::warn!(
246 "Cannot check layout dirty status for root #{}: {}",
247 root,
248 err
249 );
250 true });
252
253 let needs_layout =
254 self.force_layout_pass || has_scoped_repasses || tree_needs_layout_check;
255
256 if !needs_layout {
257 log::trace!("Skipping layout: tree is clean");
258 self.layout_requested = false;
259 self.force_layout_pass = false;
260 applier.clear_runtime_handle();
261 return;
262 }
263
264 self.layout_requested = false;
265 self.force_layout_pass = false;
266
267 match cranpose_ui::measure_layout_with_options(
269 &mut applier,
270 root,
271 viewport_size,
272 MeasureLayoutOptions {
273 collect_semantics: false,
274 build_layout_tree: false,
275 },
276 ) {
277 Ok(_measurements) => {
278 self.layout_tree = None;
279 self.semantics_snapshot_revision =
280 self.semantics_snapshot_revision.wrapping_add(1);
281 if self.semantics_enabled {
282 self.semantics_tree = None;
283 }
284 if has_scoped_repasses
285 && !global_layout_invalidation
286 && !force_layout_pass
287 && !scoped_layout_nodes.is_empty()
288 {
289 self.scoped_layout_scene_nodes = scoped_layout_nodes;
290 } else {
291 self.scoped_layout_scene_nodes.clear();
292 }
293 self.scene_dirty = true;
294 }
295 Err(err) => {
296 log::error!("failed to compute layout: {err}");
297 self.layout_tree = None;
298 self.semantics_tree = None;
299 self.semantics_snapshot_revision =
300 self.semantics_snapshot_revision.wrapping_add(1);
301 self.scoped_layout_scene_nodes.clear();
302 self.scene_dirty = true;
303 }
304 }
305 applier.clear_runtime_handle();
306 } else {
307 self.layout_tree = None;
308 self.semantics_tree = None;
309 self.semantics_snapshot_revision = self.semantics_snapshot_revision.wrapping_add(1);
310 self.scoped_layout_scene_nodes.clear();
311 self.scene_dirty = true;
312 self.layout_requested = false;
313 self.force_layout_pass = false;
314 }
315 }
316
317 fn run_post_layout_recomposition(&mut self) -> bool {
318 if !self.composition.should_recompose() {
319 return false;
320 }
321
322 let Some(root_key) = self.composition.root_key() else {
323 return false;
324 };
325
326 match self.composition.reconcile(root_key, &mut *self.content) {
327 Ok(changed) => {
328 if !changed {
329 return false;
330 }
331 self.fps_monitor.record_recomposition();
332 if self.composition_tree_needs_layout() {
333 self.request_layout_pass();
334 self.run_layout_phase_in_context();
335 }
336 request_render_invalidation();
337 true
338 }
339 Err(NodeError::Missing { id }) => {
340 log::debug!(
341 "Post-layout recomposition skipped: node {} no longer exists",
342 id
343 );
344 self.request_layout_pass();
345 request_render_invalidation();
346 true
347 }
348 Err(err) => {
349 log::error!("post-layout recomposition failed: {err}");
350 self.request_layout_pass();
351 request_render_invalidation();
352 true
353 }
354 }
355 }
356
357 fn run_dispatch_queues(&mut self) {
358 if has_pending_pointer_repasses() {
362 let mut applier = self.composition.applier_mut();
363 process_pointer_repasses(|node_id| {
364 match clear_dispatch_invalidation(
365 &mut applier,
366 node_id,
367 DispatchInvalidationKind::Pointer,
368 ) {
369 Ok(true) => {
370 log::trace!("Cleared pointer repass flag for node #{}", node_id);
371 }
372 Ok(false) => {}
373 Err(err) => {
374 log::debug!(
375 "Could not process pointer repass for node #{}: {}",
376 node_id,
377 err
378 );
379 }
380 }
381 });
382 }
383
384 if has_pending_focus_invalidations() {
388 let mut applier = self.composition.applier_mut();
389 process_focus_invalidations(|node_id| {
390 match clear_dispatch_invalidation(
391 &mut applier,
392 node_id,
393 DispatchInvalidationKind::Focus,
394 ) {
395 Ok(true) => {
396 log::trace!("Cleared focus sync flag for node #{}", node_id);
397 }
398 Ok(false) => {}
399 Err(err) => {
400 log::debug!(
401 "Could not process focus invalidation for node #{}: {}",
402 node_id,
403 err
404 );
405 }
406 }
407 });
408 }
409
410 if has_pending_semantics_invalidations() {
419 let mut applier = self.composition.applier_mut();
420 process_semantics_invalidations(|node_id| {
421 cranpose_core::bubble_semantics_dirty(&mut *applier, node_id);
422 });
423 }
424 }
425
426 fn refresh_draw_repasses(&mut self) -> Vec<NodeId> {
427 let dirty_nodes = take_draw_repass_nodes();
428 if dirty_nodes.is_empty() {
429 return dirty_nodes;
430 }
431 self.refresh_draw_nodes(dirty_nodes)
432 }
433
434 fn refresh_retained_redraw_nodes(&mut self) -> Vec<NodeId> {
435 let Some(root) = self.composition.root() else {
436 return Vec::new();
437 };
438 let mut dirty_nodes = Vec::new();
439 {
440 let mut applier = self.composition.applier_mut();
441 collect_retained_redraw_nodes(&mut applier, root, &mut dirty_nodes);
442 }
443 if dirty_nodes.is_empty() {
444 return dirty_nodes;
445 }
446 self.refresh_draw_nodes(dirty_nodes)
447 }
448
449 fn refresh_draw_nodes(&mut self, dirty_nodes: Vec<NodeId>) -> Vec<NodeId> {
450 let Some(layout_tree) = self.layout_tree.as_mut() else {
451 return dirty_nodes;
452 };
453
454 let dirty_set: HashSet<NodeId> = dirty_nodes.into_iter().collect();
455 let mut applier = self.composition.applier_mut();
456 let refresh_scope = build_draw_refresh_scope(&mut applier, &dirty_set);
457 refresh_layout_box_data(
458 &mut applier,
459 layout_tree.root_mut(),
460 &refresh_scope,
461 &dirty_set,
462 );
463 dirty_set.into_iter().collect()
464 }
465
466 #[cfg(test)]
467 pub(crate) fn run_render_phase(&mut self) -> FrameUpdateResult {
468 let app_context = Rc::clone(&self.app_context);
469 app_context.enter(|| self.run_render_phase_in_context(false))
470 }
471
472 fn run_render_phase_with_recomposition_state(
473 &mut self,
474 recomposed_this_frame: bool,
475 ) -> FrameUpdateResult {
476 let app_context = Rc::clone(&self.app_context);
477 app_context.enter(|| self.run_render_phase_in_context(recomposed_this_frame))
478 }
479
480 fn run_render_phase_in_context(&mut self, recomposed_this_frame: bool) -> FrameUpdateResult {
481 let render_dirty = take_render_invalidation();
482 let pointer_dirty = take_pointer_invalidation();
483 take_focus_invalidation();
484 let draw_repass_pending = cranpose_ui::has_pending_draw_repasses();
485 let mut draw_dirty_nodes = self.refresh_draw_repasses();
486 if render_dirty && !draw_repass_pending && draw_dirty_nodes.is_empty() {
487 draw_dirty_nodes = self.refresh_retained_redraw_nodes();
488 }
489 let layout_dirty_nodes = std::mem::take(&mut self.scoped_layout_scene_nodes);
490 let structural_parents = if cranpose_core::env_flag!("CRANPOSE_DISABLE_STRUCTURAL_DIRT") {
495 Vec::new()
496 } else if let Some(root) = self.composition.root() {
497 self.composition
498 .applier_mut()
499 .take_structural_change_parents_attached_to(root)
500 } else {
501 Vec::new()
502 };
503 let structural_dirty = !structural_parents.is_empty();
504 let mut partial_dirty_nodes = draw_dirty_nodes.clone();
505 partial_dirty_nodes.extend(layout_dirty_nodes.iter().copied());
506 partial_dirty_nodes.extend(structural_parents.iter().copied());
507 partial_dirty_nodes.sort_unstable();
508 partial_dirty_nodes.dedup();
509 let draw_dirty_node_count = draw_dirty_nodes.len();
510 let layout_dirty_node_count = layout_dirty_nodes.len();
511 let structural_dirty_node_count = structural_parents.len();
512 let partial_dirty_node_count = partial_dirty_nodes.len();
513 let _ = cranpose_ui::has_focused_field();
518 let cursor_blink_dirty = cranpose_ui::tick_cursor_blink();
520
521 let render_only_dirty = (render_dirty
522 && partial_dirty_nodes.is_empty()
523 && !draw_repass_pending
524 && !structural_dirty)
525 || cursor_blink_dirty;
526 let scene_dirty = self.scene_dirty;
527 let draw_only_partial_dirty = !draw_dirty_nodes.is_empty()
528 && layout_dirty_nodes.is_empty()
529 && !pointer_dirty
530 && !recomposed_this_frame
531 && !structural_dirty;
532 let scoped_scene_dirty = scene_dirty && !layout_dirty_nodes.is_empty();
533 let full_scene_dirty = scene_dirty && !scoped_scene_dirty && !draw_only_partial_dirty;
534 let partial_scene_dirty = !partial_dirty_nodes.is_empty();
535 let needs_scene_rebuild = full_scene_dirty
536 || scoped_scene_dirty
537 || partial_scene_dirty
538 || draw_repass_pending
539 || render_only_dirty
540 || structural_dirty;
541
542 if !needs_scene_rebuild {
543 log_render_phase_dirty_diagnostics(RenderPhaseDirtyDiagnostics {
544 render_dirty,
545 pointer_dirty,
546 scene_dirty,
547 draw_repass_pending,
548 draw_dirty_nodes: draw_dirty_node_count,
549 layout_dirty_nodes: layout_dirty_node_count,
550 structural_dirty_nodes: structural_dirty_node_count,
551 partial_dirty_nodes: partial_dirty_node_count,
552 dirty_node_ids: render_phase_dirty_diagnostics_enabled().then(|| {
553 format!(
554 "draw={:?} layout={:?} structural={:?} partial={:?}",
555 draw_dirty_nodes,
556 layout_dirty_nodes,
557 structural_parents,
558 partial_dirty_nodes
559 )
560 }),
561 render_only_dirty,
562 recomposed_this_frame,
563 path: "skip",
564 });
565 self.scoped_layout_scene_nodes = layout_dirty_nodes;
566 return FrameUpdateResult::default();
567 }
568 self.scene_dirty = false;
569 let viewport_size = Size {
570 width: self.viewport.0,
571 height: self.viewport.1,
572 };
573 let visual_update_only =
574 draw_only_partial_dirty && !partial_dirty_nodes.is_empty() && !full_scene_dirty;
575 let structure_changed = !render_only_dirty && !visual_update_only;
576
577 if let Some(root) = self.composition.root() {
579 let mut applier = self.composition.applier_mut();
580 let use_partial_update =
581 !partial_dirty_nodes.is_empty() && !render_only_dirty && !full_scene_dirty;
582 let use_visual_update = use_partial_update && draw_only_partial_dirty;
583 log_render_phase_dirty_diagnostics(RenderPhaseDirtyDiagnostics {
584 render_dirty,
585 pointer_dirty,
586 scene_dirty,
587 draw_repass_pending,
588 draw_dirty_nodes: draw_dirty_node_count,
589 layout_dirty_nodes: layout_dirty_node_count,
590 structural_dirty_nodes: structural_dirty_node_count,
591 partial_dirty_nodes: partial_dirty_node_count,
592 dirty_node_ids: render_phase_dirty_diagnostics_enabled().then(|| {
593 format!(
594 "draw={:?} layout={:?} structural={:?} partial={:?}",
595 draw_dirty_nodes,
596 layout_dirty_nodes,
597 structural_parents,
598 partial_dirty_nodes
599 )
600 }),
601 render_only_dirty,
602 recomposed_this_frame,
603 path: if use_visual_update {
604 "visual-update"
605 } else if use_partial_update {
606 "update"
607 } else {
608 "rebuild"
609 },
610 });
611 let rebuild_result = if use_visual_update {
612 self.renderer.update_visual_scene_from_applier(
613 &mut applier,
614 root,
615 viewport_size,
616 &partial_dirty_nodes,
617 )
618 } else if use_partial_update {
619 self.renderer.update_scene_from_applier(
620 &mut applier,
621 root,
622 viewport_size,
623 &partial_dirty_nodes,
624 )
625 } else {
626 self.renderer
627 .rebuild_scene_from_applier(&mut applier, root, viewport_size)
628 };
629 if let Err(err) = rebuild_result {
630 log::error!("renderer rebuild failed: {err:?}");
632 self.renderer.scene_mut().clear();
633 }
634 } else {
635 self.renderer.scene_mut().clear();
636 }
637
638 if self.dev_options.fps_counter {
640 self.refresh_dev_overlay_text_for_frame_at(viewport_size, Instant::now());
641 let renderer = &mut self.renderer;
642 let text = self.dev_overlay_text.as_str();
643 renderer.draw_dev_overlay(text, viewport_size);
644 }
645
646 FrameUpdateResult {
647 visual_changed: true,
648 structure_changed,
649 }
650 }
651
652 fn refresh_dev_overlay_text_for_frame_at(&mut self, viewport_size: Size, now: Instant) {
653 if !self.dev_overlay_text_needs_refresh(viewport_size, now) {
654 return;
655 }
656 self.dev_overlay_text = self.build_dev_overlay_text(viewport_size);
657 self.dev_overlay_last_refresh = Some(now);
658 self.dev_overlay_viewport = Some(viewport_size);
659 }
660
661 fn dev_overlay_text_needs_refresh(&self, viewport_size: Size, now: Instant) -> bool {
662 if self.dev_overlay_text.is_empty() || self.dev_overlay_viewport != Some(viewport_size) {
663 return true;
664 }
665
666 self.dev_overlay_last_refresh
667 .map(|last| {
668 now.checked_duration_since(last).unwrap_or_default() >= DEV_OVERLAY_REFRESH_INTERVAL
669 })
670 .unwrap_or(true)
671 }
672
673 fn build_dev_overlay_text(&mut self, viewport_size: Size) -> String {
674 self.dev_overlay_controls.clear();
675
676 let stats = self.fps_monitor.stats();
677 let mut text = format!(
678 "{:.0} FPS | avg {:.1}ms | p95 {:.1}ms | max {:.1}ms | work {:.1}ms | {} recomp/s",
679 stats.fps,
680 stats.avg_ms,
681 stats.p95_ms,
682 stats.max_ms,
683 stats.work_avg_ms,
684 stats.recomps_per_second
685 );
686
687 if !self.dev_options.frame_pacing_controls {
688 return text;
689 }
690
691 text.push_str(" | ");
692 let mut controls = Vec::with_capacity(FramePacingMode::ALL.len());
693 for (index, mode) in FramePacingMode::ALL.into_iter().enumerate() {
694 if index > 0 {
695 text.push(' ');
696 }
697 let start = text.len();
698 if mode == self.dev_options.frame_pacing_mode {
699 text.push('[');
700 text.push_str(mode.label());
701 text.push(']');
702 } else {
703 text.push_str(mode.label());
704 }
705 controls.push((start, text.len(), mode));
706 }
707
708 let overlay_width = text.len() as f32 * DEV_OVERLAY_CHAR_WIDTH;
709 let overlay_x = (viewport_size.width - overlay_width - DEV_OVERLAY_PADDING * 2.0)
710 .max(DEV_OVERLAY_PADDING);
711 let overlay_y = DEV_OVERLAY_PADDING;
712 let text_x = overlay_x + DEV_OVERLAY_PADDING / 2.0;
713 let text_y = overlay_y + DEV_OVERLAY_PADDING / 4.0;
714 let text_height = DEV_OVERLAY_FONT_SIZE * 1.4;
715
716 self.dev_overlay_controls = controls
717 .into_iter()
718 .map(|(start, end, mode)| DevOverlayControl {
719 bounds: Rect {
720 x: text_x + start as f32 * DEV_OVERLAY_CHAR_WIDTH - 3.0,
721 y: text_y - 3.0,
722 width: (end - start) as f32 * DEV_OVERLAY_CHAR_WIDTH + 6.0,
723 height: text_height + 6.0,
724 },
725 mode,
726 })
727 .collect();
728
729 text
730 }
731}
732
733fn clear_dispatch_invalidation(
734 applier: &mut MemoryApplier,
735 node_id: NodeId,
736 invalidation: DispatchInvalidationKind,
737) -> Result<bool, NodeError> {
738 match invalidation {
739 DispatchInvalidationKind::Pointer => {
740 match applier.with_node::<LayoutNode, _>(node_id, |node| {
741 let needs_pointer_pass = node.needs_pointer_pass();
742 if needs_pointer_pass {
743 node.clear_needs_pointer_pass();
744 }
745 needs_pointer_pass
746 }) {
747 Ok(cleared) => Ok(cleared),
748 Err(NodeError::TypeMismatch { .. }) => applier
749 .with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
750 let needs_pointer_pass = node.needs_pointer_pass();
751 if needs_pointer_pass {
752 node.clear_needs_pointer_pass();
753 }
754 needs_pointer_pass
755 }),
756 Err(err) => Err(err),
757 }
758 }
759 DispatchInvalidationKind::Focus => {
760 match applier.with_node::<LayoutNode, _>(node_id, |node| {
761 let needs_focus_sync = node.needs_focus_sync();
762 if needs_focus_sync {
763 node.clear_needs_focus_sync();
764 }
765 needs_focus_sync
766 }) {
767 Ok(cleared) => Ok(cleared),
768 Err(NodeError::TypeMismatch { .. }) => applier
769 .with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
770 let needs_focus_sync = node.needs_focus_sync();
771 if needs_focus_sync {
772 node.clear_needs_focus_sync();
773 }
774 needs_focus_sync
775 }),
776 Err(err) => Err(err),
777 }
778 }
779 }
780}
781
782pub(crate) fn build_draw_refresh_scope(
783 applier: &mut MemoryApplier,
784 dirty_nodes: &HashSet<NodeId>,
785) -> HashSet<NodeId> {
786 let mut refresh_scope = HashSet::with_capacity(dirty_nodes.len());
787 for &dirty_node in dirty_nodes {
788 let mut current = Some(dirty_node);
789 while let Some(node_id) = current {
790 if !refresh_scope.insert(node_id) {
791 break;
792 }
793 current = applier.get_mut(node_id).ok().and_then(|node| node.parent());
794 }
795 }
796 refresh_scope
797}
798
799fn collect_retained_redraw_nodes(
800 applier: &mut MemoryApplier,
801 node_id: NodeId,
802 dirty_nodes: &mut Vec<NodeId>,
803) {
804 let children = match applier.get_mut(node_id) {
805 Ok(node) => node.children(),
806 Err(_) => return,
807 };
808
809 let redraw = match applier.with_node::<LayoutNode, _>(node_id, |node| {
810 let needs_redraw = node.needs_redraw();
811 if needs_redraw {
812 node.clear_needs_redraw();
813 }
814 needs_redraw
815 }) {
816 Ok(needs_redraw) => needs_redraw,
817 Err(NodeError::TypeMismatch { .. }) => applier
818 .with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
819 let needs_redraw = node.needs_redraw();
820 if needs_redraw {
821 node.clear_needs_redraw();
822 }
823 needs_redraw
824 })
825 .unwrap_or(false),
826 Err(_) => false,
827 };
828 if redraw {
829 dirty_nodes.push(node_id);
830 }
831
832 for child in children {
833 collect_retained_redraw_nodes(applier, child, dirty_nodes);
834 }
835}
836
837fn refresh_layout_box_data(
838 applier: &mut MemoryApplier,
839 layout: &mut cranpose_ui::layout::LayoutBox,
840 refresh_scope: &HashSet<NodeId>,
841 dirty_nodes: &HashSet<NodeId>,
842) {
843 if !refresh_scope.contains(&layout.node_id) {
844 return;
845 }
846
847 if dirty_nodes.contains(&layout.node_id) {
848 if let Ok((modifier, resolved_modifiers, slices)) =
849 applier.with_node::<LayoutNode, _>(layout.node_id, |node| {
850 node.clear_needs_redraw();
851 (
852 node.modifier.clone(),
853 node.resolved_modifiers(),
854 node.modifier_slices_snapshot(),
855 )
856 })
857 {
858 layout.node_data.modifier = modifier;
859 layout.node_data.resolved_modifiers = resolved_modifiers;
860 layout.node_data.modifier_slices = slices;
861 } else if let Ok((modifier, resolved_modifiers)) = applier
862 .with_node::<SubcomposeLayoutNode, _>(layout.node_id, |node| {
863 node.clear_needs_redraw();
864 (node.modifier(), node.resolved_modifiers())
865 })
866 {
867 layout.node_data.modifier = modifier.clone();
868 layout.node_data.resolved_modifiers = resolved_modifiers;
869 layout.node_data.modifier_slices =
870 std::rc::Rc::new(cranpose_ui::collect_slices_from_modifier(&modifier));
871 }
872 }
873
874 for child in &mut layout.children {
875 refresh_layout_box_data(applier, child, refresh_scope, dirty_nodes);
876 }
877}