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 static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
64 *ENABLED.get_or_init(|| std::env::var_os("CRANPOSE_RENDER_PHASE_DIRTY_DIAG").is_some())
65}
66
67struct RenderPhaseDirtyDiagnostics<'a> {
68 render_dirty: bool,
69 pointer_dirty: bool,
70 scene_dirty: bool,
71 draw_repass_pending: bool,
72 draw_dirty_nodes: usize,
73 layout_dirty_nodes: usize,
74 structural_dirty_nodes: usize,
75 partial_dirty_nodes: usize,
76 dirty_node_ids: Option<String>,
77 render_only_dirty: bool,
78 recomposed_this_frame: bool,
79 path: &'a str,
80}
81
82fn log_render_phase_dirty_diagnostics(diagnostics: RenderPhaseDirtyDiagnostics<'_>) {
83 if !render_phase_dirty_diagnostics_enabled() {
84 return;
85 }
86
87 let RenderPhaseDirtyDiagnostics {
88 render_dirty,
89 pointer_dirty,
90 scene_dirty,
91 draw_repass_pending,
92 draw_dirty_nodes,
93 layout_dirty_nodes,
94 structural_dirty_nodes,
95 partial_dirty_nodes,
96 dirty_node_ids,
97 render_only_dirty,
98 recomposed_this_frame,
99 path,
100 } = diagnostics;
101 if let Some(dirty_node_ids) = dirty_node_ids {
102 log::warn!(
103 "[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}",
104 );
105 } else {
106 log::warn!(
107 "[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}",
108 );
109 }
110}
111
112impl<R> AppShell<R>
113where
114 R: Renderer,
115 R::Error: Debug,
116{
117 pub fn set_semantics_enabled(&mut self, enabled: bool) {
118 if self.semantics_enabled == enabled {
119 return;
120 }
121 self.semantics_enabled = enabled;
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(crate) fn process_frame(&mut self) -> FrameUpdateResult {
131 let app_context = Rc::clone(&self.app_context);
132 app_context.enter(|| self.process_frame_in_context(false))
133 }
134
135 pub(crate) fn process_frame_in_context(
136 &mut self,
137 recomposed_before_frame: bool,
138 ) -> FrameUpdateResult {
139 let frame_start = Instant::now();
140
141 self.run_layout_phase();
142 let after_initial_layout = Instant::now();
143
144 let recomposed_this_frame = recomposed_before_frame || self.run_post_layout_recomposition();
145
146 let after_layout = Instant::now();
147
148 self.run_dispatch_queues();
149
150 let after_dispatch = Instant::now();
151
152 clear_transient_scroll_motion_contexts();
153
154 let result = self.run_render_phase_with_recomposition_state(recomposed_this_frame);
155 let after_render = Instant::now();
156 log_frame_stage_telemetry(
157 frame_start,
158 after_initial_layout,
159 after_layout,
160 after_dispatch,
161 after_render,
162 );
163 result
164 }
165
166 pub(crate) fn run_layout_phase(&mut self) {
167 let app_context = Rc::clone(&self.app_context);
168 app_context.enter(|| self.run_layout_phase_in_context());
169 }
170
171 fn run_layout_phase_in_context(&mut self) {
172 let has_scoped_repasses = cranpose_ui::has_pending_layout_repasses()
175 || cranpose_ui::has_pending_measure_repasses();
176 let scoped_layout_nodes = if cranpose_ui::has_pending_layout_repasses() {
177 cranpose_ui::pending_layout_repass_nodes_snapshot()
178 } else {
179 Vec::new()
180 };
181
182 let invalidation_requested = take_layout_invalidation();
186 let global_layout_invalidation = invalidation_requested && !has_scoped_repasses;
187 let force_layout_pass = self.force_layout_pass;
188
189 if invalidation_requested && !has_scoped_repasses {
190 cranpose_ui::layout::invalidate_all_layout_caches();
191
192 if let Some(root) = self.composition.root() {
195 let mut applier = self.composition.applier_mut();
196 match applier.with_node::<LayoutNode, _>(root, |node| {
197 node.mark_needs_measure();
198 node.mark_needs_layout();
199 }) {
200 Ok(()) | Err(NodeError::Missing { .. }) => {}
201 Err(NodeError::TypeMismatch { .. }) => {
202 let _ = applier.with_node::<SubcomposeLayoutNode, _>(root, |node| {
203 node.mark_needs_measure();
204 node.mark_needs_layout_flag();
205 });
206 }
207 Err(_) => {}
208 }
209 }
210 self.request_forced_layout_pass();
211 } else if invalidation_requested || has_scoped_repasses {
212 self.request_layout_pass();
213 }
214
215 if !self.layout_requested {
216 return;
217 }
218
219 let viewport_size = Size {
220 width: self.viewport.0,
221 height: self.viewport.1,
222 };
223 if let Some(root) = self.composition.root() {
224 let handle = self.composition.runtime_handle();
225 let mut applier = self.composition.applier_mut();
226 applier.set_runtime_handle(handle);
227
228 let tree_needs_layout_check = cranpose_ui::tree_needs_layout(&mut *applier, root)
229 .unwrap_or_else(|err| {
230 log::warn!(
231 "Cannot check layout dirty status for root #{}: {}",
232 root,
233 err
234 );
235 true });
237
238 let needs_layout =
239 self.force_layout_pass || has_scoped_repasses || tree_needs_layout_check;
240
241 if !needs_layout {
242 log::trace!("Skipping layout: tree is clean");
243 self.layout_requested = false;
244 self.force_layout_pass = false;
245 applier.clear_runtime_handle();
246 return;
247 }
248
249 self.layout_requested = false;
250 self.force_layout_pass = false;
251
252 match cranpose_ui::measure_layout_with_options(
254 &mut applier,
255 root,
256 viewport_size,
257 MeasureLayoutOptions {
258 collect_semantics: false,
259 build_layout_tree: false,
260 },
261 ) {
262 Ok(_measurements) => {
263 self.layout_tree = None;
264 if self.semantics_enabled {
265 self.semantics_tree = None;
266 }
267 if has_scoped_repasses
268 && !global_layout_invalidation
269 && !force_layout_pass
270 && !scoped_layout_nodes.is_empty()
271 {
272 self.scoped_layout_scene_nodes = scoped_layout_nodes;
273 } else {
274 self.scoped_layout_scene_nodes.clear();
275 }
276 self.scene_dirty = true;
277 }
278 Err(err) => {
279 log::error!("failed to compute layout: {err}");
280 self.layout_tree = None;
281 self.semantics_tree = None;
282 self.scoped_layout_scene_nodes.clear();
283 self.scene_dirty = true;
284 }
285 }
286 applier.clear_runtime_handle();
287 } else {
288 self.layout_tree = None;
289 self.semantics_tree = None;
290 self.scoped_layout_scene_nodes.clear();
291 self.scene_dirty = true;
292 self.layout_requested = false;
293 self.force_layout_pass = false;
294 }
295 }
296
297 fn run_post_layout_recomposition(&mut self) -> bool {
298 if !self.composition.should_render() {
299 return false;
300 }
301
302 let Some(root_key) = self.composition.root_key() else {
303 return false;
304 };
305
306 match self.composition.reconcile(root_key, &mut *self.content) {
307 Ok(changed) => {
308 if !changed {
309 return false;
310 }
311 self.fps_monitor.record_recomposition();
312 if self.composition_tree_needs_layout() {
313 self.request_layout_pass();
314 self.run_layout_phase_in_context();
315 }
316 request_render_invalidation();
317 true
318 }
319 Err(NodeError::Missing { id }) => {
320 log::debug!(
321 "Post-layout recomposition skipped: node {} no longer exists",
322 id
323 );
324 self.request_layout_pass();
325 request_render_invalidation();
326 true
327 }
328 Err(err) => {
329 log::error!("post-layout recomposition failed: {err}");
330 self.request_layout_pass();
331 request_render_invalidation();
332 true
333 }
334 }
335 }
336
337 fn run_dispatch_queues(&mut self) {
338 if has_pending_pointer_repasses() {
342 let mut applier = self.composition.applier_mut();
343 process_pointer_repasses(|node_id| {
344 match clear_dispatch_invalidation(
345 &mut applier,
346 node_id,
347 DispatchInvalidationKind::Pointer,
348 ) {
349 Ok(true) => {
350 log::trace!("Cleared pointer repass flag for node #{}", node_id);
351 }
352 Ok(false) => {}
353 Err(err) => {
354 log::debug!(
355 "Could not process pointer repass for node #{}: {}",
356 node_id,
357 err
358 );
359 }
360 }
361 });
362 }
363
364 if has_pending_focus_invalidations() {
368 let mut applier = self.composition.applier_mut();
369 process_focus_invalidations(|node_id| {
370 match clear_dispatch_invalidation(
371 &mut applier,
372 node_id,
373 DispatchInvalidationKind::Focus,
374 ) {
375 Ok(true) => {
376 log::trace!("Cleared focus sync flag for node #{}", node_id);
377 }
378 Ok(false) => {}
379 Err(err) => {
380 log::debug!(
381 "Could not process focus invalidation for node #{}: {}",
382 node_id,
383 err
384 );
385 }
386 }
387 });
388 }
389 }
390
391 fn refresh_draw_repasses(&mut self) -> Vec<NodeId> {
392 let dirty_nodes = take_draw_repass_nodes();
393 if dirty_nodes.is_empty() {
394 return dirty_nodes;
395 }
396 self.refresh_draw_nodes(dirty_nodes)
397 }
398
399 fn refresh_retained_redraw_nodes(&mut self) -> Vec<NodeId> {
400 let Some(root) = self.composition.root() else {
401 return Vec::new();
402 };
403 let mut dirty_nodes = Vec::new();
404 {
405 let mut applier = self.composition.applier_mut();
406 collect_retained_redraw_nodes(&mut applier, root, &mut dirty_nodes);
407 }
408 if dirty_nodes.is_empty() {
409 return dirty_nodes;
410 }
411 self.refresh_draw_nodes(dirty_nodes)
412 }
413
414 fn refresh_draw_nodes(&mut self, dirty_nodes: Vec<NodeId>) -> Vec<NodeId> {
415 let Some(layout_tree) = self.layout_tree.as_mut() else {
416 return dirty_nodes;
417 };
418
419 let dirty_set: HashSet<NodeId> = dirty_nodes.into_iter().collect();
420 let mut applier = self.composition.applier_mut();
421 let refresh_scope = build_draw_refresh_scope(&mut applier, &dirty_set);
422 refresh_layout_box_data(
423 &mut applier,
424 layout_tree.root_mut(),
425 &refresh_scope,
426 &dirty_set,
427 );
428 dirty_set.into_iter().collect()
429 }
430
431 #[cfg(test)]
432 pub(crate) fn run_render_phase(&mut self) -> FrameUpdateResult {
433 let app_context = Rc::clone(&self.app_context);
434 app_context.enter(|| self.run_render_phase_in_context(false))
435 }
436
437 fn run_render_phase_with_recomposition_state(
438 &mut self,
439 recomposed_this_frame: bool,
440 ) -> FrameUpdateResult {
441 let app_context = Rc::clone(&self.app_context);
442 app_context.enter(|| self.run_render_phase_in_context(recomposed_this_frame))
443 }
444
445 fn run_render_phase_in_context(&mut self, recomposed_this_frame: bool) -> FrameUpdateResult {
446 let render_dirty = take_render_invalidation();
447 let pointer_dirty = take_pointer_invalidation();
448 take_focus_invalidation();
449 let draw_repass_pending = cranpose_ui::has_pending_draw_repasses();
450 let mut draw_dirty_nodes = self.refresh_draw_repasses();
451 if render_dirty && !draw_repass_pending && draw_dirty_nodes.is_empty() {
452 draw_dirty_nodes = self.refresh_retained_redraw_nodes();
453 }
454 let layout_dirty_nodes = std::mem::take(&mut self.scoped_layout_scene_nodes);
455 let structural_parents = if std::env::var_os("CRANPOSE_DISABLE_STRUCTURAL_DIRT").is_some() {
460 Vec::new()
461 } else if let Some(root) = self.composition.root() {
462 self.composition
463 .applier_mut()
464 .take_structural_change_parents_attached_to(root)
465 } else {
466 Vec::new()
467 };
468 let structural_dirty = !structural_parents.is_empty();
469 let mut partial_dirty_nodes = draw_dirty_nodes.clone();
470 partial_dirty_nodes.extend(layout_dirty_nodes.iter().copied());
471 partial_dirty_nodes.extend(structural_parents.iter().copied());
472 partial_dirty_nodes.sort_unstable();
473 partial_dirty_nodes.dedup();
474 let draw_dirty_node_count = draw_dirty_nodes.len();
475 let layout_dirty_node_count = layout_dirty_nodes.len();
476 let structural_dirty_node_count = structural_parents.len();
477 let partial_dirty_node_count = partial_dirty_nodes.len();
478 let _ = cranpose_ui::has_focused_field();
483 let cursor_blink_dirty = cranpose_ui::tick_cursor_blink();
485
486 let render_only_dirty = (render_dirty
487 && partial_dirty_nodes.is_empty()
488 && !draw_repass_pending
489 && !structural_dirty)
490 || cursor_blink_dirty;
491 let scene_dirty = self.scene_dirty;
492 let draw_only_partial_dirty = !draw_dirty_nodes.is_empty()
493 && layout_dirty_nodes.is_empty()
494 && !pointer_dirty
495 && !recomposed_this_frame
496 && !structural_dirty;
497 let scoped_scene_dirty = scene_dirty && !layout_dirty_nodes.is_empty();
498 let full_scene_dirty = scene_dirty && !scoped_scene_dirty && !draw_only_partial_dirty;
499 let partial_scene_dirty = !partial_dirty_nodes.is_empty();
500 let needs_scene_rebuild = full_scene_dirty
501 || scoped_scene_dirty
502 || partial_scene_dirty
503 || draw_repass_pending
504 || render_only_dirty
505 || structural_dirty;
506
507 if !needs_scene_rebuild {
508 log_render_phase_dirty_diagnostics(RenderPhaseDirtyDiagnostics {
509 render_dirty,
510 pointer_dirty,
511 scene_dirty,
512 draw_repass_pending,
513 draw_dirty_nodes: draw_dirty_node_count,
514 layout_dirty_nodes: layout_dirty_node_count,
515 structural_dirty_nodes: structural_dirty_node_count,
516 partial_dirty_nodes: partial_dirty_node_count,
517 dirty_node_ids: render_phase_dirty_diagnostics_enabled().then(|| {
518 format!(
519 "draw={:?} layout={:?} structural={:?} partial={:?}",
520 draw_dirty_nodes,
521 layout_dirty_nodes,
522 structural_parents,
523 partial_dirty_nodes
524 )
525 }),
526 render_only_dirty,
527 recomposed_this_frame,
528 path: "skip",
529 });
530 self.scoped_layout_scene_nodes = layout_dirty_nodes;
531 return FrameUpdateResult::default();
532 }
533 self.scene_dirty = false;
534 let viewport_size = Size {
535 width: self.viewport.0,
536 height: self.viewport.1,
537 };
538 let visual_update_only =
539 draw_only_partial_dirty && !partial_dirty_nodes.is_empty() && !full_scene_dirty;
540 let structure_changed = !render_only_dirty && !visual_update_only;
541
542 if let Some(root) = self.composition.root() {
544 let mut applier = self.composition.applier_mut();
545 let use_partial_update =
546 !partial_dirty_nodes.is_empty() && !render_only_dirty && !full_scene_dirty;
547 let use_visual_update = use_partial_update && draw_only_partial_dirty;
548 log_render_phase_dirty_diagnostics(RenderPhaseDirtyDiagnostics {
549 render_dirty,
550 pointer_dirty,
551 scene_dirty,
552 draw_repass_pending,
553 draw_dirty_nodes: draw_dirty_node_count,
554 layout_dirty_nodes: layout_dirty_node_count,
555 structural_dirty_nodes: structural_dirty_node_count,
556 partial_dirty_nodes: partial_dirty_node_count,
557 dirty_node_ids: render_phase_dirty_diagnostics_enabled().then(|| {
558 format!(
559 "draw={:?} layout={:?} structural={:?} partial={:?}",
560 draw_dirty_nodes,
561 layout_dirty_nodes,
562 structural_parents,
563 partial_dirty_nodes
564 )
565 }),
566 render_only_dirty,
567 recomposed_this_frame,
568 path: if use_visual_update {
569 "visual-update"
570 } else if use_partial_update {
571 "update"
572 } else {
573 "rebuild"
574 },
575 });
576 let rebuild_result = if use_visual_update {
577 self.renderer.update_visual_scene_from_applier(
578 &mut applier,
579 root,
580 viewport_size,
581 &partial_dirty_nodes,
582 )
583 } else if use_partial_update {
584 self.renderer.update_scene_from_applier(
585 &mut applier,
586 root,
587 viewport_size,
588 &partial_dirty_nodes,
589 )
590 } else {
591 self.renderer
592 .rebuild_scene_from_applier(&mut applier, root, viewport_size)
593 };
594 if let Err(err) = rebuild_result {
595 log::error!("renderer rebuild failed: {err:?}");
597 self.renderer.scene_mut().clear();
598 }
599 } else {
600 self.renderer.scene_mut().clear();
601 }
602
603 if self.dev_options.fps_counter {
605 self.refresh_dev_overlay_text_for_frame_at(viewport_size, Instant::now());
606 let renderer = &mut self.renderer;
607 let text = self.dev_overlay_text.as_str();
608 renderer.draw_dev_overlay(text, viewport_size);
609 }
610
611 FrameUpdateResult {
612 visual_changed: true,
613 structure_changed,
614 }
615 }
616
617 fn refresh_dev_overlay_text_for_frame_at(&mut self, viewport_size: Size, now: Instant) {
618 if !self.dev_overlay_text_needs_refresh(viewport_size, now) {
619 return;
620 }
621 self.dev_overlay_text = self.build_dev_overlay_text(viewport_size);
622 self.dev_overlay_last_refresh = Some(now);
623 self.dev_overlay_viewport = Some(viewport_size);
624 }
625
626 fn dev_overlay_text_needs_refresh(&self, viewport_size: Size, now: Instant) -> bool {
627 if self.dev_overlay_text.is_empty() || self.dev_overlay_viewport != Some(viewport_size) {
628 return true;
629 }
630
631 self.dev_overlay_last_refresh
632 .map(|last| {
633 now.checked_duration_since(last).unwrap_or_default() >= DEV_OVERLAY_REFRESH_INTERVAL
634 })
635 .unwrap_or(true)
636 }
637
638 fn build_dev_overlay_text(&mut self, viewport_size: Size) -> String {
639 self.dev_overlay_controls.clear();
640
641 let stats = self.fps_monitor.stats();
642 let mut text = format!(
643 "{:.0} FPS | avg {:.1}ms | p95 {:.1}ms | max {:.1}ms | work {:.1}ms | {} recomp/s",
644 stats.fps,
645 stats.avg_ms,
646 stats.p95_ms,
647 stats.max_ms,
648 stats.work_avg_ms,
649 stats.recomps_per_second
650 );
651
652 if !self.dev_options.frame_pacing_controls {
653 return text;
654 }
655
656 text.push_str(" | ");
657 let mut controls = Vec::with_capacity(FramePacingMode::ALL.len());
658 for (index, mode) in FramePacingMode::ALL.into_iter().enumerate() {
659 if index > 0 {
660 text.push(' ');
661 }
662 let start = text.len();
663 if mode == self.dev_options.frame_pacing_mode {
664 text.push('[');
665 text.push_str(mode.label());
666 text.push(']');
667 } else {
668 text.push_str(mode.label());
669 }
670 controls.push((start, text.len(), mode));
671 }
672
673 let overlay_width = text.len() as f32 * DEV_OVERLAY_CHAR_WIDTH;
674 let overlay_x = (viewport_size.width - overlay_width - DEV_OVERLAY_PADDING * 2.0)
675 .max(DEV_OVERLAY_PADDING);
676 let overlay_y = DEV_OVERLAY_PADDING;
677 let text_x = overlay_x + DEV_OVERLAY_PADDING / 2.0;
678 let text_y = overlay_y + DEV_OVERLAY_PADDING / 4.0;
679 let text_height = DEV_OVERLAY_FONT_SIZE * 1.4;
680
681 self.dev_overlay_controls = controls
682 .into_iter()
683 .map(|(start, end, mode)| DevOverlayControl {
684 bounds: Rect {
685 x: text_x + start as f32 * DEV_OVERLAY_CHAR_WIDTH - 3.0,
686 y: text_y - 3.0,
687 width: (end - start) as f32 * DEV_OVERLAY_CHAR_WIDTH + 6.0,
688 height: text_height + 6.0,
689 },
690 mode,
691 })
692 .collect();
693
694 text
695 }
696}
697
698fn clear_dispatch_invalidation(
699 applier: &mut MemoryApplier,
700 node_id: NodeId,
701 invalidation: DispatchInvalidationKind,
702) -> Result<bool, NodeError> {
703 match invalidation {
704 DispatchInvalidationKind::Pointer => {
705 match applier.with_node::<LayoutNode, _>(node_id, |node| {
706 let needs_pointer_pass = node.needs_pointer_pass();
707 if needs_pointer_pass {
708 node.clear_needs_pointer_pass();
709 }
710 needs_pointer_pass
711 }) {
712 Ok(cleared) => Ok(cleared),
713 Err(NodeError::TypeMismatch { .. }) => applier
714 .with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
715 let needs_pointer_pass = node.needs_pointer_pass();
716 if needs_pointer_pass {
717 node.clear_needs_pointer_pass();
718 }
719 needs_pointer_pass
720 }),
721 Err(err) => Err(err),
722 }
723 }
724 DispatchInvalidationKind::Focus => {
725 match applier.with_node::<LayoutNode, _>(node_id, |node| {
726 let needs_focus_sync = node.needs_focus_sync();
727 if needs_focus_sync {
728 node.clear_needs_focus_sync();
729 }
730 needs_focus_sync
731 }) {
732 Ok(cleared) => Ok(cleared),
733 Err(NodeError::TypeMismatch { .. }) => applier
734 .with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
735 let needs_focus_sync = node.needs_focus_sync();
736 if needs_focus_sync {
737 node.clear_needs_focus_sync();
738 }
739 needs_focus_sync
740 }),
741 Err(err) => Err(err),
742 }
743 }
744 }
745}
746
747pub(crate) fn build_draw_refresh_scope(
748 applier: &mut MemoryApplier,
749 dirty_nodes: &HashSet<NodeId>,
750) -> HashSet<NodeId> {
751 let mut refresh_scope = HashSet::with_capacity(dirty_nodes.len());
752 for &dirty_node in dirty_nodes {
753 let mut current = Some(dirty_node);
754 while let Some(node_id) = current {
755 if !refresh_scope.insert(node_id) {
756 break;
757 }
758 current = applier.get_mut(node_id).ok().and_then(|node| node.parent());
759 }
760 }
761 refresh_scope
762}
763
764fn collect_retained_redraw_nodes(
765 applier: &mut MemoryApplier,
766 node_id: NodeId,
767 dirty_nodes: &mut Vec<NodeId>,
768) {
769 let children = match applier.get_mut(node_id) {
770 Ok(node) => node.children(),
771 Err(_) => return,
772 };
773
774 let redraw = match applier.with_node::<LayoutNode, _>(node_id, |node| {
775 let needs_redraw = node.needs_redraw();
776 if needs_redraw {
777 node.clear_needs_redraw();
778 }
779 needs_redraw
780 }) {
781 Ok(needs_redraw) => needs_redraw,
782 Err(NodeError::TypeMismatch { .. }) => applier
783 .with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
784 let needs_redraw = node.needs_redraw();
785 if needs_redraw {
786 node.clear_needs_redraw();
787 }
788 needs_redraw
789 })
790 .unwrap_or(false),
791 Err(_) => false,
792 };
793 if redraw {
794 dirty_nodes.push(node_id);
795 }
796
797 for child in children {
798 collect_retained_redraw_nodes(applier, child, dirty_nodes);
799 }
800}
801
802fn refresh_layout_box_data(
803 applier: &mut MemoryApplier,
804 layout: &mut cranpose_ui::layout::LayoutBox,
805 refresh_scope: &HashSet<NodeId>,
806 dirty_nodes: &HashSet<NodeId>,
807) {
808 if !refresh_scope.contains(&layout.node_id) {
809 return;
810 }
811
812 if dirty_nodes.contains(&layout.node_id) {
813 if let Ok((modifier, resolved_modifiers, slices)) =
814 applier.with_node::<LayoutNode, _>(layout.node_id, |node| {
815 node.clear_needs_redraw();
816 (
817 node.modifier.clone(),
818 node.resolved_modifiers(),
819 node.modifier_slices_snapshot(),
820 )
821 })
822 {
823 layout.node_data.modifier = modifier;
824 layout.node_data.resolved_modifiers = resolved_modifiers;
825 layout.node_data.modifier_slices = slices;
826 } else if let Ok((modifier, resolved_modifiers)) = applier
827 .with_node::<SubcomposeLayoutNode, _>(layout.node_id, |node| {
828 node.clear_needs_redraw();
829 (node.modifier(), node.resolved_modifiers())
830 })
831 {
832 layout.node_data.modifier = modifier.clone();
833 layout.node_data.resolved_modifiers = resolved_modifiers;
834 layout.node_data.modifier_slices =
835 std::rc::Rc::new(cranpose_ui::collect_slices_from_modifier(&modifier));
836 }
837 }
838
839 for child in &mut layout.children {
840 refresh_layout_box_data(applier, child, refresh_scope, dirty_nodes);
841 }
842}