Skip to main content

cranpose_app_shell/
shell_frame.rs

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_SEPARATOR: &str = " | ";
7const DEV_OVERLAY_REFRESH_INTERVAL: std::time::Duration = std::time::Duration::from_millis(250);
8const DEFAULT_FRAME_STAGE_TELEMETRY_THRESHOLD_MS: f64 = 4.0;
9
10#[derive(Copy, Clone)]
11enum DispatchInvalidationKind {
12    Pointer,
13    Focus,
14}
15
16fn frame_stage_telemetry_threshold_ms() -> Option<f64> {
17    static THRESHOLD_MS: std::sync::OnceLock<Option<f64>> = std::sync::OnceLock::new();
18    *THRESHOLD_MS.get_or_init(|| {
19        let explicit = std::env::var("CRANPOSE_FRAME_STAGE_TELEMETRY_MS")
20            .ok()
21            .and_then(|value| value.parse::<f64>().ok())
22            .filter(|value| value.is_finite() && *value >= 0.0);
23        explicit.or_else(|| {
24            std::env::var_os("CRANPOSE_FRAME_STAGE_TELEMETRY")
25                .is_some()
26                .then_some(DEFAULT_FRAME_STAGE_TELEMETRY_THRESHOLD_MS)
27        })
28    })
29}
30
31/// The stages of a frame that its telemetry times: start, initial layout,
32/// post-layout recomposition, dispatch and render.
33type FrameStages = cranpose_core::StageTimes<5>;
34
35fn log_frame_stage_telemetry(stages: &FrameStages) {
36    let Some(threshold_ms) = frame_stage_telemetry_threshold_ms() else {
37        return;
38    };
39    let Some(
40        [
41            frame_start,
42            after_initial_layout,
43            after_layout,
44            after_dispatch,
45            after_render,
46        ],
47    ) = stages.times()
48    else {
49        return;
50    };
51    let ms = cranpose_core::stage_ms;
52    let total_ms = ms(frame_start, after_render);
53    if total_ms < threshold_ms {
54        return;
55    }
56    log::warn!(
57        "[frame-stage-telemetry] total_ms={total_ms:.2} layout_ms={:.2} initial_layout_ms={:.2} post_layout_ms={:.2} dispatch_ms={:.2} scene_ms={:.2}",
58        ms(frame_start, after_layout),
59        ms(frame_start, after_initial_layout),
60        ms(after_initial_layout, after_layout),
61        ms(after_layout, after_dispatch),
62        ms(after_dispatch, after_render),
63    );
64}
65
66fn render_phase_dirty_diagnostics_enabled() -> bool {
67    cranpose_core::env_flag!("CRANPOSE_RENDER_PHASE_DIRTY_DIAG")
68}
69
70struct RenderPhaseDirtyDiagnostics<'a> {
71    render_dirty: bool,
72    pointer_dirty: bool,
73    scene_dirty: bool,
74    draw_repass_pending: bool,
75    draw_dirty_nodes: usize,
76    layer_property_nodes: usize,
77    layout_dirty_nodes: usize,
78    moved_nodes: usize,
79    structural_dirty_nodes: usize,
80    partial_dirty_nodes: usize,
81    dirty_node_ids: Option<String>,
82    render_only_dirty: bool,
83    recomposed_this_frame: bool,
84    path: &'a str,
85}
86
87fn log_render_phase_dirty_diagnostics(diagnostics: RenderPhaseDirtyDiagnostics<'_>) {
88    if !render_phase_dirty_diagnostics_enabled() {
89        return;
90    }
91
92    let RenderPhaseDirtyDiagnostics {
93        render_dirty,
94        pointer_dirty,
95        scene_dirty,
96        draw_repass_pending,
97        draw_dirty_nodes,
98        layer_property_nodes,
99        layout_dirty_nodes,
100        moved_nodes,
101        structural_dirty_nodes,
102        partial_dirty_nodes,
103        dirty_node_ids,
104        render_only_dirty,
105        recomposed_this_frame,
106        path,
107    } = diagnostics;
108    if let Some(dirty_node_ids) = dirty_node_ids {
109        log::warn!(
110            "[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} layer_property_nodes={layer_property_nodes} layout_dirty_nodes={layout_dirty_nodes} moved_nodes={moved_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}",
111        );
112    } else {
113        log::warn!(
114            "[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} layer_property_nodes={layer_property_nodes} layout_dirty_nodes={layout_dirty_nodes} moved_nodes={moved_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}",
115        );
116    }
117}
118
119#[derive(Clone, Copy)]
120struct FrameDirt {
121    render_dirty: bool,
122    pointer_dirty: bool,
123    attributed: bool,
124    layer_properties_pending: bool,
125    recomposed_this_frame: bool,
126}
127
128struct SurfaceDirt {
129    draw_dirty_nodes: Vec<NodeId>,
130    layer_property_nodes: Vec<NodeId>,
131    layout_dirty_nodes: Vec<NodeId>,
132    moved_nodes: Vec<NodeId>,
133    structural_parents: Vec<NodeId>,
134    partial_dirty_nodes: Vec<NodeId>,
135    draw_repass_pending: bool,
136    render_only_dirty: bool,
137    visual_only_partial_dirty: bool,
138    full_scene_dirty: bool,
139    needs_scene_rebuild: bool,
140}
141
142struct ScopedVisualNodes {
143    draw: Vec<NodeId>,
144    layers: Vec<NodeId>,
145}
146
147/// The nodes a surface's layout lanes hold: laid out again, and only moved.
148struct ScopedLayoutNodes {
149    reshaped: Vec<NodeId>,
150    moved: Vec<NodeId>,
151}
152
153impl SurfaceDirt {
154    fn classify(
155        frame: &FrameDirt,
156        scene_dirty: bool,
157        draw_repass_pending: bool,
158        visual_nodes: ScopedVisualNodes,
159        layout: ScopedLayoutNodes,
160        structural_parents: Vec<NodeId>,
161        mut partial_dirty_nodes: Vec<NodeId>,
162    ) -> Self {
163        let ScopedVisualNodes {
164            draw: draw_dirty_nodes,
165            layers: layer_property_nodes,
166        } = visual_nodes;
167        let ScopedLayoutNodes {
168            reshaped: layout_dirty_nodes,
169            moved: moved_nodes,
170        } = layout;
171        let structural_dirty = !structural_parents.is_empty();
172        // A node in two of the lists stays in both: the scene keys its
173        // updates by node, so sorting every id to drop the second copy would
174        // do that work twice.
175        partial_dirty_nodes.clear();
176        partial_dirty_nodes.extend(draw_dirty_nodes.iter().copied());
177        partial_dirty_nodes.extend(layout_dirty_nodes.iter().copied());
178        partial_dirty_nodes.extend(structural_parents.iter().copied());
179
180        // Moved nodes are not part of the partial list: the scene moves them.
181        let layout_changed = !layout_dirty_nodes.is_empty() || !moved_nodes.is_empty();
182        let partial_scene_dirty = !partial_dirty_nodes.is_empty() || !moved_nodes.is_empty();
183        let render_only_dirty = frame.render_dirty
184            && !frame.attributed
185            && !partial_scene_dirty
186            && layer_property_nodes.is_empty()
187            && !draw_repass_pending
188            && !structural_dirty;
189        let visual_only_partial_dirty = (!draw_dirty_nodes.is_empty()
190            || !layer_property_nodes.is_empty())
191            && !layout_changed
192            && !frame.pointer_dirty
193            && !frame.recomposed_this_frame
194            && !structural_dirty;
195        let scoped_scene_dirty = scene_dirty && layout_changed;
196        let full_scene_dirty = scene_dirty && !scoped_scene_dirty && !visual_only_partial_dirty;
197        // A scoped scene change always has nodes, so `partial_scene_dirty`
198        // covers it.
199        let needs_scene_rebuild = full_scene_dirty
200            || partial_scene_dirty
201            || !layer_property_nodes.is_empty()
202            || draw_repass_pending
203            || structural_dirty;
204        Self {
205            draw_dirty_nodes,
206            layer_property_nodes,
207            layout_dirty_nodes,
208            moved_nodes,
209            structural_parents,
210            partial_dirty_nodes,
211            draw_repass_pending,
212            render_only_dirty,
213            visual_only_partial_dirty,
214            full_scene_dirty,
215            needs_scene_rebuild,
216        }
217    }
218
219    fn use_partial_update(&self) -> bool {
220        (!self.partial_dirty_nodes.is_empty()
221            || !self.layer_property_nodes.is_empty()
222            || !self.moved_nodes.is_empty())
223            && !self.render_only_dirty
224            && !self.full_scene_dirty
225    }
226
227    fn use_visual_update(&self) -> bool {
228        self.use_partial_update() && self.visual_only_partial_dirty
229    }
230
231    fn rebuild_path(&self) -> &'static str {
232        if self.use_visual_update() {
233            "visual-update"
234        } else if self.use_partial_update() {
235            "update"
236        } else {
237            "rebuild"
238        }
239    }
240
241    fn log(&self, frame: &FrameDirt, scene_dirty: bool, path: &str) {
242        log_render_phase_dirty_diagnostics(RenderPhaseDirtyDiagnostics {
243            render_dirty: frame.render_dirty,
244            pointer_dirty: frame.pointer_dirty,
245            scene_dirty,
246            draw_repass_pending: self.draw_repass_pending,
247            draw_dirty_nodes: self.draw_dirty_nodes.len(),
248            layer_property_nodes: self.layer_property_nodes.len(),
249            layout_dirty_nodes: self.layout_dirty_nodes.len(),
250            moved_nodes: self.moved_nodes.len(),
251            structural_dirty_nodes: self.structural_parents.len(),
252            partial_dirty_nodes: self.partial_dirty_nodes.len(),
253            dirty_node_ids: render_phase_dirty_diagnostics_enabled().then(|| {
254                format!(
255                    "draw={:?} layers={:?} layout={:?} moved={:?} structural={:?} partial={:?}",
256                    self.draw_dirty_nodes,
257                    self.layer_property_nodes,
258                    self.layout_dirty_nodes,
259                    self.moved_nodes,
260                    self.structural_parents,
261                    self.partial_dirty_nodes,
262                )
263            }),
264            render_only_dirty: self.render_only_dirty,
265            recomposed_this_frame: frame.recomposed_this_frame,
266            path,
267        });
268    }
269}
270
271struct SurfaceFrame {
272    result: FrameUpdateResult,
273    rebuilt: bool,
274}
275
276fn pending_repass_nodes(context: &cranpose_ui::AppContext) -> Vec<NodeId> {
277    let mut nodes = if context.has_pending_layout_repasses() {
278        context.pending_layout_repass_nodes_snapshot()
279    } else {
280        Vec::new()
281    };
282    if context.has_pending_measure_repasses() {
283        for node in context.pending_measure_repass_nodes_snapshot() {
284            if !nodes.contains(&node) {
285                nodes.push(node);
286            }
287        }
288    }
289    nodes
290}
291
292fn mark_root_for_layout(applier: &mut MemoryApplier, root: NodeId) {
293    match applier.with_node::<LayoutNode, _>(root, |node| {
294        node.mark_needs_measure();
295        node.mark_needs_layout();
296    }) {
297        Ok(()) | Err(NodeError::Missing { .. }) => {}
298        Err(NodeError::TypeMismatch { .. }) => {
299            let _ = applier.with_node::<SubcomposeLayoutNode, _>(root, |node| {
300                node.mark_needs_measure();
301                node.mark_needs_layout_flag();
302            });
303        }
304        Err(_) => {}
305    }
306}
307
308pub(crate) const IDLE_PREFETCH_MAX_PASSES: usize = 4;
309
310impl<R> AppShell<R>
311where
312    R: Renderer,
313    R::Error: Debug,
314{
315    /// Turns semantics tracking on or off. Turning it on forces a layout pass and marks every
316    /// surface dirty. Turning it off drops the cached semantics trees.
317    pub fn set_semantics_enabled(&mut self, enabled: bool) {
318        if self.app.semantics_enabled == enabled {
319            return;
320        }
321        self.app.semantics_enabled = enabled;
322        self.app.semantics_snapshot_revision = self.app.semantics_snapshot_revision.wrapping_add(1);
323        if enabled {
324            self.app.request_forced_layout_pass();
325            self.mark_all_dirty();
326        } else {
327            for surface in &mut self.surfaces {
328                surface.semantics_tree = None;
329            }
330        }
331    }
332
333    /// Sets how the platform should vote the display's frame rate for this
334    /// app. The default, [`FrameRatePreference::Auto`], mirrors Compose: the
335    /// panel's fastest rate while frames are being produced, no preference
336    /// when the scene is still.
337    pub fn set_frame_rate_preference(&mut self, preference: crate::FrameRatePreference) {
338        self.surfaces[0].frame_rate_preference = preference;
339    }
340
341    /// The app's current display frame-rate preference. Read by platform
342    /// backends each frame; the vote itself is applied by the backend that
343    /// owns the native window.
344    pub fn frame_rate_preference(&self) -> crate::FrameRatePreference {
345        self.surfaces[0].frame_rate_preference
346    }
347
348    pub(crate) fn process_frame(&mut self) -> FrameUpdateResult {
349        let _app_context = self.app.app_context.enter_scope();
350        self.process_frame_in_context(false)
351    }
352
353    pub(crate) fn process_frame_in_context(
354        &mut self,
355        recomposed_before_frame: bool,
356    ) -> FrameUpdateResult {
357        let mut stages = FrameStages::start(frame_stage_telemetry_threshold_ms().is_some());
358
359        #[cfg(feature = "installed-fonts")]
360        self.apply_installed_fonts();
361        self.sync_window_roots_in_context();
362        self.run_layout_phase_in_context();
363        stages.reached();
364
365        let recomposed_this_frame = recomposed_before_frame || self.run_post_layout_recomposition();
366
367        stages.reached();
368
369        self.run_dispatch_queues();
370        self.update_modal_focus();
371        self.reveal_new_focus();
372
373        stages.reached();
374
375        self.app
376            .app_context
377            .clear_transient_scroll_motion_contexts();
378
379        let result = self.run_render_phase_in_context(recomposed_this_frame);
380        stages.reached();
381        log_frame_stage_telemetry(&stages);
382        result
383    }
384
385    /// Whether a lazy list waits for [`Self::run_idle_prefetch`].
386    pub fn has_idle_prefetch_work(&self) -> bool {
387        self.app
388            .app_context
389            .enter(cranpose_ui::has_lazy_prefetch_requests)
390    }
391
392    /// Composes lazy list items that frames left beyond their viewports for
393    /// later, in layout passes, at most four of them. A pass runs while
394    /// `deadline`, if any, still leaves room for the recent cost of one full pass
395    /// and `frame_can_start`, asked before each pass, says the next frame
396    /// cannot start yet.
397    /// Call it while waiting for the next frame. Returns whether a pass ran.
398    pub fn run_idle_prefetch(
399        &mut self,
400        deadline: Option<Instant>,
401        mut frame_can_start: impl FnMut(&mut R) -> bool,
402    ) -> bool {
403        let _app_context = self.app.app_context.enter_scope();
404        let mut passes = 0;
405        while passes < IDLE_PREFETCH_MAX_PASSES
406            && cranpose_ui::has_lazy_prefetch_requests()
407            && deadline.is_none_or(|deadline| {
408                Instant::now() + cranpose_ui::lazy_prefetch_pass_cost() <= deadline
409            })
410            && !frame_can_start(self.renderer())
411        {
412            cranpose_ui::with_lazy_prefetch_pass(|| {
413                cranpose_ui::drain_lazy_prefetch_requests(cranpose_ui::schedule_measure_repass);
414                self.run_layout_phase_in_context();
415                cranpose_ui::warm_prefetched_slices(&mut self.app.composition.applier_mut());
416            });
417            passes += 1;
418        }
419        passes > 0
420    }
421
422    /// Gives every renderer the fonts installed since the last frame, before
423    /// this frame lays text out.
424    #[cfg(feature = "installed-fonts")]
425    fn apply_installed_fonts(&mut self) {
426        let generation = cranpose_render_common::installed_fonts::installed_font_generation();
427        if generation == self.app.installed_fonts {
428            return;
429        }
430        self.app.installed_fonts = generation;
431        for surface in &mut self.surfaces {
432            surface
433                .renderer
434                .apply_installed_fonts(&self.app.app_context);
435        }
436    }
437
438    #[cfg(test)]
439    pub(crate) fn run_layout_phase(&mut self) {
440        let _app_context = self.app.app_context.enter_scope();
441        self.run_layout_phase_in_context();
442    }
443
444    pub(crate) fn run_layout_phase_in_context(&mut self) {
445        let context = &self.app.app_context;
446        let has_scoped_repasses =
447            context.has_pending_layout_repasses() || context.has_pending_measure_repasses();
448        let scoped_layout_nodes = pending_repass_nodes(context);
449        let global_layout_invalidation = context.take_layout_invalidation();
450        let force_layout_pass = self.app.force_layout_pass;
451
452        if global_layout_invalidation {
453            cranpose_ui::layout::invalidate_all_layout_caches();
454            if let Some(root) = self.app.composition.root() {
455                mark_root_for_layout(&mut self.app.composition.applier_mut(), root);
456            }
457            self.app.request_forced_layout_pass();
458        } else if has_scoped_repasses {
459            self.app.request_layout_pass();
460        }
461
462        if !self.app.layout_requested {
463            return;
464        }
465
466        let Some(root) = self.app.composition.root() else {
467            self.reset_layout_snapshots();
468            return;
469        };
470        let viewport_size = self.surfaces[0].viewport_size();
471        let handle = self.app.composition.runtime_handle();
472        let mut applier = self.app.composition.applier_mut();
473        applier.set_runtime_handle(handle);
474
475        let tree_needs_layout_check = cranpose_ui::tree_needs_layout(&mut *applier, root)
476            .unwrap_or_else(|err| {
477                log::warn!("Cannot check layout dirty status for root #{root}: {err}");
478                true
479            });
480        let needs_layout =
481            self.app.force_layout_pass || has_scoped_repasses || tree_needs_layout_check;
482        self.app.layout_requested = false;
483        self.app.force_layout_pass = false;
484
485        if !needs_layout {
486            log::trace!("Skipping layout: tree is clean");
487            applier.clear_runtime_handle();
488            return;
489        }
490
491        let measured = cranpose_ui::measure_layout_with_options(
492            &mut applier,
493            root,
494            viewport_size,
495            MeasureLayoutOptions {
496                collect_semantics: false,
497                build_layout_tree: false,
498            },
499        );
500        applier.clear_runtime_handle();
501        drop(applier);
502        match measured {
503            Ok(_measurements) => {
504                self.forget_frame_snapshots();
505                self.record_layout_scene_nodes(
506                    global_layout_invalidation || force_layout_pass,
507                    has_scoped_repasses,
508                    scoped_layout_nodes,
509                );
510            }
511            Err(err) => {
512                log::error!("failed to compute layout: {err}");
513                self.reset_layout_snapshots();
514            }
515        }
516    }
517
518    fn forget_frame_snapshots(&mut self) {
519        self.app.semantics_snapshot_revision = self.app.semantics_snapshot_revision.wrapping_add(1);
520        for surface in &mut self.surfaces {
521            surface.forget_snapshots();
522        }
523    }
524
525    fn reset_layout_snapshots(&mut self) {
526        self.forget_frame_snapshots();
527        for surface in &mut self.surfaces {
528            surface.invalidate_scene_root(surface.root_node(&self.app));
529        }
530        let _ = cranpose_ui::take_geometry_scene_nodes();
531        self.app.layout_requested = false;
532        self.app.force_layout_pass = false;
533    }
534
535    fn record_layout_scene_nodes(
536        &mut self,
537        global: bool,
538        has_scoped_repasses: bool,
539        scoped_layout_nodes: Vec<NodeId>,
540    ) {
541        self.app
542            .app_context
543            .take_geometry_scene_nodes_into(&mut self.geometry_scene_nodes);
544        self.app.content_moved |= !self.geometry_scene_nodes.is_empty();
545        if global {
546            for surface in &mut self.surfaces {
547                surface.invalidate_scene_root(surface.root_node(&self.app));
548            }
549        }
550        // The scene updates the root of a global pass in place and keeps the
551        // layers below it, so the nodes the pass reshaped or moved go to the
552        // scene too: a padding that grew moves its node's children although
553        // no child moved in its parent.
554        let scoped = (has_scoped_repasses && !global)
555            .then_some(scoped_layout_nodes)
556            .into_iter()
557            .flatten();
558        route_geometry_scene_nodes(
559            &mut self.app,
560            &mut self.surfaces,
561            scoped.chain(self.geometry_scene_nodes.reshaped.iter().copied()),
562            &self.geometry_scene_nodes.moved,
563            &mut self.routing_scratch,
564        );
565    }
566
567    fn run_post_layout_recomposition(&mut self) -> bool {
568        if !self.app.composition.should_recompose() {
569            return false;
570        }
571
572        let Some(root_key) = self.app.composition.root_key() else {
573            return false;
574        };
575
576        match self
577            .app
578            .composition
579            .reconcile(root_key, &mut *self.app.content)
580        {
581            Ok(changed) => {
582                if !changed {
583                    return false;
584                }
585                self.app.fps_monitor.record_recomposition();
586                self.sync_window_roots_in_context();
587                if self.app.composition_tree_needs_layout() {
588                    self.app.request_layout_pass();
589                    self.run_layout_phase_in_context();
590                }
591                request_render_invalidation();
592                true
593            }
594            Err(NodeError::Missing { id }) => {
595                log::debug!("Post-layout recomposition skipped: node {id} no longer exists");
596                self.app.request_layout_pass();
597                request_render_invalidation();
598                true
599            }
600            Err(err) => {
601                log::error!("post-layout recomposition failed: {err}");
602                self.app.request_layout_pass();
603                request_render_invalidation();
604                true
605            }
606        }
607    }
608
609    fn run_dispatch_queues(&mut self) {
610        if self.app.app_context.has_pending_pointer_repasses() {
611            let mut applier = self.app.composition.applier_mut();
612            process_pointer_repasses(|node_id| {
613                match clear_dispatch_invalidation(
614                    &mut applier,
615                    node_id,
616                    DispatchInvalidationKind::Pointer,
617                ) {
618                    Ok(true) => {
619                        log::trace!("Cleared pointer repass flag for node #{node_id}");
620                    }
621                    Ok(false) => {}
622                    Err(err) => {
623                        log::debug!("Could not process pointer repass for node #{node_id}: {err}");
624                    }
625                }
626            });
627        }
628
629        if self.app.app_context.has_pending_focus_invalidations() {
630            let mut applier = self.app.composition.applier_mut();
631            process_focus_invalidations(|node_id| {
632                match clear_dispatch_invalidation(
633                    &mut applier,
634                    node_id,
635                    DispatchInvalidationKind::Focus,
636                ) {
637                    Ok(true) => {
638                        log::trace!("Cleared focus sync flag for node #{node_id}");
639                    }
640                    Ok(false) => {}
641                    Err(err) => {
642                        log::debug!(
643                            "Could not process focus invalidation for node #{node_id}: {err}"
644                        );
645                    }
646                }
647            });
648        }
649
650        self.app.flush_semantics_invalidations();
651    }
652
653    fn take_structural_change_parents(&mut self) {
654        self.pending_dirty_nodes.clear();
655        if cranpose_core::env_flag!("CRANPOSE_DISABLE_STRUCTURAL_DIRT") {
656            return;
657        }
658        if self.app.composition.root().is_none() {
659            return;
660        }
661        self.app
662            .composition
663            .applier_mut()
664            .take_structural_change_parents_into(&mut self.pending_dirty_nodes);
665        // A child's z index orders it among its siblings, in its parent's
666        // layer: the parent changed as a child list does.
667        let reordered = self.pending_dirty_nodes.len();
668        self.app
669            .app_context
670            .append_z_order_changes(&mut self.pending_dirty_nodes);
671        if self.pending_dirty_nodes.len() == reordered {
672            return;
673        }
674        let mut applier = self.app.composition.applier_mut();
675        for node in &mut self.pending_dirty_nodes[reordered..] {
676            if let Some(parent) = applier.get_mut(*node).ok().and_then(|child| child.parent()) {
677                *node = parent;
678            }
679        }
680    }
681
682    #[cfg(test)]
683    pub(crate) fn run_render_phase(&mut self) -> FrameUpdateResult {
684        let _app_context = self.app.app_context.enter_scope();
685        self.run_render_phase_in_context(false)
686    }
687
688    fn run_render_phase_in_context(&mut self, recomposed_this_frame: bool) -> FrameUpdateResult {
689        let inspector_revision = if self.app.inspector_projector.is_some()
690            && self
691                .surfaces
692                .iter()
693                .any(|surface| surface.inspector.state.open)
694        {
695            self.semantics_snapshot_revision()
696        } else {
697            0
698        };
699        let context = &self.app.app_context;
700        context.tick_cursor_blink();
701        let render_dirty = context.take_render_invalidation();
702        let pointer_dirty = context.take_pointer_invalidation();
703        context.take_focus_invalidation();
704        for surface in &mut self.surfaces {
705            surface.scoped_draw_nodes.clear();
706            surface.scoped_layer_property_nodes.clear();
707            surface.structural_scene_nodes.clear();
708        }
709        self.app
710            .app_context
711            .take_draw_repass_nodes_into(&mut self.pending_dirty_nodes);
712        route_nodes_by_surface(
713            &mut self.app,
714            &mut self.surfaces,
715            self.pending_dirty_nodes.iter().copied(),
716            SurfaceDirtyLane::Draw,
717            &mut self.routing_scratch,
718        );
719        self.app
720            .app_context
721            .take_layer_property_repass_nodes_into(&mut self.pending_layer_property_nodes);
722        route_nodes_by_surface(
723            &mut self.app,
724            &mut self.surfaces,
725            self.pending_layer_property_nodes.iter().copied(),
726            SurfaceDirtyLane::LayerProperties,
727            &mut self.routing_scratch,
728        );
729        self.take_structural_change_parents();
730        route_nodes_by_surface(
731            &mut self.app,
732            &mut self.surfaces,
733            self.pending_dirty_nodes.iter().copied(),
734            SurfaceDirtyLane::Structural,
735            &mut self.routing_scratch,
736        );
737        self.pending_dirty_nodes.clear();
738        self.pending_layer_property_nodes.clear();
739        let _ = self.app.app_context.has_focused_field();
740        let attributed = self.surfaces.iter().any(|surface| {
741            !surface.scoped_draw_nodes.is_empty()
742                || !surface.scoped_layer_property_nodes.is_empty()
743                || !surface.structural_scene_nodes.is_empty()
744        }) || self.surfaces.iter().any(|surface| {
745            !surface.scoped_layout_scene_nodes.is_empty()
746                || !surface.scoped_moved_scene_nodes.is_empty()
747        });
748        let layer_properties_pending = self
749            .surfaces
750            .iter()
751            .any(|surface| !surface.scoped_layer_property_nodes.is_empty());
752        let frame = FrameDirt {
753            render_dirty,
754            pointer_dirty,
755            attributed,
756            layer_properties_pending,
757            recomposed_this_frame,
758        };
759
760        let mut result = FrameUpdateResult::default();
761        for surface in &mut self.surfaces {
762            let mut frame = render_surface(&mut self.app, surface, &frame);
763            frame.result.visual_changed |=
764                crate::inspector::refresh(&mut self.app, surface, inspector_revision);
765            surface.last_update = frame.result;
766            surface.frame_owed |= frame.result.visual_changed;
767            result.visual_changed |= frame.result.visual_changed;
768            result.structure_changed |= frame.result.structure_changed;
769            result.content_redrawn |= frame.rebuilt;
770        }
771        if result.content_redrawn {
772            self.prune_stale_draw_observations();
773        }
774        result.content_moved = std::mem::take(&mut self.app.content_moved);
775        result
776    }
777
778    /// Drops the draw observations of nodes no surface's render graph holds
779    /// any more, once every [`OBSERVATION_PRUNE_INTERVAL`] rebuilt frames:
780    /// finding them walks every node of every graph, and an observation
781    /// left behind in between costs no more than a redraw request for a
782    /// node nothing draws.
783    fn prune_stale_draw_observations(&mut self) {
784        self.app.rebuilt_frames_since_observation_prune += 1;
785        if self.app.rebuilt_frames_since_observation_prune < OBSERVATION_PRUNE_INTERVAL {
786            return;
787        }
788        self.app.rebuilt_frames_since_observation_prune = 0;
789        let mut retained_visual_nodes = HashSet::default();
790        let mut any_graph = false;
791        for surface in &mut self.surfaces {
792            any_graph |= surface
793                .renderer
794                .scene()
795                .collect_retained_visual_observation_nodes(&mut surface.retained_visual_nodes);
796            retained_visual_nodes.extend(surface.retained_visual_nodes.iter().copied());
797        }
798        if any_graph {
799            cranpose_ui::prune_draw_observations_to_nodes(&retained_visual_nodes);
800        }
801    }
802}
803
804/// Rebuilt frames between prunes of stale draw observations: once a second
805/// at 60 Hz.
806pub(crate) const OBSERVATION_PRUNE_INTERVAL: u32 = 60;
807
808fn render_surface<R>(
809    app: &mut ShellApp,
810    surface: &mut RootSurface<R>,
811    frame: &FrameDirt,
812) -> SurfaceFrame
813where
814    R: Renderer,
815    R::Error: Debug,
816{
817    let draw_dirty_nodes = std::mem::take(&mut surface.scoped_draw_nodes);
818    let layer_property_nodes = std::mem::take(&mut surface.scoped_layer_property_nodes);
819    let structural_parents = std::mem::take(&mut surface.structural_scene_nodes);
820    let draw_repass_pending = !draw_dirty_nodes.is_empty();
821    let mut draw_dirty_nodes = refresh_draw_nodes(app, surface, draw_dirty_nodes);
822    if frame.render_dirty && !frame.layer_properties_pending && !draw_repass_pending {
823        draw_dirty_nodes = refresh_retained_redraw_nodes(app, surface, draw_dirty_nodes);
824    }
825    let layout = ScopedLayoutNodes {
826        reshaped: std::mem::take(&mut surface.scoped_layout_scene_nodes),
827        moved: std::mem::take(&mut surface.scoped_moved_scene_nodes),
828    };
829    let partial_dirty_nodes = std::mem::take(&mut surface.partial_scene_nodes);
830    let scene_dirty = surface.scene_dirty;
831    let mut dirt = SurfaceDirt::classify(
832        frame,
833        scene_dirty,
834        draw_repass_pending,
835        ScopedVisualNodes {
836            draw: draw_dirty_nodes,
837            layers: layer_property_nodes,
838        },
839        layout,
840        structural_parents,
841        partial_dirty_nodes,
842    );
843
844    if !dirt.needs_scene_rebuild {
845        dirt.log(
846            frame,
847            scene_dirty,
848            if dirt.render_only_dirty {
849                "present-retained"
850            } else {
851                "skip"
852            },
853        );
854        surface.scoped_layout_scene_nodes = dirt.layout_dirty_nodes;
855        surface.scoped_moved_scene_nodes = dirt.moved_nodes;
856        surface.scoped_draw_nodes = dirt.draw_dirty_nodes;
857        surface.scoped_layer_property_nodes = dirt.layer_property_nodes;
858        surface.structural_scene_nodes = dirt.structural_parents;
859        surface.partial_scene_nodes = dirt.partial_dirty_nodes;
860        if dirt.render_only_dirty && app.dev_options.fps_counter {
861            draw_dev_overlay(app, surface);
862        }
863        return SurfaceFrame {
864            result: FrameUpdateResult {
865                visual_changed: dirt.render_only_dirty,
866                ..FrameUpdateResult::default()
867            },
868            rebuilt: false,
869        };
870    }
871
872    surface.scene_dirty = false;
873    let structure_changed = !dirt.render_only_dirty && !dirt.use_visual_update();
874    rebuild_surface_scene(app, surface, frame, scene_dirty, &dirt);
875    if dev_overlay_belongs_on(surface.id, app.dev_options.fps_counter) {
876        draw_dev_overlay(app, surface);
877    }
878    dirt.layout_dirty_nodes.clear();
879    dirt.moved_nodes.clear();
880    surface.scoped_moved_scene_nodes = dirt.moved_nodes;
881    surface.scoped_draw_nodes = dirt.draw_dirty_nodes;
882    surface.scoped_layer_property_nodes = dirt.layer_property_nodes;
883    surface.structural_scene_nodes = dirt.structural_parents;
884    surface.partial_scene_nodes = dirt.partial_dirty_nodes;
885    surface.scoped_layout_scene_nodes = dirt.layout_dirty_nodes;
886    SurfaceFrame {
887        result: FrameUpdateResult {
888            visual_changed: true,
889            structure_changed,
890            ..FrameUpdateResult::default()
891        },
892        rebuilt: true,
893    }
894}
895
896fn rebuild_surface_scene<R>(
897    app: &mut ShellApp,
898    surface: &mut RootSurface<R>,
899    frame: &FrameDirt,
900    scene_dirty: bool,
901    dirt: &SurfaceDirt,
902) where
903    R: Renderer,
904    R::Error: Debug,
905{
906    let viewport_size = surface.viewport_size();
907    let Some(root) = surface.root_node(app) else {
908        surface.renderer.scene_mut().clear();
909        return;
910    };
911    let mut applier = app.composition.applier_mut();
912    dirt.log(frame, scene_dirty, dirt.rebuild_path());
913    let rebuild_result = if dirt.use_visual_update() {
914        surface.renderer.update_visual_scene_from_applier(
915            &mut applier,
916            root,
917            viewport_size,
918            SceneUpdates {
919                content: &dirt.partial_dirty_nodes,
920                layers: &dirt.layer_property_nodes,
921                moved: &dirt.moved_nodes,
922            },
923        )
924    } else if dirt.use_partial_update() {
925        surface.renderer.update_scene_from_applier(
926            &mut applier,
927            root,
928            viewport_size,
929            SceneUpdates {
930                content: &dirt.partial_dirty_nodes,
931                layers: &dirt.layer_property_nodes,
932                moved: &dirt.moved_nodes,
933            },
934        )
935    } else {
936        surface
937            .renderer
938            .rebuild_scene_from_applier(&mut applier, root, viewport_size)
939    };
940    if let Err(err) = rebuild_result {
941        log::error!("renderer rebuild failed: {err:?}");
942        surface.renderer.scene_mut().clear();
943    }
944}
945
946fn dev_overlay_belongs_on(id: RootId, fps_counter: bool) -> bool {
947    fps_counter && matches!(id, RootId::Primary)
948}
949
950fn draw_dev_overlay<R: Renderer>(app: &ShellApp, surface: &mut RootSurface<R>) {
951    let viewport_size = surface.viewport_size();
952    surface.refresh_dev_overlay_text_for_frame_at(app, viewport_size, Instant::now());
953    surface
954        .renderer
955        .draw_dev_overlay(surface.dev_overlay_text.as_str(), viewport_size);
956}
957
958fn refresh_draw_nodes<R: Renderer>(
959    app: &mut ShellApp,
960    surface: &mut RootSurface<R>,
961    mut dirty_nodes: Vec<NodeId>,
962) -> Vec<NodeId> {
963    if dirty_nodes.is_empty() {
964        return dirty_nodes;
965    }
966    let Some(layout_tree) = surface.layout_tree.as_mut() else {
967        return dirty_nodes;
968    };
969
970    dirty_nodes.sort_unstable();
971    dirty_nodes.dedup();
972    let dirty_set: HashSet<NodeId> = dirty_nodes.iter().copied().collect();
973    let mut applier = app.composition.applier_mut();
974    let refresh_scope = build_draw_refresh_scope(&mut applier, &dirty_set);
975    refresh_layout_box_data(
976        &mut applier,
977        layout_tree.root_mut(),
978        &refresh_scope,
979        &dirty_set,
980    );
981    dirty_nodes
982}
983
984fn refresh_retained_redraw_nodes<R: Renderer>(
985    app: &mut ShellApp,
986    surface: &mut RootSurface<R>,
987    mut dirty_nodes: Vec<NodeId>,
988) -> Vec<NodeId> {
989    dirty_nodes.clear();
990    let Some(root) = surface.root_node(app) else {
991        return dirty_nodes;
992    };
993    {
994        let mut applier = app.composition.applier_mut();
995        collect_retained_redraw_nodes(
996            &mut applier,
997            &app.app_context,
998            root,
999            &mut dirty_nodes,
1000            &mut surface.redraw_walk,
1001        );
1002    }
1003    refresh_draw_nodes(app, surface, dirty_nodes)
1004}
1005
1006impl<R: Renderer> RootSurface<R> {
1007    fn refresh_dev_overlay_text_for_frame_at(
1008        &mut self,
1009        app: &ShellApp,
1010        viewport_size: Size,
1011        now: Instant,
1012    ) {
1013        if !self.dev_overlay_text_needs_refresh(viewport_size, now) {
1014            return;
1015        }
1016        self.dev_overlay_text = self.build_dev_overlay_text(app, viewport_size);
1017        self.dev_overlay_last_refresh = Some(now);
1018        self.dev_overlay_viewport = Some(viewport_size);
1019    }
1020
1021    fn dev_overlay_text_needs_refresh(&self, viewport_size: Size, now: Instant) -> bool {
1022        if self.dev_overlay_text.is_empty() || self.dev_overlay_viewport != Some(viewport_size) {
1023            return true;
1024        }
1025
1026        self.dev_overlay_last_refresh.is_none_or(|last| {
1027            now.checked_duration_since(last).unwrap_or_default() >= DEV_OVERLAY_REFRESH_INTERVAL
1028        })
1029    }
1030
1031    fn build_dev_overlay_text(&mut self, app: &ShellApp, viewport_size: Size) -> String {
1032        self.dev_overlay_controls.clear();
1033
1034        let stats = app.fps_monitor.stats();
1035        // Each field with its rank: a viewport too narrow for the whole line,
1036        // such as a phone browser's, drops the highest ranks first.
1037        let fields = [
1038            (0, format!("{:.0} FPS", stats.fps)),
1039            (3, format!("avg {:.1}ms", stats.avg_ms)),
1040            (2, format!("p95 {:.1}ms", stats.p95_ms)),
1041            (4, format!("max {:.1}ms", stats.max_ms)),
1042            (1, format!("work {:.1}ms", stats.work_avg_ms)),
1043            (5, format!("{} recomp/s", stats.recomps_per_second)),
1044        ];
1045        let line_len = |max_rank: u8| {
1046            let (count, chars) = fields
1047                .iter()
1048                .filter(|(rank, _)| *rank <= max_rank)
1049                .fold((0_usize, 0_usize), |(count, chars), (_, field)| {
1050                    (count + 1, chars + field.len())
1051                });
1052            chars + count.saturating_sub(1) * DEV_OVERLAY_SEPARATOR.len()
1053        };
1054        let fitting_chars = ((viewport_size.width - DEV_OVERLAY_PADDING * 2.0)
1055            / DEV_OVERLAY_CHAR_WIDTH)
1056            .max(0.0) as usize;
1057        let controls_chars = if app.dev_options.frame_pacing_controls {
1058            DEV_OVERLAY_SEPARATOR.len() + frame_pacing_controls_len()
1059        } else {
1060            0
1061        };
1062        let mut max_rank = 5;
1063        while max_rank > 0 && line_len(max_rank) + controls_chars > fitting_chars {
1064            max_rank -= 1;
1065        }
1066        let mut text = String::with_capacity(line_len(max_rank) + controls_chars);
1067        for (_, field) in fields.iter().filter(|(rank, _)| *rank <= max_rank) {
1068            if !text.is_empty() {
1069                text.push_str(DEV_OVERLAY_SEPARATOR);
1070            }
1071            text.push_str(field);
1072        }
1073
1074        if !app.dev_options.frame_pacing_controls {
1075            return text;
1076        }
1077
1078        text.push_str(DEV_OVERLAY_SEPARATOR);
1079        let mut controls = Vec::with_capacity(FramePacingMode::ALL.len());
1080        for (index, mode) in FramePacingMode::ALL.into_iter().enumerate() {
1081            if index > 0 {
1082                text.push(' ');
1083            }
1084            let start = text.len();
1085            if mode == app.dev_options.frame_pacing_mode {
1086                text.push('[');
1087                text.push_str(mode.label());
1088                text.push(']');
1089            } else {
1090                text.push_str(mode.label());
1091            }
1092            controls.push((start, text.len(), mode));
1093        }
1094
1095        let overlay_width = text.len() as f32 * DEV_OVERLAY_CHAR_WIDTH;
1096        let overlay_x = (viewport_size.width - overlay_width - DEV_OVERLAY_PADDING * 2.0)
1097            .max(DEV_OVERLAY_PADDING);
1098        let overlay_y = DEV_OVERLAY_PADDING;
1099        let text_x = overlay_x + DEV_OVERLAY_PADDING / 2.0;
1100        let text_y = overlay_y + DEV_OVERLAY_PADDING / 4.0;
1101        let text_height = DEV_OVERLAY_FONT_SIZE * 1.4;
1102
1103        self.dev_overlay_controls = controls
1104            .into_iter()
1105            .map(|(start, end, mode)| DevOverlayControl {
1106                bounds: Rect {
1107                    x: text_x + start as f32 * DEV_OVERLAY_CHAR_WIDTH - 3.0,
1108                    y: text_y - 3.0,
1109                    width: (end - start) as f32 * DEV_OVERLAY_CHAR_WIDTH + 6.0,
1110                    height: text_height + 6.0,
1111                },
1112                mode,
1113            })
1114            .collect();
1115
1116        text
1117    }
1118}
1119
1120/// The length of the frame pacing controls: every mode's label, a space
1121/// between labels and brackets around the selected one.
1122fn frame_pacing_controls_len() -> usize {
1123    let labels: usize = FramePacingMode::ALL
1124        .iter()
1125        .map(|mode| mode.label().len())
1126        .sum();
1127    labels + FramePacingMode::ALL.len() - 1 + 2
1128}
1129
1130fn clear_dispatch_invalidation(
1131    applier: &mut MemoryApplier,
1132    node_id: NodeId,
1133    invalidation: DispatchInvalidationKind,
1134) -> Result<bool, NodeError> {
1135    match invalidation {
1136        DispatchInvalidationKind::Pointer => {
1137            match applier.with_node::<LayoutNode, _>(node_id, |node| {
1138                let needs_pointer_pass = node.needs_pointer_pass();
1139                if needs_pointer_pass {
1140                    node.clear_needs_pointer_pass();
1141                }
1142                needs_pointer_pass
1143            }) {
1144                Ok(cleared) => Ok(cleared),
1145                Err(NodeError::TypeMismatch { .. }) => applier
1146                    .with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
1147                        let needs_pointer_pass = node.needs_pointer_pass();
1148                        if needs_pointer_pass {
1149                            node.clear_needs_pointer_pass();
1150                        }
1151                        needs_pointer_pass
1152                    }),
1153                Err(err) => Err(err),
1154            }
1155        }
1156        DispatchInvalidationKind::Focus => {
1157            match applier.with_node::<LayoutNode, _>(node_id, |node| {
1158                let needs_focus_sync = node.needs_focus_sync();
1159                if needs_focus_sync {
1160                    node.clear_needs_focus_sync();
1161                }
1162                needs_focus_sync
1163            }) {
1164                Ok(cleared) => Ok(cleared),
1165                Err(NodeError::TypeMismatch { .. }) => applier
1166                    .with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
1167                        let needs_focus_sync = node.needs_focus_sync();
1168                        if needs_focus_sync {
1169                            node.clear_needs_focus_sync();
1170                        }
1171                        needs_focus_sync
1172                    }),
1173                Err(err) => Err(err),
1174            }
1175        }
1176    }
1177}
1178
1179pub(crate) fn build_draw_refresh_scope(
1180    applier: &mut MemoryApplier,
1181    dirty_nodes: &HashSet<NodeId>,
1182) -> HashSet<NodeId> {
1183    let mut refresh_scope =
1184        HashSet::with_capacity_and_hasher(dirty_nodes.len(), Default::default());
1185    for &dirty_node in dirty_nodes {
1186        let mut current = Some(dirty_node);
1187        while let Some(node_id) = current {
1188            if !refresh_scope.insert(node_id) {
1189                break;
1190            }
1191            current = applier.get_mut(node_id).ok().and_then(|node| node.parent());
1192        }
1193    }
1194    refresh_scope
1195}
1196
1197/// Collects into `dirty_nodes` the nodes under `root` whose redraw flag was
1198/// set before they had an id, clearing the flags: a draw repass names every
1199/// other node whose content changed. A node under another window root, or
1200/// not attached yet, stays noted in `context` for a later frame. `pending`
1201/// keeps its storage for the next frame.
1202fn collect_retained_redraw_nodes(
1203    applier: &mut MemoryApplier,
1204    context: &cranpose_ui::AppContext,
1205    root: NodeId,
1206    dirty_nodes: &mut Vec<NodeId>,
1207    pending: &mut Vec<NodeId>,
1208) {
1209    pending.clear();
1210    context.take_retained_redraw_nodes_into(pending);
1211    for &id in pending.iter() {
1212        match redraw_place(applier, id, root) {
1213            RedrawPlace::Here => {
1214                if take_redraw_flag(applier, id) {
1215                    dirty_nodes.push(id);
1216                }
1217            }
1218            RedrawPlace::Elsewhere => context.keep_retained_redraw_node(id),
1219            RedrawPlace::Gone => {}
1220        }
1221    }
1222}
1223
1224/// Where a node noted for a redraw lies for the surface of `root`.
1225enum RedrawPlace {
1226    /// Under `root` and under no other window root: the surface takes it.
1227    Here,
1228    /// Under another window root, or not attached to a root yet.
1229    Elsewhere,
1230    /// The node is gone.
1231    Gone,
1232}
1233
1234fn redraw_place(applier: &mut MemoryApplier, id: NodeId, root: NodeId) -> RedrawPlace {
1235    let mut current = id;
1236    loop {
1237        if current == root {
1238            return RedrawPlace::Here;
1239        }
1240        let Ok(node) = applier.get_mut(current) else {
1241            return if current == id {
1242                RedrawPlace::Gone
1243            } else {
1244                RedrawPlace::Elsewhere
1245            };
1246        };
1247        let parent = node.parent();
1248        if node
1249            .as_any_mut()
1250            .downcast_mut::<LayoutNode>()
1251            .is_some_and(|layout| layout.is_window_root())
1252        {
1253            return RedrawPlace::Elsewhere;
1254        }
1255        let Some(parent) = parent else {
1256            return RedrawPlace::Elsewhere;
1257        };
1258        current = parent;
1259    }
1260}
1261
1262/// Clears the redraw flag of the layout or subcompose node `id`, and returns
1263/// whether it was set.
1264fn take_redraw_flag(applier: &mut MemoryApplier, id: NodeId) -> bool {
1265    let Ok(node) = applier.get_mut(id) else {
1266        return false;
1267    };
1268    let any = node.as_any_mut();
1269    if let Some(layout) = any.downcast_mut::<LayoutNode>() {
1270        let redraw = layout.needs_redraw();
1271        layout.clear_needs_redraw();
1272        return redraw;
1273    }
1274    match any.downcast_mut::<SubcomposeLayoutNode>() {
1275        Some(subcompose) => {
1276            let redraw = subcompose.needs_redraw();
1277            subcompose.clear_needs_redraw();
1278            redraw
1279        }
1280        None => false,
1281    }
1282}
1283
1284fn refresh_layout_box_data(
1285    applier: &mut MemoryApplier,
1286    layout: &mut cranpose_ui::layout::LayoutBox,
1287    refresh_scope: &HashSet<NodeId>,
1288    dirty_nodes: &HashSet<NodeId>,
1289) {
1290    if !refresh_scope.contains(&layout.node_id) {
1291        return;
1292    }
1293
1294    if dirty_nodes.contains(&layout.node_id) {
1295        let refreshed = applier
1296            .with_node::<LayoutNode, _>(layout.node_id, |node| {
1297                node.clear_needs_redraw();
1298                (
1299                    node.modifier.clone(),
1300                    node.resolved_modifiers(),
1301                    node.modifier_slices_snapshot(),
1302                    node.semantics_configuration(),
1303                )
1304            })
1305            .or_else(|_| {
1306                applier.with_node::<SubcomposeLayoutNode, _>(layout.node_id, |node| {
1307                    node.clear_needs_redraw();
1308                    (
1309                        node.modifier(),
1310                        node.resolved_modifiers(),
1311                        node.modifier_slices_snapshot(),
1312                        node.semantics_configuration(),
1313                    )
1314                })
1315            });
1316        if let Ok((modifier, resolved_modifiers, slices, semantics)) = refreshed {
1317            layout.node_data.modifier = modifier;
1318            layout.node_data.resolved_modifiers = resolved_modifiers;
1319            layout.node_data.modifier_slices = slices;
1320            layout.node_data.semantics = semantics.map(std::rc::Rc::new);
1321        }
1322    }
1323
1324    for child in &mut layout.children {
1325        refresh_layout_box_data(applier, child, refresh_scope, dirty_nodes);
1326    }
1327}
1328
1329#[cfg(test)]
1330#[path = "tests/shell_frame_tests.rs"]
1331mod tests;