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