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