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 cursor_blink_dirty = cranpose_ui::tick_cursor_blink();
459
460 let render_only_dirty =
461 (render_dirty && partial_dirty_nodes.is_empty() && !draw_repass_pending)
462 || cursor_blink_dirty;
463 let scene_dirty = self.scene_dirty;
464 let draw_only_partial_dirty = !draw_dirty_nodes.is_empty()
465 && layout_dirty_nodes.is_empty()
466 && !pointer_dirty
467 && !recomposed_this_frame;
468 let scoped_scene_dirty = scene_dirty && !layout_dirty_nodes.is_empty();
469 let full_scene_dirty = scene_dirty && !scoped_scene_dirty && !draw_only_partial_dirty;
470 let partial_scene_dirty = !partial_dirty_nodes.is_empty();
471 let needs_scene_rebuild = full_scene_dirty
472 || scoped_scene_dirty
473 || partial_scene_dirty
474 || draw_repass_pending
475 || render_only_dirty;
476
477 if !needs_scene_rebuild {
478 log_render_phase_dirty_diagnostics(RenderPhaseDirtyDiagnostics {
479 render_dirty,
480 pointer_dirty,
481 scene_dirty,
482 draw_repass_pending,
483 draw_dirty_nodes: draw_dirty_node_count,
484 layout_dirty_nodes: layout_dirty_node_count,
485 partial_dirty_nodes: partial_dirty_node_count,
486 dirty_node_ids: render_phase_dirty_diagnostics_enabled().then(|| {
487 format!(
488 "draw={:?} layout={:?} partial={:?}",
489 draw_dirty_nodes, layout_dirty_nodes, partial_dirty_nodes
490 )
491 }),
492 render_only_dirty,
493 recomposed_this_frame,
494 path: "skip",
495 });
496 self.scoped_layout_scene_nodes = layout_dirty_nodes;
497 return FrameUpdateResult::default();
498 }
499 self.scene_dirty = false;
500 let viewport_size = Size {
501 width: self.viewport.0,
502 height: self.viewport.1,
503 };
504 let visual_update_only =
505 draw_only_partial_dirty && !partial_dirty_nodes.is_empty() && !full_scene_dirty;
506 let structure_changed = !render_only_dirty && !visual_update_only;
507
508 if let Some(root) = self.composition.root() {
510 let mut applier = self.composition.applier_mut();
511 let use_partial_update =
512 !partial_dirty_nodes.is_empty() && !render_only_dirty && !full_scene_dirty;
513 let use_visual_update = use_partial_update && draw_only_partial_dirty;
514 log_render_phase_dirty_diagnostics(RenderPhaseDirtyDiagnostics {
515 render_dirty,
516 pointer_dirty,
517 scene_dirty,
518 draw_repass_pending,
519 draw_dirty_nodes: draw_dirty_node_count,
520 layout_dirty_nodes: layout_dirty_node_count,
521 partial_dirty_nodes: partial_dirty_node_count,
522 dirty_node_ids: render_phase_dirty_diagnostics_enabled().then(|| {
523 format!(
524 "draw={:?} layout={:?} partial={:?}",
525 draw_dirty_nodes, layout_dirty_nodes, partial_dirty_nodes
526 )
527 }),
528 render_only_dirty,
529 recomposed_this_frame,
530 path: if use_visual_update {
531 "visual-update"
532 } else if use_partial_update {
533 "update"
534 } else {
535 "rebuild"
536 },
537 });
538 let rebuild_result = if use_visual_update {
539 self.renderer.update_visual_scene_from_applier(
540 &mut applier,
541 root,
542 viewport_size,
543 &partial_dirty_nodes,
544 )
545 } else if use_partial_update {
546 self.renderer.update_scene_from_applier(
547 &mut applier,
548 root,
549 viewport_size,
550 &partial_dirty_nodes,
551 )
552 } else {
553 self.renderer
554 .rebuild_scene_from_applier(&mut applier, root, viewport_size)
555 };
556 if let Err(err) = rebuild_result {
557 log::error!("renderer rebuild failed: {err:?}");
559 self.renderer.scene_mut().clear();
560 }
561 } else {
562 self.renderer.scene_mut().clear();
563 }
564
565 if self.dev_options.fps_counter {
567 self.refresh_dev_overlay_text_for_frame_at(viewport_size, Instant::now());
568 let renderer = &mut self.renderer;
569 let text = self.dev_overlay_text.as_str();
570 renderer.draw_dev_overlay(text, viewport_size);
571 }
572
573 FrameUpdateResult {
574 visual_changed: true,
575 structure_changed,
576 }
577 }
578
579 fn refresh_dev_overlay_text_for_frame_at(&mut self, viewport_size: Size, now: Instant) {
580 if !self.dev_overlay_text_needs_refresh(viewport_size, now) {
581 return;
582 }
583 self.dev_overlay_text = self.build_dev_overlay_text(viewport_size);
584 self.dev_overlay_last_refresh = Some(now);
585 self.dev_overlay_viewport = Some(viewport_size);
586 }
587
588 fn dev_overlay_text_needs_refresh(&self, viewport_size: Size, now: Instant) -> bool {
589 if self.dev_overlay_text.is_empty() || self.dev_overlay_viewport != Some(viewport_size) {
590 return true;
591 }
592
593 self.dev_overlay_last_refresh
594 .map(|last| {
595 now.checked_duration_since(last).unwrap_or_default() >= DEV_OVERLAY_REFRESH_INTERVAL
596 })
597 .unwrap_or(true)
598 }
599
600 fn build_dev_overlay_text(&mut self, viewport_size: Size) -> String {
601 self.dev_overlay_controls.clear();
602
603 let stats = self.fps_monitor.stats();
604 let mut text = format!(
605 "{:.0} FPS | avg {:.1}ms | p95 {:.1}ms | max {:.1}ms | work {:.1}ms | {} recomp/s",
606 stats.fps,
607 stats.avg_ms,
608 stats.p95_ms,
609 stats.max_ms,
610 stats.work_avg_ms,
611 stats.recomps_per_second
612 );
613
614 if !self.dev_options.frame_pacing_controls {
615 return text;
616 }
617
618 text.push_str(" | ");
619 let mut controls = Vec::with_capacity(FramePacingMode::ALL.len());
620 for (index, mode) in FramePacingMode::ALL.into_iter().enumerate() {
621 if index > 0 {
622 text.push(' ');
623 }
624 let start = text.len();
625 if mode == self.dev_options.frame_pacing_mode {
626 text.push('[');
627 text.push_str(mode.label());
628 text.push(']');
629 } else {
630 text.push_str(mode.label());
631 }
632 controls.push((start, text.len(), mode));
633 }
634
635 let overlay_width = text.len() as f32 * DEV_OVERLAY_CHAR_WIDTH;
636 let overlay_x = (viewport_size.width - overlay_width - DEV_OVERLAY_PADDING * 2.0)
637 .max(DEV_OVERLAY_PADDING);
638 let overlay_y = DEV_OVERLAY_PADDING;
639 let text_x = overlay_x + DEV_OVERLAY_PADDING / 2.0;
640 let text_y = overlay_y + DEV_OVERLAY_PADDING / 4.0;
641 let text_height = DEV_OVERLAY_FONT_SIZE * 1.4;
642
643 self.dev_overlay_controls = controls
644 .into_iter()
645 .map(|(start, end, mode)| DevOverlayControl {
646 bounds: Rect {
647 x: text_x + start as f32 * DEV_OVERLAY_CHAR_WIDTH - 3.0,
648 y: text_y - 3.0,
649 width: (end - start) as f32 * DEV_OVERLAY_CHAR_WIDTH + 6.0,
650 height: text_height + 6.0,
651 },
652 mode,
653 })
654 .collect();
655
656 text
657 }
658}
659
660fn clear_dispatch_invalidation(
661 applier: &mut MemoryApplier,
662 node_id: NodeId,
663 invalidation: DispatchInvalidationKind,
664) -> Result<bool, NodeError> {
665 match invalidation {
666 DispatchInvalidationKind::Pointer => {
667 match applier.with_node::<LayoutNode, _>(node_id, |node| {
668 let needs_pointer_pass = node.needs_pointer_pass();
669 if needs_pointer_pass {
670 node.clear_needs_pointer_pass();
671 }
672 needs_pointer_pass
673 }) {
674 Ok(cleared) => Ok(cleared),
675 Err(NodeError::TypeMismatch { .. }) => applier
676 .with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
677 let needs_pointer_pass = node.needs_pointer_pass();
678 if needs_pointer_pass {
679 node.clear_needs_pointer_pass();
680 }
681 needs_pointer_pass
682 }),
683 Err(err) => Err(err),
684 }
685 }
686 DispatchInvalidationKind::Focus => {
687 match applier.with_node::<LayoutNode, _>(node_id, |node| {
688 let needs_focus_sync = node.needs_focus_sync();
689 if needs_focus_sync {
690 node.clear_needs_focus_sync();
691 }
692 needs_focus_sync
693 }) {
694 Ok(cleared) => Ok(cleared),
695 Err(NodeError::TypeMismatch { .. }) => applier
696 .with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
697 let needs_focus_sync = node.needs_focus_sync();
698 if needs_focus_sync {
699 node.clear_needs_focus_sync();
700 }
701 needs_focus_sync
702 }),
703 Err(err) => Err(err),
704 }
705 }
706 }
707}
708
709pub(crate) fn build_draw_refresh_scope(
710 applier: &mut MemoryApplier,
711 dirty_nodes: &HashSet<NodeId>,
712) -> HashSet<NodeId> {
713 let mut refresh_scope = HashSet::with_capacity(dirty_nodes.len());
714 for &dirty_node in dirty_nodes {
715 let mut current = Some(dirty_node);
716 while let Some(node_id) = current {
717 if !refresh_scope.insert(node_id) {
718 break;
719 }
720 current = applier.get_mut(node_id).ok().and_then(|node| node.parent());
721 }
722 }
723 refresh_scope
724}
725
726fn collect_retained_redraw_nodes(
727 applier: &mut MemoryApplier,
728 node_id: NodeId,
729 dirty_nodes: &mut Vec<NodeId>,
730) {
731 let children = match applier.get_mut(node_id) {
732 Ok(node) => node.children(),
733 Err(_) => return,
734 };
735
736 let redraw = match applier.with_node::<LayoutNode, _>(node_id, |node| {
737 let needs_redraw = node.needs_redraw();
738 if needs_redraw {
739 node.clear_needs_redraw();
740 }
741 needs_redraw
742 }) {
743 Ok(needs_redraw) => needs_redraw,
744 Err(NodeError::TypeMismatch { .. }) => applier
745 .with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
746 let needs_redraw = node.needs_redraw();
747 if needs_redraw {
748 node.clear_needs_redraw();
749 }
750 needs_redraw
751 })
752 .unwrap_or(false),
753 Err(_) => false,
754 };
755 if redraw {
756 dirty_nodes.push(node_id);
757 }
758
759 for child in children {
760 collect_retained_redraw_nodes(applier, child, dirty_nodes);
761 }
762}
763
764fn refresh_layout_box_data(
765 applier: &mut MemoryApplier,
766 layout: &mut cranpose_ui::layout::LayoutBox,
767 refresh_scope: &HashSet<NodeId>,
768 dirty_nodes: &HashSet<NodeId>,
769) {
770 if !refresh_scope.contains(&layout.node_id) {
771 return;
772 }
773
774 if dirty_nodes.contains(&layout.node_id) {
775 if let Ok((modifier, resolved_modifiers, slices)) =
776 applier.with_node::<LayoutNode, _>(layout.node_id, |node| {
777 node.clear_needs_redraw();
778 (
779 node.modifier.clone(),
780 node.resolved_modifiers(),
781 node.modifier_slices_snapshot(),
782 )
783 })
784 {
785 layout.node_data.modifier = modifier;
786 layout.node_data.resolved_modifiers = resolved_modifiers;
787 layout.node_data.modifier_slices = slices;
788 } else if let Ok((modifier, resolved_modifiers)) = applier
789 .with_node::<SubcomposeLayoutNode, _>(layout.node_id, |node| {
790 node.clear_needs_redraw();
791 (node.modifier(), node.resolved_modifiers())
792 })
793 {
794 layout.node_data.modifier = modifier.clone();
795 layout.node_data.resolved_modifiers = resolved_modifiers;
796 layout.node_data.modifier_slices =
797 std::rc::Rc::new(cranpose_ui::collect_slices_from_modifier(&modifier));
798 }
799 }
800
801 for child in &mut layout.children {
802 refresh_layout_box_data(applier, child, refresh_scope, dirty_nodes);
803 }
804}