Skip to main content

fret_ui/tree/layout/
entrypoints.rs

1use super::*;
2
3use crate::layout_constraints::LayoutSize;
4use crate::layout_constraints::{AvailableSpace, LayoutConstraints};
5use crate::layout_engine::build_viewport_flow_subtree;
6use crate::layout_pass::LayoutPassKind;
7
8impl<H: UiHost> UiTree<H> {
9    pub fn layout_all(
10        &mut self,
11        app: &mut H,
12        services: &mut dyn UiServices,
13        bounds: Rect,
14        scale_factor: f32,
15    ) {
16        self.layout_all_with_pass_kind(app, services, bounds, scale_factor, LayoutPassKind::Final);
17    }
18
19    #[stacksafe::stacksafe]
20    pub(crate) fn layout_all_with_pass_kind(
21        &mut self,
22        app: &mut H,
23        services: &mut dyn UiServices,
24        bounds: Rect,
25        scale_factor: f32,
26        pass_kind: LayoutPassKind,
27    ) {
28        if pass_kind == LayoutPassKind::Final
29            && let Some(window) = self.window
30        {
31            let frame_id = app.frame_id();
32            app.with_global_mut_untracked(
33                fret_core::WindowFrameClockService::default,
34                |svc, _host| svc.record_frame(window, frame_id),
35            );
36        }
37
38        let profile_layout_all = crate::runtime_config::ui_runtime_config().layout_all_profile
39            && pass_kind == LayoutPassKind::Final;
40        let profile_started = profile_layout_all.then(Instant::now);
41        let mut t_invalidate_scroll_handle_bindings: Option<Duration> = None;
42        let mut t_expand_view_cache_invalidations: Option<Duration> = None;
43        let mut t_request_build_roots: Option<Duration> = None;
44        let mut t_layout_roots: Option<Duration> = None;
45        let mut t_pending_barriers: Option<Duration> = None;
46        let mut t_repair_view_cache_bounds: Option<Duration> = None;
47        let mut t_layout_contained_view_cache_roots: Option<Duration> = None;
48        let mut t_collapse_layout_observations: Option<Duration> = None;
49        let mut t_refresh_semantics: Option<Duration> = None;
50        let mut t_prepaint_after_layout: Option<Duration> = None;
51        let mut t_flush_deferred_cleanup: Option<Duration> = None;
52
53        if pass_kind == LayoutPassKind::Final {
54            self.layout_node_profile = LayoutNodeProfileConfig::from_env()
55                .map(|cfg| LayoutNodeProfileState::new(cfg, app.frame_id()));
56            self.measure_node_profile = MeasureNodeProfileConfig::from_env()
57                .map(|cfg| MeasureNodeProfileState::new(cfg, app.frame_id()));
58        } else {
59            self.layout_node_profile = None;
60            self.measure_node_profile = None;
61        }
62
63        self.measure_cache_this_frame.clear();
64        self.scratch_bounds_records.clear();
65
66        if pass_kind == LayoutPassKind::Final {
67            self.update_interactive_resize_state_for_layout(app.frame_id(), bounds, scale_factor);
68            self.prune_detached_layout_followups();
69        }
70        let force_post_resize_rebuild =
71            pass_kind == LayoutPassKind::Final && self.interactive_resize_requires_full_rebuild();
72
73        let started = self.debug_enabled.then(Instant::now);
74        if self.debug_enabled {
75            self.begin_debug_frame_if_needed(app.frame_id());
76            self.debug_stats.frame_id = app.frame_id();
77            self.debug_stats.layout_nodes_visited = 0;
78            self.debug_stats.layout_nodes_performed = 0;
79            self.debug_stats.layout_engine_solves = 0;
80            self.debug_stats.layout_engine_solve_time = Duration::default();
81            self.debug_stats.layout_engine_child_rect_queries = 0;
82            self.debug_stats.layout_engine_child_rect_time = Duration::default();
83            self.debug_stats.layout_engine_widget_fallback_solves = 0;
84            self.debug_stats.layout_collect_roots_time = Duration::default();
85            self.debug_stats
86                .layout_invalidate_scroll_handle_bindings_time = Duration::default();
87            self.debug_stats.layout_expand_view_cache_invalidations_time = Duration::default();
88            self.debug_stats.layout_request_build_roots_time = Duration::default();
89            self.debug_stats.layout_pending_barrier_relayouts_time = Duration::default();
90            self.debug_stats.layout_repair_view_cache_bounds_time = Duration::default();
91            self.debug_stats.layout_contained_view_cache_roots_time = Duration::default();
92            self.debug_stats.layout_collapse_layout_observations_time = Duration::default();
93            self.debug_stats.layout_observation_record_time = Duration::default();
94            self.debug_stats.layout_observation_record_models_items = 0;
95            self.debug_stats.layout_observation_record_globals_items = 0;
96            self.debug_stats.layout_prepaint_after_layout_time = Duration::default();
97            self.debug_stats.layout_skipped_engine_frame = false;
98            self.debug_stats.layout_fast_path_taken = false;
99            self.debug_stats.layout_invalidations_count = self.layout_invalidations_count;
100            self.debug_stats.view_cache_active = self.view_cache_active();
101            self.debug_stats.focus = self.focus;
102            self.debug_stats.captured = self.captured_for(fret_core::PointerId(0));
103        }
104
105        let roots_started = self.debug_enabled.then(Instant::now);
106        let roots: Vec<NodeId> = self
107            .visible_layers_in_paint_order()
108            .map(|layer| self.layers[layer].root)
109            .collect();
110        if let Some(roots_started) = roots_started {
111            self.debug_stats.layout_collect_roots_time += roots_started.elapsed();
112        }
113
114        let roots_len = roots.len();
115        let trace_layout = tracing::enabled!(tracing::Level::TRACE);
116
117        let mut viewport_cursor: usize = 0;
118
119        let layout_phase_time_enabled = self.debug_enabled || profile_layout_all;
120        let window = self.window;
121        let frame_id = app.frame_id();
122        let (_, invalidate_elapsed) = fret_perf::measure_span(
123            layout_phase_time_enabled,
124            trace_layout,
125            || {
126                tracing::trace_span!(
127                    "fret.ui.layout.invalidate_scroll_handle_bindings",
128                    window = ?window,
129                    frame_id = frame_id.0,
130                    pass_kind = ?pass_kind,
131                )
132            },
133            || {
134                self.invalidate_scroll_handle_bindings_for_changed_handles(
135                    app, pass_kind, true, true,
136                )
137            },
138        );
139        if profile_layout_all {
140            t_invalidate_scroll_handle_bindings = invalidate_elapsed;
141        }
142        if self.debug_enabled
143            && let Some(invalidate_elapsed) = invalidate_elapsed
144        {
145            self.debug_stats
146                .layout_invalidate_scroll_handle_bindings_time += invalidate_elapsed;
147        }
148
149        let any_root_needs_layout_or_bounds = roots.iter().any(|&root| {
150            self.nodes
151                .get(root)
152                .is_some_and(|node| node.invalidation.layout || node.bounds != bounds)
153        });
154        let any_pending_barrier_needs_layout = self.pending_barrier_relayouts.iter().any(|&root| {
155            self.node_is_attached_to_layer_tree(root)
156                && self
157                    .nodes
158                    .get(root)
159                    .is_some_and(|node| node.invalidation.layout)
160        });
161        let any_view_cache_root_needs_layout = self.view_cache_active()
162            && self.nodes.iter().any(|(id, node)| {
163                self.node_is_attached_to_layer_tree(id)
164                    && node.view_cache.enabled
165                    && node.invalidation.layout
166            });
167
168        if pass_kind == LayoutPassKind::Final
169            && !any_root_needs_layout_or_bounds
170            && !any_view_cache_root_needs_layout
171            && !any_pending_barrier_needs_layout
172            && self.invalidated_paint_nodes == 0
173            && self.invalidated_hit_test_nodes == 0
174            && !force_post_resize_rebuild
175        {
176            self.pending_barrier_relayouts.retain(|&root| {
177                self.nodes
178                    .get(root)
179                    .is_some_and(|node| node.invalidation.layout)
180            });
181            let prepaint_started = self.debug_enabled.then(Instant::now);
182            self.prepaint_after_layout_stable_frame(app);
183            if let Some(prepaint_started) = prepaint_started {
184                self.debug_stats.layout_prepaint_after_layout_time += prepaint_started.elapsed();
185            }
186            self.debug_stats.layout_skipped_engine_frame = true;
187
188            let focus_started = self.debug_enabled.then(Instant::now);
189            self.resolve_pending_focus_target_if_needed(app);
190            self.repair_focus_node_from_focused_element_if_needed(app);
191            if let Some(focus_started) = focus_started {
192                self.debug_stats.layout_focus_repair_time += focus_started.elapsed();
193            }
194
195            if self.semantics_requested {
196                let semantics_started = self.debug_enabled.then(Instant::now);
197                self.semantics_requested = false;
198                self.refresh_semantics_snapshot(app);
199                if let Some(semantics_started) = semantics_started {
200                    self.debug_stats.layout_semantics_refresh_time += semantics_started.elapsed();
201                }
202            }
203
204            let deferred_cleanup_started = self.debug_enabled.then(Instant::now);
205            self.flush_deferred_cleanup(services);
206            if let Some(deferred_cleanup_started) = deferred_cleanup_started {
207                self.debug_stats.layout_deferred_cleanup_time += deferred_cleanup_started.elapsed();
208            }
209            self.last_layout_frame_id = Some(app.frame_id());
210            self.refine_pending_window_runtime_snapshots_after_layout(app);
211            if let Some(started) = started {
212                self.debug_stats.layout_time = started
213                    .elapsed()
214                    .saturating_sub(self.debug_stats.layout_prepaint_after_layout_time);
215            }
216            return;
217        }
218
219        if pass_kind == LayoutPassKind::Final {
220            let (_, expand_elapsed) = fret_perf::measure_span(
221                layout_phase_time_enabled,
222                trace_layout,
223                || {
224                    tracing::trace_span!(
225                        "fret.ui.layout.expand_view_cache_invalidations",
226                        window = ?window,
227                        frame_id = frame_id.0,
228                        pass_kind = ?pass_kind,
229                    )
230                },
231                || self.expand_view_cache_layout_invalidations_if_needed(),
232            );
233            if profile_layout_all {
234                t_expand_view_cache_invalidations = expand_elapsed;
235            }
236            if self.debug_enabled
237                && let Some(expand_elapsed) = expand_elapsed
238            {
239                self.debug_stats.layout_expand_view_cache_invalidations_time += expand_elapsed;
240            }
241        }
242
243        // Fast path (ADR 0175): if nothing requires layout this frame, skip the layout engine and
244        // only run prepaint/semantics. This keeps scroll-only and cache-hit frames cheap while
245        // still allowing prepaint-windowed surfaces to update their ephemeral outputs.
246        if pass_kind == LayoutPassKind::Final
247            && self.pending_barrier_relayouts.is_empty()
248            && self.last_layout_bounds == Some(bounds)
249            && self.last_layout_scale_factor == Some(scale_factor)
250            && !self.any_attached_layout_invalidations()
251            && !force_post_resize_rebuild
252        {
253            self.debug_stats.layout_fast_path_taken = true;
254            self.prepaint_after_layout(app, scale_factor);
255
256            self.repair_focus_node_from_focused_element_if_needed(app);
257
258            if self.semantics_requested {
259                self.semantics_requested = false;
260                self.refresh_semantics_snapshot(app);
261            }
262            self.flush_deferred_cleanup(services);
263            self.last_layout_frame_id = Some(app.frame_id());
264            self.refine_pending_window_runtime_snapshots_after_layout(app);
265
266            self.last_layout_bounds = Some(bounds);
267            self.last_layout_scale_factor = Some(scale_factor);
268
269            if let Some(started) = started {
270                self.debug_stats.layout_time = started.elapsed();
271            }
272            return;
273        }
274
275        let (layout_engine_solves_start, layout_engine_solve_time_start) = {
276            self.begin_layout_engine_frame(app);
277            if self.debug_enabled {
278                (
279                    self.layout_engine.solve_count(),
280                    self.layout_engine.last_solve_time(),
281                )
282            } else {
283                (0, Duration::default())
284            }
285        };
286
287        let (_, request_build_elapsed) = fret_perf::measure_span(
288            layout_phase_time_enabled,
289            trace_layout,
290            || {
291                tracing::trace_span!(
292                    "fret.ui.layout.request_build_roots",
293                    window = ?window,
294                    frame_id = frame_id.0,
295                    pass_kind = ?pass_kind,
296                    roots_len,
297                )
298            },
299            || {
300                self.request_build_window_roots_if_final(
301                    app,
302                    services,
303                    &roots,
304                    bounds,
305                    scale_factor,
306                    pass_kind,
307                );
308            },
309        );
310        if profile_layout_all {
311            t_request_build_roots = request_build_elapsed;
312        }
313        if self.debug_enabled
314            && let Some(request_build_elapsed) = request_build_elapsed
315        {
316            self.debug_stats.layout_request_build_roots_time += request_build_elapsed;
317        }
318
319        let (_, roots_elapsed) = fret_perf::measure_span(
320            layout_phase_time_enabled,
321            trace_layout,
322            || {
323                tracing::trace_span!(
324                    "fret.ui.layout.roots",
325                    window = ?window,
326                    frame_id = frame_id.0,
327                    pass_kind = ?pass_kind,
328                    roots_len,
329                )
330            },
331            || {
332                for root in roots {
333                    let _ = self.layout_in_with_pass_kind(
334                        app,
335                        services,
336                        root,
337                        bounds,
338                        scale_factor,
339                        pass_kind,
340                        crate::layout::overflow::LayoutOverflowContext::default(),
341                    );
342
343                    self.flush_viewport_roots_after_root(
344                        app,
345                        services,
346                        scale_factor,
347                        pass_kind,
348                        &mut viewport_cursor,
349                    );
350                }
351            },
352        );
353        if profile_layout_all {
354            t_layout_roots = roots_elapsed;
355        }
356        if self.debug_enabled
357            && let Some(roots_elapsed) = roots_elapsed
358        {
359            self.debug_stats.layout_roots_time += roots_elapsed;
360        }
361
362        if pass_kind == LayoutPassKind::Final {
363            let (_, barrier_elapsed) = fret_perf::measure_span(
364                layout_phase_time_enabled,
365                trace_layout,
366                || {
367                    tracing::trace_span!(
368                        "fret.ui.layout.pending_barriers",
369                        window = ?window,
370                        frame_id = frame_id.0,
371                        pass_kind = ?pass_kind,
372                    )
373                },
374                || {
375                    self.layout_pending_barrier_relayouts_if_needed(
376                        app,
377                        services,
378                        scale_factor,
379                        pass_kind,
380                        &mut viewport_cursor,
381                    );
382                },
383            );
384            if profile_layout_all {
385                t_pending_barriers = barrier_elapsed;
386            }
387            if self.debug_enabled
388                && let Some(barrier_elapsed) = barrier_elapsed
389            {
390                self.debug_stats.layout_barrier_relayouts_time += barrier_elapsed;
391                self.debug_stats.layout_pending_barrier_relayouts_time += barrier_elapsed;
392            }
393        }
394
395        if pass_kind == LayoutPassKind::Final {
396            let (_, view_cache_elapsed) = fret_perf::measure_span(
397                layout_phase_time_enabled,
398                trace_layout,
399                || {
400                    tracing::trace_span!(
401                        "fret.ui.layout.view_cache",
402                        window = ?window,
403                        frame_id = frame_id.0,
404                        pass_kind = ?pass_kind,
405                    )
406                },
407                || {
408                    let (_, repair_elapsed) = fret_perf::measure_span(
409                        layout_phase_time_enabled,
410                        trace_layout,
411                        || {
412                            tracing::trace_span!(
413                                "fret.ui.layout.view_cache.repair_bounds",
414                                window = ?window,
415                                frame_id = frame_id.0,
416                                pass_kind = ?pass_kind,
417                            )
418                        },
419                        || self.repair_view_cache_root_bounds_from_engine_if_needed(app),
420                    );
421                    if profile_layout_all {
422                        t_repair_view_cache_bounds = repair_elapsed;
423                    }
424                    if self.debug_enabled
425                        && let Some(repair_elapsed) = repair_elapsed
426                    {
427                        self.debug_stats.layout_repair_view_cache_bounds_time += repair_elapsed;
428                    }
429
430                    let (_, contained_elapsed) = fret_perf::measure_span(
431                        layout_phase_time_enabled,
432                        trace_layout,
433                        || {
434                            tracing::trace_span!(
435                                "fret.ui.layout.view_cache.layout_contained_roots",
436                                window = ?window,
437                                frame_id = frame_id.0,
438                                pass_kind = ?pass_kind,
439                            )
440                        },
441                        || {
442                            self.layout_contained_view_cache_roots_if_needed(
443                                app,
444                                services,
445                                scale_factor,
446                                pass_kind,
447                                &mut viewport_cursor,
448                            );
449                        },
450                    );
451                    if profile_layout_all {
452                        t_layout_contained_view_cache_roots = contained_elapsed;
453                    }
454                    if self.debug_enabled
455                        && let Some(contained_elapsed) = contained_elapsed
456                    {
457                        self.debug_stats.layout_contained_view_cache_roots_time +=
458                            contained_elapsed;
459                    }
460
461                    let (_, collapse_elapsed) = fret_perf::measure_span(
462                        layout_phase_time_enabled,
463                        trace_layout,
464                        || {
465                            tracing::trace_span!(
466                                "fret.ui.layout.view_cache.collapse_observations",
467                                window = ?window,
468                                frame_id = frame_id.0,
469                                pass_kind = ?pass_kind,
470                            )
471                        },
472                        || self.collapse_layout_observations_to_view_cache_roots_if_needed(),
473                    );
474                    if profile_layout_all {
475                        t_collapse_layout_observations = collapse_elapsed;
476                    }
477                    if self.debug_enabled
478                        && let Some(collapse_elapsed) = collapse_elapsed
479                    {
480                        self.debug_stats.layout_collapse_layout_observations_time +=
481                            collapse_elapsed;
482                    }
483                },
484            );
485            if self.debug_enabled
486                && let Some(view_cache_elapsed) = view_cache_elapsed
487            {
488                self.debug_stats.layout_view_cache_time += view_cache_elapsed;
489            }
490        }
491
492        if pass_kind == LayoutPassKind::Final {
493            self.flush_layout_bounds_records_if_needed(app);
494            let (_, prepaint_elapsed) = fret_perf::measure_span(
495                layout_phase_time_enabled,
496                trace_layout,
497                || {
498                    tracing::trace_span!(
499                        "fret.ui.layout.prepaint_after_layout",
500                        window = ?window,
501                        frame_id = frame_id.0,
502                        pass_kind = ?pass_kind,
503                    )
504                },
505                || self.prepaint_after_layout(app, scale_factor),
506            );
507            if profile_layout_all {
508                t_prepaint_after_layout = prepaint_elapsed;
509            }
510            if self.debug_enabled
511                && let Some(prepaint_elapsed) = prepaint_elapsed
512            {
513                self.debug_stats.layout_prepaint_after_layout_time += prepaint_elapsed;
514            }
515        }
516        if pass_kind == LayoutPassKind::Final {
517            let (_, focus_elapsed) = fret_perf::measure_span(
518                self.debug_enabled,
519                trace_layout,
520                || {
521                    tracing::trace_span!(
522                        "fret.ui.layout.focus_repair",
523                        window = ?window,
524                        frame_id = frame_id.0,
525                        pass_kind = ?pass_kind,
526                    )
527                },
528                || {
529                    self.resolve_pending_focus_target_if_needed(app);
530                    self.repair_focus_node_from_focused_element_if_needed(app)
531                },
532            );
533            if let Some(focus_elapsed) = focus_elapsed {
534                self.debug_stats.layout_focus_repair_time += focus_elapsed;
535            }
536        }
537
538        if self.semantics_requested {
539            let (_, semantics_elapsed) = fret_perf::measure_span(
540                layout_phase_time_enabled,
541                trace_layout,
542                || {
543                    tracing::trace_span!(
544                        "fret.ui.layout.refresh_semantics",
545                        window = ?window,
546                        frame_id = frame_id.0,
547                        pass_kind = ?pass_kind,
548                    )
549                },
550                || {
551                    self.semantics_requested = false;
552                    self.refresh_semantics_snapshot(app);
553                },
554            );
555            if profile_layout_all {
556                t_refresh_semantics = semantics_elapsed;
557            }
558            if self.debug_enabled
559                && let Some(semantics_elapsed) = semantics_elapsed
560            {
561                self.debug_stats.layout_semantics_refresh_time += semantics_elapsed;
562            }
563        }
564        let (_, deferred_cleanup_elapsed) = fret_perf::measure_span(
565            layout_phase_time_enabled,
566            trace_layout,
567            || {
568                tracing::trace_span!(
569                    "fret.ui.layout.flush_deferred_cleanup",
570                    window = ?window,
571                    frame_id = frame_id.0,
572                    pass_kind = ?pass_kind,
573                )
574            },
575            || self.flush_deferred_cleanup(services),
576        );
577        if profile_layout_all {
578            t_flush_deferred_cleanup = deferred_cleanup_elapsed;
579        }
580        if self.debug_enabled
581            && let Some(deferred_cleanup_elapsed) = deferred_cleanup_elapsed
582        {
583            self.debug_stats.layout_deferred_cleanup_time += deferred_cleanup_elapsed;
584        }
585
586        // layout_time is computed below, and should exclude prepaint_after_layout time (since that
587        // work is accounted separately and runs even on "layout fast path" frames).
588
589        if let Some(started) = started {
590            self.debug_stats.layout_time = started
591                .elapsed()
592                .saturating_sub(self.debug_stats.layout_prepaint_after_layout_time);
593        }
594
595        if pass_kind == LayoutPassKind::Final {
596            self.finish_final_layout_frame(app);
597        }
598
599        if pass_kind == LayoutPassKind::Final {
600            self.last_layout_bounds = Some(bounds);
601            self.last_layout_scale_factor = Some(scale_factor);
602        }
603
604        if self.debug_enabled {
605            self.debug_stats.layout_engine_solves = self
606                .layout_engine
607                .solve_count()
608                .saturating_sub(layout_engine_solves_start);
609            self.debug_stats.layout_engine_solve_time = self
610                .layout_engine
611                .last_solve_time()
612                .saturating_sub(layout_engine_solve_time_start);
613        }
614
615        if let Some(started) = profile_started {
616            let total = started.elapsed();
617            tracing::info!(
618                window = ?self.window,
619                total_ms = total.as_millis(),
620                invalidate_scroll_handle_bindings_ms =
621                    t_invalidate_scroll_handle_bindings.map(|d| d.as_millis()),
622                expand_view_cache_invalidations_ms =
623                    t_expand_view_cache_invalidations.map(|d| d.as_millis()),
624                request_build_roots_ms = t_request_build_roots.map(|d| d.as_millis()),
625                layout_roots_ms = t_layout_roots.map(|d| d.as_millis()),
626                pending_barriers_ms = t_pending_barriers.map(|d| d.as_millis()),
627                repair_view_cache_bounds_ms = t_repair_view_cache_bounds.map(|d| d.as_millis()),
628                layout_contained_view_cache_roots_ms =
629                    t_layout_contained_view_cache_roots.map(|d| d.as_millis()),
630                collapse_layout_observations_ms =
631                    t_collapse_layout_observations.map(|d| d.as_millis()),
632                refresh_semantics_ms = t_refresh_semantics.map(|d| d.as_millis()),
633                prepaint_after_layout_ms = t_prepaint_after_layout.map(|d| d.as_millis()),
634                flush_deferred_cleanup_ms = t_flush_deferred_cleanup.map(|d| d.as_millis()),
635                layout_nodes_performed = self.debug_stats.layout_nodes_performed,
636                "layout_all profile"
637            );
638        }
639
640        if pass_kind == LayoutPassKind::Final {
641            self.emit_layout_node_profile(app);
642            self.emit_measure_node_profile(app);
643        }
644    }
645
646    fn emit_layout_node_profile(&mut self, app: &mut H) {
647        let Some(profile) = self.layout_node_profile.take() else {
648            return;
649        };
650        if profile.entries.is_empty() {
651            return;
652        }
653        let Some(window) = self.window else {
654            return;
655        };
656
657        let mut test_id_by_node: HashMap<NodeId, String> = HashMap::new();
658        if let Some(snapshot) = self.semantics_snapshot() {
659            for node in &snapshot.nodes {
660                if let Some(test_id) = node.test_id.as_deref() {
661                    test_id_by_node.insert(node.id, test_id.to_string());
662                }
663            }
664        }
665
666        let resolve_test_id = |tree: &UiTree<H>, id: NodeId| -> Option<&str> {
667            let mut cur = Some(id);
668            while let Some(node) = cur {
669                if let Some(test_id) = test_id_by_node.get(&node) {
670                    return Some(test_id.as_str());
671                }
672                cur = tree.nodes.get(node).and_then(|n| n.parent);
673            }
674            None
675        };
676
677        for (rank, entry) in profile.entries.iter().enumerate() {
678            let kind = crate::declarative::frame::element_record_for_node(app, window, entry.node)
679                .map(|r| r.instance.kind_name());
680
681            let element_path: Option<String> = self
682                .nodes
683                .get(entry.node)
684                .and_then(|n| n.element)
685                .and_then(|element| {
686                    #[cfg(feature = "diagnostics")]
687                    {
688                        crate::elements::with_window_state(app, window, |st| {
689                            st.debug_path_for_element(element)
690                        })
691                    }
692                    #[cfg(not(feature = "diagnostics"))]
693                    {
694                        let _ = element;
695                        None
696                    }
697                });
698
699            tracing::info!(
700                window = ?self.window,
701                frame_id = profile.frame_id.0,
702                nodes_profiled = profile.nodes_profiled,
703                total_self_ms = profile.total_self_time.as_millis() as u64,
704                rank,
705                node = ?entry.node,
706                pass = ?entry.pass_kind,
707                self_us = entry.elapsed_self.as_micros() as u64,
708                total_us = entry.elapsed_total.as_micros() as u64,
709                kind = kind.unwrap_or("<unknown>"),
710                test_id = resolve_test_id(self, entry.node),
711                element_path = element_path.as_deref().unwrap_or("<unknown>"),
712                bounds_w = entry.bounds.size.width.0,
713                bounds_h = entry.bounds.size.height.0,
714                "layout_node profile"
715            );
716        }
717    }
718
719    fn emit_measure_node_profile(&mut self, app: &mut H) {
720        let Some(profile) = self.measure_node_profile.take() else {
721            return;
722        };
723        if profile.entries.is_empty() {
724            return;
725        }
726        let Some(window) = self.window else {
727            return;
728        };
729
730        let mut test_id_by_node: HashMap<NodeId, String> = HashMap::new();
731        if let Some(snapshot) = self.semantics_snapshot() {
732            for node in &snapshot.nodes {
733                if let Some(test_id) = node.test_id.as_deref() {
734                    test_id_by_node.insert(node.id, test_id.to_string());
735                }
736            }
737        }
738
739        let resolve_test_id = |tree: &UiTree<H>, id: NodeId| -> Option<&str> {
740            let mut cur = Some(id);
741            while let Some(node) = cur {
742                if let Some(test_id) = test_id_by_node.get(&node) {
743                    return Some(test_id.as_str());
744                }
745                cur = tree.nodes.get(node).and_then(|n| n.parent);
746            }
747            None
748        };
749
750        for (rank, entry) in profile.entries.iter().enumerate() {
751            let kind = crate::declarative::frame::element_record_for_node(app, window, entry.node)
752                .map(|r| r.instance.kind_name());
753
754            let element_path: Option<String> = self
755                .nodes
756                .get(entry.node)
757                .and_then(|n| n.element)
758                .and_then(|element| {
759                    #[cfg(feature = "diagnostics")]
760                    {
761                        crate::elements::with_window_state(app, window, |st| {
762                            st.debug_path_for_element(element)
763                        })
764                    }
765                    #[cfg(not(feature = "diagnostics"))]
766                    {
767                        let _ = element;
768                        None
769                    }
770                });
771
772            tracing::info!(
773                window = ?self.window,
774                frame_id = profile.frame_id.0,
775                nodes_profiled = profile.nodes_profiled,
776                total_self_ms = profile.total_self_time.as_millis() as u64,
777                rank,
778                node = ?entry.node,
779                self_us = entry.elapsed_self.as_micros() as u64,
780                total_us = entry.elapsed_total.as_micros() as u64,
781                kind = kind.unwrap_or("<unknown>"),
782                test_id = resolve_test_id(self, entry.node),
783                element_path = element_path.as_deref().unwrap_or("<unknown>"),
784                known_w = entry.constraints.known.width.map(|p| p.0),
785                known_h = entry.constraints.known.height.map(|p| p.0),
786                avail_w = ?entry.constraints.available.width,
787                avail_h = ?entry.constraints.available.height,
788                "measure_node profile"
789            );
790        }
791    }
792
793    fn repair_focus_node_from_focused_element_if_needed(&mut self, app: &mut H) {
794        let Some(window) = self.window else {
795            return;
796        };
797        let Some(focused) = self.focus() else {
798            return;
799        };
800        let Some(element) = self.node_element(focused) else {
801            #[cfg(debug_assertions)]
802            if crate::runtime_config::ui_runtime_config().debug_focus_repair {
803                eprintln!("focus_repair: focused={focused:?} has no element");
804            }
805            return;
806        };
807        let Some(canonical) =
808            self.resolve_live_attached_node_for_element(app, Some(window), element)
809        else {
810            #[cfg(debug_assertions)]
811            if crate::runtime_config::ui_runtime_config().debug_focus_repair {
812                eprintln!(
813                    "focus_repair: focused={focused:?} element={element:?} has no canonical node",
814                );
815            }
816            return;
817        };
818        #[cfg(debug_assertions)]
819        if crate::runtime_config::ui_runtime_config().debug_focus_repair {
820            eprintln!(
821                "focus_repair: focused={focused:?} element={element:?} canonical={canonical:?} canonical_exists={}",
822                self.node_exists(canonical)
823            );
824        }
825        if canonical != focused && self.node_exists(canonical) {
826            self.set_focus(Some(canonical));
827            self.request_post_layout_window_runtime_snapshot_refine();
828        }
829
830        let Some(focused) = self.focus() else {
831            return;
832        };
833        let Some(node) = self.nodes.get(focused) else {
834            return;
835        };
836        if node.bounds.size.width.0 <= 0.0 || node.bounds.size.height.0 <= 0.0 {
837            #[cfg(debug_assertions)]
838            if crate::runtime_config::ui_runtime_config().debug_focus_repair {
839                eprintln!(
840                    "focus_repair: clearing focus={focused:?} due to empty bounds={:?}",
841                    node.bounds
842                );
843            }
844            self.set_focus(None);
845            self.request_post_layout_window_runtime_snapshot_refine();
846        }
847    }
848
849    fn repair_view_cache_root_bounds_from_engine_if_needed(&mut self, _app: &mut H) {
850        if !self.view_cache_active() {
851            return;
852        }
853
854        let mut targets: Vec<(NodeId, Rect, Point)> = Vec::with_capacity(16);
855        for (id, node) in self.nodes.iter() {
856            if !node.view_cache.enabled {
857                continue;
858            }
859            if node.bounds.size != Size::default() {
860                continue;
861            }
862            let Some(parent) = node.parent else {
863                continue;
864            };
865            let Some(parent_bounds) = self.nodes.get(parent).map(|n| n.bounds) else {
866                continue;
867            };
868            let Some(local) = self.layout_engine_child_local_rect(parent, id) else {
869                continue;
870            };
871
872            let origin = Point::new(
873                Px(parent_bounds.origin.x.0 + local.origin.x.0),
874                Px(parent_bounds.origin.y.0 + local.origin.y.0),
875            );
876            let new_bounds = Rect::new(origin, local.size);
877            targets.push((id, new_bounds, node.bounds.origin));
878        }
879
880        for (root, new_bounds, old_origin) in targets {
881            let delta = Point::new(
882                Px(new_bounds.origin.x.0 - old_origin.x.0),
883                Px(new_bounds.origin.y.0 - old_origin.y.0),
884            );
885
886            if let Some(node) = self.nodes.get_mut(root) {
887                node.bounds = new_bounds;
888            }
889
890            if delta.x.0 == 0.0 && delta.y.0 == 0.0 {
891                continue;
892            }
893
894            let mut stack: Vec<NodeId> = self
895                .nodes
896                .get(root)
897                .map(|n| n.children.clone())
898                .unwrap_or_default();
899            while let Some(id) = stack.pop() {
900                let Some(n) = self.nodes.get_mut(id) else {
901                    continue;
902                };
903                n.bounds.origin = Point::new(
904                    Px(n.bounds.origin.x.0 + delta.x.0),
905                    Px(n.bounds.origin.y.0 + delta.y.0),
906                );
907                stack.extend(n.children.iter().copied());
908            }
909        }
910    }
911
912    fn layout_pending_barrier_relayouts_if_needed(
913        &mut self,
914        app: &mut H,
915        services: &mut dyn UiServices,
916        scale_factor: f32,
917        pass_kind: LayoutPassKind,
918        viewport_cursor: &mut usize,
919    ) {
920        if pass_kind != LayoutPassKind::Final {
921            return;
922        }
923
924        // Barrier relayouts can update descendant layout without invalidating ancestors. That
925        // means scroll containers that rely on cached content extents can "pin" their scroll
926        // ranges to the previous frame if a contained barrier expands near the bottom of a scroll
927        // view.
928        //
929        // To keep scroll extents consistent, allow barrier relayouts to schedule a follow-up
930        // relayout for the nearest scrollable ancestor.
931        const MAX_PASSES: usize = 4;
932        let mut passes: usize = 0;
933        let mut scheduled_followups: HashSet<NodeId> = HashSet::new();
934
935        while passes < MAX_PASSES {
936            passes = passes.saturating_add(1);
937
938            let pending = self.take_pending_barrier_relayouts();
939            if pending.is_empty() {
940                break;
941            }
942
943            let mut unique = HashSet::<NodeId>::with_capacity(pending.len());
944            let mut targets: Vec<NodeId> = Vec::with_capacity(pending.len());
945            for node in pending {
946                if unique.insert(node) {
947                    targets.push(node);
948                }
949            }
950
951            let mut roots_with_bounds: Vec<(NodeId, Rect)> = Vec::with_capacity(targets.len());
952            for root in targets {
953                let Some(node) = self.nodes.get(root) else {
954                    continue;
955                };
956                if !node.invalidation.layout {
957                    continue;
958                }
959
960                // Barrier relayouts intentionally do not invalidate ancestors. Prefer the retained
961                // bounds (stable barrier viewport), but fall back to resolving bounds from the parent
962                // layout-engine rect when needed (e.g. newly mounted nodes with default bounds).
963                let mut bounds = node.bounds;
964                if (bounds.size == Size::default() || bounds.origin == Point::default())
965                    && let Some(parent) = node.parent
966                    && let Some(parent_bounds) = self.nodes.get(parent).map(|n| n.bounds)
967                    && let Some(local) = self.layout_engine_child_local_rect(parent, root)
968                {
969                    let resolved = Rect::new(
970                        Point::new(
971                            Px(parent_bounds.origin.x.0 + local.origin.x.0),
972                            Px(parent_bounds.origin.y.0 + local.origin.y.0),
973                        ),
974                        local.size,
975                    );
976                    if resolved.size != Size::default() {
977                        bounds = resolved;
978                    }
979                }
980
981                if bounds.size == Size::default() {
982                    continue;
983                }
984
985                roots_with_bounds.push((root, bounds));
986            }
987
988            // Pending barrier relayouts run as contained solves. Pre-solve each root via the
989            // layout engine to avoid widget-local fallback solves (which amplify tail latency by
990            // triggering extra out-of-band engine passes).
991            self.solve_barrier_flow_roots_if_needed(
992                app,
993                services,
994                &roots_with_bounds,
995                scale_factor,
996            );
997
998            for (root, bounds) in roots_with_bounds {
999                let _ = self.layout_in_with_pass_kind(
1000                    app,
1001                    services,
1002                    root,
1003                    bounds,
1004                    scale_factor,
1005                    pass_kind,
1006                    crate::layout::overflow::LayoutOverflowContext::default(),
1007                );
1008                if self.debug_enabled {
1009                    self.debug_stats.barrier_relayouts_performed = self
1010                        .debug_stats
1011                        .barrier_relayouts_performed
1012                        .saturating_add(1);
1013                }
1014
1015                // After contained relayout, schedule a follow-up barrier relayout for the nearest
1016                // scrollable ancestor so it can recompute scroll extents against the new subtree
1017                // bounds without forcing a full ancestor relayout.
1018                let mut current = self.nodes.get(root).and_then(|n| n.parent);
1019                while let Some(id) = current {
1020                    let can_scroll = self
1021                        .nodes
1022                        .get(id)
1023                        .and_then(|n| n.widget.as_ref())
1024                        .is_some_and(|w| w.can_scroll_descendant_into_view());
1025                    if can_scroll {
1026                        if scheduled_followups.insert(id) {
1027                            self.schedule_barrier_relayout_with_source_and_detail(
1028                                id,
1029                                UiDebugInvalidationSource::Other,
1030                                UiDebugInvalidationDetail::Unknown,
1031                            );
1032                        }
1033                        break;
1034                    }
1035                    current = self.nodes.get(id).and_then(|n| n.parent);
1036                }
1037
1038                self.flush_viewport_roots_after_root(
1039                    app,
1040                    services,
1041                    scale_factor,
1042                    pass_kind,
1043                    viewport_cursor,
1044                );
1045            }
1046        }
1047    }
1048
1049    pub fn layout(
1050        &mut self,
1051        app: &mut H,
1052        services: &mut dyn UiServices,
1053        root: NodeId,
1054        available: Size,
1055        scale_factor: f32,
1056    ) -> Size {
1057        let bounds = Rect::new(
1058            Point::new(fret_core::Px(0.0), fret_core::Px(0.0)),
1059            available,
1060        );
1061        self.update_interactive_resize_state_for_layout(app.frame_id(), bounds, scale_factor);
1062        let force_post_resize_rebuild = self.interactive_resize_requires_full_rebuild();
1063        if force_post_resize_rebuild {
1064            self.mark_subtree_invalidation_local(root, Invalidation::Layout);
1065        }
1066
1067        if self.invalidated_layout_nodes == 0
1068            && self.invalidated_hit_test_nodes == 0
1069            && let Some(n) = self.nodes.get(root)
1070            && !n.invalidation.layout
1071            && !n.invalidation.hit_test
1072            && n.bounds == bounds
1073            && n.measured_size != Size::default()
1074            && !force_post_resize_rebuild
1075        {
1076            return n.measured_size;
1077        }
1078
1079        let mut viewport_cursor: usize = 0;
1080        self.begin_layout_engine_frame(app);
1081        self.request_build_window_roots_if_final(
1082            app,
1083            services,
1084            std::slice::from_ref(&root),
1085            bounds,
1086            scale_factor,
1087            LayoutPassKind::Final,
1088        );
1089        let size = self.layout_in_with_pass_kind(
1090            app,
1091            services,
1092            root,
1093            bounds,
1094            scale_factor,
1095            LayoutPassKind::Final,
1096            crate::layout::overflow::LayoutOverflowContext::default(),
1097        );
1098        self.flush_viewport_roots_after_root(
1099            app,
1100            services,
1101            scale_factor,
1102            LayoutPassKind::Final,
1103            &mut viewport_cursor,
1104        );
1105
1106        self.finish_final_layout_frame(app);
1107        size
1108    }
1109
1110    pub fn layout_in(
1111        &mut self,
1112        app: &mut H,
1113        services: &mut dyn UiServices,
1114        root: NodeId,
1115        bounds: Rect,
1116        scale_factor: f32,
1117    ) -> Size {
1118        self.update_interactive_resize_state_for_layout(app.frame_id(), bounds, scale_factor);
1119        let force_post_resize_rebuild = self.interactive_resize_requires_full_rebuild();
1120        if force_post_resize_rebuild {
1121            self.mark_subtree_invalidation_local(root, Invalidation::Layout);
1122        }
1123        if self.invalidated_layout_nodes == 0
1124            && self.invalidated_hit_test_nodes == 0
1125            && let Some(n) = self.nodes.get(root)
1126            && !n.invalidation.layout
1127            && !n.invalidation.hit_test
1128            && n.bounds == bounds
1129            && n.measured_size != Size::default()
1130            && !force_post_resize_rebuild
1131        {
1132            return n.measured_size;
1133        }
1134
1135        let mut viewport_cursor: usize = 0;
1136        self.begin_layout_engine_frame(app);
1137        self.request_build_window_roots_if_final(
1138            app,
1139            services,
1140            std::slice::from_ref(&root),
1141            bounds,
1142            scale_factor,
1143            LayoutPassKind::Final,
1144        );
1145        let size = self.layout_in_with_pass_kind(
1146            app,
1147            services,
1148            root,
1149            bounds,
1150            scale_factor,
1151            LayoutPassKind::Final,
1152            crate::layout::overflow::LayoutOverflowContext::default(),
1153        );
1154        self.flush_viewport_roots_after_root(
1155            app,
1156            services,
1157            scale_factor,
1158            LayoutPassKind::Final,
1159            &mut viewport_cursor,
1160        );
1161        self.finish_final_layout_frame(app);
1162        size
1163    }
1164
1165    #[stacksafe::stacksafe]
1166    pub fn layout_in_with_pass_kind(
1167        &mut self,
1168        app: &mut H,
1169        services: &mut dyn UiServices,
1170        root: NodeId,
1171        bounds: Rect,
1172        scale_factor: f32,
1173        pass_kind: LayoutPassKind,
1174        overflow_ctx: crate::layout::overflow::LayoutOverflowContext,
1175    ) -> Size {
1176        self.layout_node(
1177            app,
1178            services,
1179            root,
1180            bounds,
1181            scale_factor,
1182            pass_kind,
1183            overflow_ctx,
1184        )
1185    }
1186
1187    fn sync_element_bounds_cache_after_layout(&mut self, app: &mut H) {
1188        let Some(window) = self.window else {
1189            return;
1190        };
1191
1192        let nodes = &self.nodes;
1193        let scratch_element_nodes = &mut self.scratch_element_nodes;
1194
1195        crate::elements::with_window_state(app, window, |st| {
1196            st.element_nodes_copy_into(scratch_element_nodes);
1197            for &(element, node) in scratch_element_nodes.iter() {
1198                let Some(rect) = nodes.get(node).map(|n| n.bounds) else {
1199                    continue;
1200                };
1201                st.record_bounds(element, rect);
1202            }
1203        });
1204    }
1205
1206    fn finish_final_layout_frame(&mut self, app: &mut H) {
1207        self.layout_engine.end_frame();
1208        if let Some(window) = self.window {
1209            let frame_id = app.frame_id();
1210            crate::elements::with_window_state(app, window, |st| {
1211                st.clear_stale_interaction_targets_for_frame(frame_id);
1212                st.sync_active_text_selection_node(|element, seeded| {
1213                    self.resolve_live_attached_node_for_element_seeded(element, seeded)
1214                });
1215                st.sync_interaction_target_nodes(|element, seeded| {
1216                    self.resolve_live_attached_node_for_element_seeded(element, seeded)
1217                });
1218            });
1219        }
1220
1221        // Keep cross-frame `bounds_for_element(...)` queries in sync with the latest layout.
1222        // These bounds are used by component-layer policies (e.g. overlay placement) and are
1223        // expected to reflect the most recent layout pass.
1224        self.sync_element_bounds_cache_after_layout(app);
1225        self.validate_subtree_layout_dirty_counts_if_enabled();
1226        if !self.interactive_resize_active() {
1227            self.interactive_resize_needs_full_rebuild = false;
1228        }
1229        self.last_layout_frame_id = Some(app.frame_id());
1230        self.refine_pending_window_runtime_snapshots_after_layout(app);
1231    }
1232
1233    pub fn measure_in(
1234        &mut self,
1235        app: &mut H,
1236        services: &mut dyn UiServices,
1237        node: NodeId,
1238        constraints: LayoutConstraints,
1239        scale_factor: f32,
1240    ) -> Size {
1241        self.measure_node(app, services, node, constraints, scale_factor)
1242    }
1243
1244    pub(crate) fn node_is_attached_to_layer_tree(&self, node: NodeId) -> bool {
1245        self.node_root(node)
1246            .is_some_and(|root| self.root_to_layer.contains_key(&root))
1247    }
1248
1249    fn any_attached_layout_invalidations(&self) -> bool {
1250        self.nodes
1251            .iter()
1252            .any(|(id, node)| node.invalidation.layout && self.node_is_attached_to_layer_tree(id))
1253    }
1254
1255    fn prune_detached_layout_followups(&mut self) {
1256        let retained_dirty_cache_roots: std::collections::HashSet<NodeId> = self
1257            .dirty_cache_roots
1258            .iter()
1259            .copied()
1260            .filter(|&root| self.node_is_attached_to_layer_tree(root))
1261            .collect();
1262        self.dirty_cache_roots = retained_dirty_cache_roots;
1263        self.dirty_cache_root_reasons
1264            .retain(|root, _| self.dirty_cache_roots.contains(root));
1265        self.pending_barrier_relayouts = self
1266            .pending_barrier_relayouts
1267            .iter()
1268            .copied()
1269            .filter(|&root| self.node_is_attached_to_layer_tree(root))
1270            .collect();
1271    }
1272
1273    fn begin_layout_engine_frame(&mut self, app: &mut H) {
1274        self.layout_engine.begin_frame(app.frame_id());
1275        self.viewport_roots.clear();
1276    }
1277
1278    fn mark_layout_engine_seen_subtree_from_ui_children(
1279        &mut self,
1280        engine: &mut crate::layout_engine::TaffyLayoutEngine,
1281        root: NodeId,
1282    ) {
1283        if engine.layout_id_for_node(root).is_none() {
1284            return;
1285        }
1286
1287        self.scratch_node_stack.clear();
1288        self.scratch_node_stack.push(root);
1289        while let Some(node) = self.scratch_node_stack.pop() {
1290            engine.mark_seen_if_present(node);
1291            if let Some(entry) = self.nodes.get(node) {
1292                for &child in &entry.children {
1293                    self.scratch_node_stack.push(child);
1294                }
1295            }
1296        }
1297    }
1298
1299    fn layout_contained_view_cache_roots_if_needed(
1300        &mut self,
1301        app: &mut H,
1302        services: &mut dyn UiServices,
1303        scale_factor: f32,
1304        pass_kind: LayoutPassKind,
1305        viewport_cursor: &mut usize,
1306    ) {
1307        if !self.view_cache_active() {
1308            return;
1309        }
1310
1311        // If both an ancestor and a descendant cache root are invalidated in the same frame, only
1312        // relayout the ancestor; it will already relayout the subtree.
1313        //
1314        // Hot path: avoid scanning the whole node store. Cache-root invalidations are tracked in
1315        // `dirty_cache_roots`, so we can restrict this pass to the subset that actually changed.
1316        let mut candidates: Vec<NodeId> = Vec::with_capacity(16);
1317        for &id in &self.dirty_cache_roots {
1318            let Some(node) = self.nodes.get(id) else {
1319                continue;
1320            };
1321            if !node.view_cache.enabled || !node.view_cache.contained_layout {
1322                continue;
1323            }
1324            if !node.invalidation.layout {
1325                continue;
1326            }
1327            candidates.push(id);
1328        }
1329
1330        if candidates.is_empty() {
1331            return;
1332        }
1333
1334        let candidate_set: std::collections::HashSet<NodeId> = candidates.iter().copied().collect();
1335        let mut scheduled_followups: std::collections::HashSet<NodeId> =
1336            std::collections::HashSet::new();
1337
1338        let mut targets: Vec<(NodeId, Rect)> = Vec::with_capacity(candidates.len());
1339        for id in candidates {
1340            let mut skip = false;
1341            let mut parent = self.nodes.get(id).and_then(|n| n.parent);
1342            while let Some(p) = parent {
1343                if candidate_set.contains(&p) {
1344                    skip = true;
1345                    break;
1346                }
1347                parent = self.nodes.get(p).and_then(|n| n.parent);
1348            }
1349            if skip {
1350                continue;
1351            }
1352
1353            let Some(node) = self.nodes.get(id) else {
1354                continue;
1355            };
1356
1357            // Contained relayouts run after the main layout pass. If a cache root was newly
1358            // mounted (or skipped by an engine-backed parent) its retained bounds can still be
1359            // the default `Rect::default()`, which would incorrectly relayout the subtree at the
1360            // origin and desynchronize semantics/hit-testing from the painted output.
1361            //
1362            // Prefer the parent's solved layout-engine rect when available so the contained pass
1363            // runs in the same coordinate space as the parent placement.
1364            let mut bounds = node.bounds;
1365            if (bounds.size == Size::default() || bounds.origin == Point::default())
1366                && let Some(parent) = node.parent
1367                && let Some(parent_bounds) = self.nodes.get(parent).map(|n| n.bounds)
1368                && let Some(local) = self.layout_engine_child_local_rect(parent, id)
1369            {
1370                let resolved = Rect::new(
1371                    Point::new(
1372                        Px(parent_bounds.origin.x.0 + local.origin.x.0),
1373                        Px(parent_bounds.origin.y.0 + local.origin.y.0),
1374                    ),
1375                    local.size,
1376                );
1377                if resolved.size != Size::default() {
1378                    bounds = resolved;
1379                }
1380            }
1381
1382            targets.push((id, bounds));
1383        }
1384
1385        // Contained cache-root relayouts run as independent solves after the main viewport roots.
1386        // Pre-solve via the layout engine so cache-root subtrees don't trigger widget-local
1387        // fallback solves (which create extra solves and jitter within the same frame).
1388        self.solve_barrier_flow_roots_if_needed(app, services, &targets, scale_factor);
1389
1390        for (root, bounds) in targets {
1391            if self.debug_enabled {
1392                self.debug_stats.view_cache_contained_relayouts = self
1393                    .debug_stats
1394                    .view_cache_contained_relayouts
1395                    .saturating_add(1);
1396                self.debug_view_cache_contained_relayout_roots.push(root);
1397            }
1398            let _ = self.layout_in_with_pass_kind(
1399                app,
1400                services,
1401                root,
1402                bounds,
1403                scale_factor,
1404                pass_kind,
1405                crate::layout::overflow::LayoutOverflowContext::default(),
1406            );
1407            self.flush_viewport_roots_after_root(
1408                app,
1409                services,
1410                scale_factor,
1411                pass_kind,
1412                viewport_cursor,
1413            );
1414            let layout_transition = self.nodes.get_mut(root).map(|node| {
1415                let prev = node.invalidation;
1416                let layout_before = node.invalidation.layout;
1417                node.invalidation.layout = false;
1418                let next = node.invalidation;
1419                let layout_after = node.invalidation.layout;
1420                (prev, next, layout_before, layout_after)
1421            });
1422            if let Some((prev, next, layout_before, layout_after)) = layout_transition
1423                && layout_before != layout_after
1424            {
1425                record_layout_invalidation_transition(
1426                    &mut self.layout_invalidations_count,
1427                    layout_before,
1428                    layout_after,
1429                );
1430                self.note_layout_invalidation_transition_for_subtree_aggregation(
1431                    root,
1432                    layout_before,
1433                    layout_after,
1434                );
1435                self.update_invalidation_counters(prev, next);
1436            }
1437            // Contained relayout is a layout-only repair path. It may consume a layout-invalidated
1438            // cache root without implying that the declarative subtree must rerun next frame.
1439            // Keep an explicit `needs_rerender` bit authoritative, and clear the scheduling-only
1440            // dirty marker once both layout invalidation and rerender pressure are gone.
1441            self.clear_cache_root_dirty_tracking_if_clean(root);
1442
1443            // Contained view-cache relayouts run after the main root layout pass, so any scroll
1444            // ancestor that inferred its content extent earlier in the frame can be left with a
1445            // stale range. Re-run the nearest scrollable ancestor in the same frame so scroll
1446            // extents track the reconciled cache-root bounds immediately.
1447            let mut current = self.nodes.get(root).and_then(|n| n.parent);
1448            while let Some(id) = current {
1449                let can_scroll = self
1450                    .nodes
1451                    .get(id)
1452                    .and_then(|n| n.widget.as_ref())
1453                    .is_some_and(|w| w.can_scroll_descendant_into_view());
1454                if can_scroll {
1455                    if scheduled_followups.insert(id) {
1456                        self.schedule_barrier_relayout_with_source_and_detail(
1457                            id,
1458                            UiDebugInvalidationSource::Other,
1459                            UiDebugInvalidationDetail::Unknown,
1460                        );
1461                    }
1462                    break;
1463                }
1464                current = self.nodes.get(id).and_then(|n| n.parent);
1465            }
1466        }
1467
1468        if !scheduled_followups.is_empty() {
1469            self.layout_pending_barrier_relayouts_if_needed(
1470                app,
1471                services,
1472                scale_factor,
1473                pass_kind,
1474                viewport_cursor,
1475            );
1476        }
1477    }
1478
1479    fn request_build_window_roots_if_final(
1480        &mut self,
1481        app: &mut H,
1482        services: &mut dyn UiServices,
1483        roots: &[NodeId],
1484        bounds: Rect,
1485        scale_factor: f32,
1486        pass_kind: LayoutPassKind,
1487    ) {
1488        if pass_kind != LayoutPassKind::Final {
1489            return;
1490        }
1491
1492        let Some(window) = self.window else {
1493            return;
1494        };
1495
1496        let runtime_cfg = crate::runtime_config::ui_runtime_config();
1497        let profile_layout = runtime_cfg.layout_profile;
1498        let total_started = profile_layout.then(Instant::now);
1499
1500        let sf = scale_factor;
1501        let available = LayoutSize::new(
1502            AvailableSpace::Definite(bounds.size.width),
1503            AvailableSpace::Definite(bounds.size.height),
1504        );
1505
1506        let mut engine = self.take_layout_engine();
1507        engine.set_measure_profiling_enabled(self.debug_enabled && profile_layout);
1508
1509        let phase1_started = profile_layout.then(Instant::now);
1510        let reuse_cached_flow = self.interactive_resize_active();
1511        let allow_translation_only_skip = runtime_cfg.layout_skip_request_build_translation_only;
1512        let force_post_resize_rebuild = self.interactive_resize_requires_full_rebuild();
1513        if force_post_resize_rebuild {
1514            for &root in roots {
1515                self.mark_subtree_invalidation_local(root, Invalidation::Layout);
1516            }
1517        }
1518        // Phase 1: request/build for stable identity, even if we later skip compute/apply.
1519        for &root in roots {
1520            let Some((
1521                has_element,
1522                layout_invalidated,
1523                subtree_layout_dirty,
1524                prev_bounds,
1525                measured,
1526            )) = self.nodes.get(root).map(|node| {
1527                (
1528                    node.element.is_some(),
1529                    node.invalidation.layout,
1530                    self.node_subtree_layout_dirty(root),
1531                    node.bounds,
1532                    node.measured_size,
1533                )
1534            })
1535            else {
1536                continue;
1537            };
1538            if !has_element {
1539                continue;
1540            }
1541
1542            let needs_layout = layout_invalidated || prev_bounds != bounds;
1543            let is_translation_only = allow_translation_only_skip
1544                && !layout_invalidated
1545                && prev_bounds.size == bounds.size
1546                && prev_bounds.origin != bounds.origin
1547                && measured != Size::default();
1548
1549            if engine.layout_id_for_node(root).is_some() && (!needs_layout || is_translation_only) {
1550                self.mark_layout_engine_seen_subtree_from_ui_children(&mut engine, root);
1551                continue;
1552            }
1553            if reuse_cached_flow
1554                && engine.layout_id_for_node(root).is_some()
1555                && !layout_invalidated
1556                && !subtree_layout_dirty
1557            {
1558                engine.set_viewport_root_override_size(root, bounds.size, sf);
1559                self.note_interactive_resize_cached_flow_reuse();
1560                self.mark_layout_engine_seen_subtree_from_ui_children(&mut engine, root);
1561            } else {
1562                build_viewport_flow_subtree(
1563                    &mut engine,
1564                    app,
1565                    &*self,
1566                    window,
1567                    sf,
1568                    root,
1569                    bounds.size,
1570                );
1571            }
1572        }
1573        let phase1_elapsed = phase1_started.map(|s| s.elapsed());
1574
1575        let phase2_started = profile_layout.then(Instant::now);
1576        // Phase 2: compute/apply only when layout is needed.
1577        //
1578        // When multiple independent viewport roots need layout in the same frame (window root +
1579        // overlays + other detached flow roots), solving them one-by-one can amplify fixed per-solve
1580        // overhead into tail spikes. Prefer batching via the layout engine's synthetic-root path.
1581        let mut pending_solves: Vec<(NodeId, LayoutSize<AvailableSpace>)> = Vec::new();
1582        for &root in roots {
1583            let (has_element, needs_layout, is_translation_only) = match self.nodes.get(root) {
1584                Some(node) => {
1585                    let has_element = node.element.is_some();
1586                    let needs_layout = node.invalidation.layout || node.bounds != bounds;
1587                    let is_translation_only = !node.invalidation.layout
1588                        && node.bounds.size == bounds.size
1589                        && node.bounds.origin != bounds.origin
1590                        && node.measured_size != Size::default();
1591                    (has_element, needs_layout, is_translation_only)
1592                }
1593                None => continue,
1594            };
1595
1596            if !has_element || !needs_layout || is_translation_only {
1597                continue;
1598            }
1599
1600            pending_solves.push((root, available));
1601        }
1602
1603        if !pending_solves.is_empty() {
1604            let solves_before = engine.solve_count();
1605            let solve_time_before = engine.last_solve_time();
1606            engine.compute_independent_roots_with_measure_if_needed(&pending_solves, sf, |n, c| {
1607                self.measure_in(app, services, n, c, sf)
1608            });
1609
1610            if self.debug_enabled && engine.solve_count() > solves_before {
1611                let elapsed = engine.last_solve_time().saturating_sub(solve_time_before);
1612                let top_measures = engine
1613                    .last_solve_measure_hotspots()
1614                    .iter()
1615                    .map(|h| {
1616                        let mut element: Option<GlobalElementId> = None;
1617                        let mut element_kind: Option<&'static str> = None;
1618                        if let Some(record) =
1619                            crate::declarative::frame::element_record_for_node(app, window, h.node)
1620                        {
1621                            element = Some(record.element);
1622                            element_kind = Some(record.instance.kind_name());
1623                        }
1624                        let top_children = self
1625                            .debug_take_top_measure_children(h.node, 3)
1626                            .into_iter()
1627                            .map(|(child, r)| {
1628                                let mut child_element: Option<GlobalElementId> = None;
1629                                let mut child_kind: Option<&'static str> = None;
1630                                if let Some(record) =
1631                                    crate::declarative::frame::element_record_for_node(
1632                                        app, window, child,
1633                                    )
1634                                {
1635                                    child_element = Some(record.element);
1636                                    child_kind = Some(record.instance.kind_name());
1637                                }
1638                                super::UiDebugLayoutEngineMeasureChildHotspot {
1639                                    child,
1640                                    measure_time: r.total_time,
1641                                    calls: r.calls,
1642                                    element: child_element,
1643                                    element_kind: child_kind,
1644                                }
1645                            })
1646                            .collect();
1647                        super::UiDebugLayoutEngineMeasureHotspot {
1648                            node: h.node,
1649                            measure_time: h.total_time,
1650                            calls: h.calls,
1651                            cache_hits: h.cache_hits,
1652                            element,
1653                            element_kind,
1654                            top_children,
1655                        }
1656                    })
1657                    .collect();
1658                let solve_root = engine
1659                    .last_solve_root()
1660                    .unwrap_or_else(|| pending_solves[0].0);
1661                let (root_element, root_element_kind, root_element_path) =
1662                    self.debug_resolve_layout_solve_root_label(app, window, solve_root);
1663
1664                self.debug_record_layout_engine_solve(
1665                    solve_root,
1666                    root_element,
1667                    root_element_kind,
1668                    root_element_path,
1669                    elapsed,
1670                    engine.last_solve_measure_calls(),
1671                    engine.last_solve_measure_cache_hits(),
1672                    engine.last_solve_measure_time(),
1673                    top_measures,
1674                );
1675                self.debug_measure_children.clear();
1676            }
1677
1678            for &(root, _available) in &pending_solves {
1679                self.maybe_dump_taffy_subtree(app, window, &engine, root, bounds, sf);
1680            }
1681        }
1682        let phase2_elapsed = phase2_started.map(|s| s.elapsed());
1683
1684        self.put_layout_engine(engine);
1685
1686        if let Some(started) = total_started {
1687            let total = started.elapsed();
1688            tracing::info!(
1689                window = ?window,
1690                roots = roots.len(),
1691                total_ms = total.as_millis(),
1692                phase1_ms = phase1_elapsed.map(|d| d.as_millis()),
1693                phase2_ms = phase2_elapsed.map(|d| d.as_millis()),
1694                "layout root request/build profile"
1695            );
1696        }
1697    }
1698
1699    fn flush_viewport_roots_after_root(
1700        &mut self,
1701        app: &mut H,
1702        services: &mut dyn UiServices,
1703        scale_factor: f32,
1704        pass_kind: LayoutPassKind,
1705        viewport_cursor: &mut usize,
1706    ) {
1707        let sf = scale_factor;
1708        let window = self.window;
1709
1710        while *viewport_cursor < self.viewport_roots.len() {
1711            let batch_start = *viewport_cursor;
1712            let batch_end = self.viewport_roots.len();
1713            let force_post_resize_rebuild = pass_kind == LayoutPassKind::Final
1714                && self.interactive_resize_requires_full_rebuild();
1715
1716            if force_post_resize_rebuild {
1717                let roots_to_invalidate: Vec<NodeId> = self.viewport_roots[batch_start..batch_end]
1718                    .iter()
1719                    .map(|(root, _)| *root)
1720                    .collect();
1721                for root in roots_to_invalidate {
1722                    self.mark_subtree_invalidation_local(root, Invalidation::Layout);
1723                }
1724            }
1725
1726            struct ViewportWorkItem {
1727                root: NodeId,
1728                bounds: Rect,
1729                needs_layout: bool,
1730                is_translation_only: bool,
1731                layout_invalidated: bool,
1732                subtree_layout_dirty: bool,
1733            }
1734
1735            let mut batch: Vec<ViewportWorkItem> = Vec::with_capacity(batch_end - batch_start);
1736            for &(root, bounds) in &self.viewport_roots[batch_start..batch_end] {
1737                let Some((prev_bounds, invalidated, measured)) = self
1738                    .nodes
1739                    .get(root)
1740                    .map(|n| (n.bounds, n.invalidation.layout, n.measured_size))
1741                else {
1742                    continue;
1743                };
1744
1745                let needs_layout = invalidated || prev_bounds != bounds;
1746                let is_translation_only = !invalidated
1747                    && prev_bounds.size == bounds.size
1748                    && prev_bounds.origin != bounds.origin
1749                    && measured != Size::default();
1750
1751                batch.push(ViewportWorkItem {
1752                    root,
1753                    bounds,
1754                    needs_layout,
1755                    is_translation_only,
1756                    layout_invalidated: invalidated,
1757                    subtree_layout_dirty: self.node_subtree_layout_dirty(root),
1758                });
1759            }
1760
1761            if pass_kind == LayoutPassKind::Final
1762                && let Some(window) = window
1763            {
1764                let mut engine = self.take_layout_engine();
1765                engine.set_measure_profiling_enabled(
1766                    self.debug_enabled && crate::runtime_config::ui_runtime_config().layout_profile,
1767                );
1768
1769                let reuse_cached_flow = self.interactive_resize_active();
1770
1771                // Phase 1: request/build newly registered viewport roots for stable identity,
1772                // regardless of whether they will be computed this frame.
1773                for item in &batch {
1774                    if self
1775                        .nodes
1776                        .get(item.root)
1777                        .is_none_or(|node| node.element.is_none())
1778                    {
1779                        continue;
1780                    }
1781                    if engine.layout_id_for_node(item.root).is_some()
1782                        && (!item.needs_layout || item.is_translation_only)
1783                    {
1784                        self.mark_layout_engine_seen_subtree_from_ui_children(
1785                            &mut engine,
1786                            item.root,
1787                        );
1788                        continue;
1789                    }
1790                    if reuse_cached_flow
1791                        && engine.layout_id_for_node(item.root).is_some()
1792                        && !item.layout_invalidated
1793                        && !item.subtree_layout_dirty
1794                    {
1795                        engine.set_viewport_root_override_size(item.root, item.bounds.size, sf);
1796                        self.note_interactive_resize_cached_flow_reuse();
1797                        self.mark_layout_engine_seen_subtree_from_ui_children(
1798                            &mut engine,
1799                            item.root,
1800                        );
1801                    } else {
1802                        build_viewport_flow_subtree(
1803                            &mut engine,
1804                            app,
1805                            &*self,
1806                            window,
1807                            sf,
1808                            item.root,
1809                            item.bounds.size,
1810                        );
1811                    }
1812                }
1813
1814                // Phase 2: compute/apply only for roots that need layout and are not translation-only.
1815                let mut pending_solves: Vec<(NodeId, LayoutSize<AvailableSpace>)> = Vec::new();
1816                for item in &batch {
1817                    if !item.needs_layout || item.is_translation_only {
1818                        continue;
1819                    }
1820                    pending_solves.push((
1821                        item.root,
1822                        LayoutSize::new(
1823                            AvailableSpace::Definite(item.bounds.size.width),
1824                            AvailableSpace::Definite(item.bounds.size.height),
1825                        ),
1826                    ));
1827                }
1828
1829                if !pending_solves.is_empty() {
1830                    let solves_before = engine.solve_count();
1831                    let solve_time_before = engine.last_solve_time();
1832                    engine.compute_independent_roots_with_measure_if_needed(
1833                        &pending_solves,
1834                        sf,
1835                        |n, c| self.measure_in(app, services, n, c, sf),
1836                    );
1837
1838                    if self.debug_enabled && engine.solve_count() > solves_before {
1839                        let elapsed = engine.last_solve_time().saturating_sub(solve_time_before);
1840                        let top_measures = engine
1841                            .last_solve_measure_hotspots()
1842                            .iter()
1843                            .map(|h| {
1844                                let mut element: Option<GlobalElementId> = None;
1845                                let mut element_kind: Option<&'static str> = None;
1846                                if let Some(record) =
1847                                    crate::declarative::frame::element_record_for_node(
1848                                        app, window, h.node,
1849                                    )
1850                                {
1851                                    element = Some(record.element);
1852                                    element_kind = Some(record.instance.kind_name());
1853                                }
1854                                let top_children = self
1855                                    .debug_take_top_measure_children(h.node, 3)
1856                                    .into_iter()
1857                                    .map(|(child, r)| {
1858                                        let mut child_element: Option<GlobalElementId> = None;
1859                                        let mut child_kind: Option<&'static str> = None;
1860                                        if let Some(record) =
1861                                            crate::declarative::frame::element_record_for_node(
1862                                                app, window, child,
1863                                            )
1864                                        {
1865                                            child_element = Some(record.element);
1866                                            child_kind = Some(record.instance.kind_name());
1867                                        }
1868                                        super::UiDebugLayoutEngineMeasureChildHotspot {
1869                                            child,
1870                                            measure_time: r.total_time,
1871                                            calls: r.calls,
1872                                            element: child_element,
1873                                            element_kind: child_kind,
1874                                        }
1875                                    })
1876                                    .collect();
1877                                super::UiDebugLayoutEngineMeasureHotspot {
1878                                    node: h.node,
1879                                    measure_time: h.total_time,
1880                                    calls: h.calls,
1881                                    cache_hits: h.cache_hits,
1882                                    element,
1883                                    element_kind,
1884                                    top_children,
1885                                }
1886                            })
1887                            .collect();
1888                        let solve_root = engine
1889                            .last_solve_root()
1890                            .unwrap_or_else(|| pending_solves[0].0);
1891                        let (root_element, root_element_kind, root_element_path) =
1892                            self.debug_resolve_layout_solve_root_label(app, window, solve_root);
1893
1894                        self.debug_record_layout_engine_solve(
1895                            solve_root,
1896                            root_element,
1897                            root_element_kind,
1898                            root_element_path,
1899                            elapsed,
1900                            engine.last_solve_measure_calls(),
1901                            engine.last_solve_measure_cache_hits(),
1902                            engine.last_solve_measure_time(),
1903                            top_measures,
1904                        );
1905                        self.debug_measure_children.clear();
1906                    }
1907
1908                    for item in &batch {
1909                        if !item.needs_layout || item.is_translation_only {
1910                            continue;
1911                        }
1912                        self.maybe_dump_taffy_subtree(
1913                            app,
1914                            window,
1915                            &engine,
1916                            item.root,
1917                            item.bounds,
1918                            sf,
1919                        );
1920                    }
1921                }
1922
1923                self.put_layout_engine(engine);
1924            }
1925
1926            // Apply the viewport root bounds by running the regular layout pass. Even when a root
1927            // is translation-only (so we skip compute), the translation-only fast path needs to
1928            // update the retained bounds for the subtree.
1929            for item in &batch {
1930                if !item.needs_layout {
1931                    continue;
1932                }
1933
1934                let _ = self.layout_in_with_pass_kind(
1935                    app,
1936                    services,
1937                    item.root,
1938                    item.bounds,
1939                    scale_factor,
1940                    LayoutPassKind::Final,
1941                    crate::layout::overflow::LayoutOverflowContext::default(),
1942                );
1943            }
1944
1945            *viewport_cursor = batch_end;
1946        }
1947    }
1948}