Skip to main content

eui_core/surface/
pass.rs

1//! The LAYOUT pass: batched measurement (≤ 3 upcalls), the main tree and
2//! every layer laid out by the engine, layers placed (viewport, edge,
3//! anchor with flip + shift, point, toast stack) inside the safe area,
4//! scroll containers reported, `%` radii resolved against the boxes.
5
6use std::cell::RefCell;
7use std::collections::HashSet;
8use std::time::Instant;
9
10use taffy::prelude::*;
11use taffy::style::Direction;
12
13use super::{Frame, Layer, LayoutOutput, ListOutput, NodeDelta, PlacedFrame, ScrollOutput, Surface};
14use crate::engine::LeafAnswer;
15use crate::layout_tree::{LayerPlacement, NodeKind};
16use crate::measure::{HeightRequest, Intrinsics, LeafRequest, Measure, MeasureMemo, MeasureRequest};
17use crate::overlay::{place_overlay, OverlayAlign, OverlayPlacement, OverlaySide, PlaceOptions, Rect as ORect, Size as OSize, OVERLAY_PADDING};
18use crate::style::{self, BoxKind};
19
20/// The widest a centred Dialog gets, and a side Drawer.
21pub const DIALOG_MAX_WIDTH: f32 = 512.0;
22pub const DRAWER_MAX_WIDTH: f32 = 360.0;
23/// A top/bottom Drawer takes at most this share of the viewport height.
24pub const SHEET_MAX_HEIGHT: f32 = 0.9;
25/// The widest a toast gets, and the gap between stacked toasts.
26pub const TOAST_MAX_WIDTH: f32 = 420.0;
27pub const TOAST_GAP: f32 = 8.0;
28
29/// One placed layer: its placement, position name and anchor frame.
30type Placed = (Option<OverlayPlacement>, String, Option<Frame>);
31
32/// A layout pass in progress (the step API).
33#[derive(Debug, Clone)]
34pub(super) struct PassState {
35    started: Instant,
36    upcalls: u32,
37    rounds: u32,
38    missing: Vec<usize>,
39    heights: Vec<HeightRequest>,
40}
41
42/// A measured leaf, OWNED: what the step API hands a host across a lock.
43#[derive(Debug, Clone, PartialEq)]
44pub struct LeafData {
45    pub index: u32,
46    pub id: String,
47    pub component: String,
48    pub part: Option<String>,
49    pub owner_component: Option<String>,
50    pub props: serde_json::Map<String, serde_json::Value>,
51    pub text_style: crate::measure::TextStyle,
52    pub control: crate::measure::ControlBox,
53    pub lines: Option<u32>,
54}
55
56impl LeafData {
57    /// The borrowed request a [`Measure`] implementation takes.
58    pub fn request(&self) -> LeafRequest<'_> {
59        LeafRequest { index: self.index, id: &self.id, component: &self.component, part: self.part.as_deref(), owner_component: self.owner_component.as_deref(), props: &self.props, text_style: &self.text_style, control: self.control, lines: self.lines }
60    }
61}
62
63/// One step of a resumable layout pass ([`Surface::layout_begin`]).
64#[derive(Debug, Clone, PartialEq)]
65pub enum LayoutStep {
66    /// Measure these leaves' intrinsics; answer with `layout_intrinsics`.
67    Intrinsics(Vec<LeafData>),
68    /// Measure these heights; answer with `layout_heights`.
69    Heights(Vec<LeafData>, Vec<HeightRequest>),
70    /// The pass is complete.
71    Done(Box<LayoutOutput>),
72    /// No pass in progress (an answer without `layout_begin`).
73    Idle,
74}
75
76/// The area layers stay inside: the viewport minus the safe-area insets.
77#[derive(Debug, Clone, Copy)]
78struct View {
79    x: f32,
80    y: f32,
81    w: f32,
82    h: f32,
83}
84
85impl Surface {
86    fn leaf_request(&self, i: usize) -> LeafRequest<'_> {
87        let n = &self.nodes[i];
88        let s = &self.node_state[i];
89        LeafRequest { index: i as u32, id: &n.id, component: &n.component, part: n.part.as_deref(), owner_component: n.owner_component.as_deref(), props: &n.props, text_style: &s.text, control: s.control, lines: n.lines }
90    }
91
92    fn is_measured_leaf(&self, i: usize) -> bool {
93        self.live[i] && self.nodes[i].kind == NodeKind::Leaf && !self.nodes[i].hidden
94    }
95
96    /// The measurer check, the rebuild and the restyle every pass starts with.
97    fn prepare(&mut self, measurer: u64) {
98        self.ensure_reduced();
99        if self.last_measurer.is_some_and(|m| m != measurer) {
100            self.invalidate_measures();
101        }
102        self.last_measurer = Some(measurer);
103        if self.needs_build {
104            self.subtree_pending.clear();
105            self.rebuild();
106        } else {
107            for id in std::mem::take(&mut self.subtree_pending) {
108                if !self.rebuild_subtree(&id) {
109                    self.rebuild();
110                    self.subtree_pending.clear();
111                    break;
112                }
113            }
114        }
115        self.restyle();
116    }
117
118    /// Every live measured leaf without a (current) memo entry.
119    fn missing_leaves(&self) -> Vec<usize> {
120        (0..self.nodes.len())
121            .filter(|&i| self.is_measured_leaf(i) && (!self.memo.intrinsics.contains_key(&(i as u32)) || self.memo_versions.get(&(i as u32)).copied() != Some(self.node_state[i].content_version)))
122            .collect()
123    }
124
125    /// R8 F53: every stored answer is sanitised (`Intrinsics::sanitised`,
126    /// `finite_or_zero`): a host's NaN or infinite size never reaches the
127    /// memo, whose clamps would panic on it.
128    fn apply_intrinsics(&mut self, missing: &[usize], answers: &[Intrinsics]) {
129        for (k, &i) in missing.iter().enumerate() {
130            if let Some(a) = answers.get(k) {
131                self.memo.intrinsics.insert(i as u32, a.sanitised());
132                self.memo_versions.insert(i as u32, self.node_state[i].content_version);
133                self.engine.mark_dirty(i as u32);
134            }
135        }
136    }
137
138    fn apply_heights(&mut self, requests: &[HeightRequest], heights: &[f32]) {
139        for (k, r) in requests.iter().enumerate() {
140            if let Some(h) = heights.get(k) {
141                self.memo.insert_height(r.index, MeasureMemo::width_key(r.width), crate::measure::finite_or_zero(*h));
142                self.engine.mark_dirty(r.index);
143            }
144        }
145    }
146
147    /// Phase 2: the main tree and every layer against the memo; the height
148    /// requests (deduplicated) the memo could only guess.
149    fn compute_all(&mut self) -> (Vec<Frame>, Vec<Placed>, Vec<HeightRequest>) {
150        crate::roomy(|| self.compute_all_now())
151    }
152
153    fn compute_all_now(&mut self) -> (Vec<Frame>, Vec<Placed>, Vec<HeightRequest>) {
154        let (vw, vh) = self.viewport;
155        let guesses = RefCell::new(Vec::<HeightRequest>::new());
156        let mut frames = vec![Frame::default(); self.nodes.len()];
157        let mut placed = Vec::with_capacity(self.layers.len());
158        if let Some(root) = self.main_root {
159            // CSS parity (VAPP-91): a block-level surface root is as wide as
160            // the surface unless its own style sets a width; the web's
161            // inline-flex natives (base-css.ts) keep their content width.
162            // Round 3: a `bar` Segmented always fills (base-css.ts `width: 100%`).
163            let root_node = &self.nodes[root as usize];
164            let inline_root = matches!(root_node.component.as_str(), "Button" | "Toggle" | "Link" | "Icon" | "Ring" | "Spinner")
165                || (root_node.component == "Segmented" && (root_node.props.get("variant").and_then(|v| v.as_str()) != Some("bar") || crate::segmented::is_rail(&root_node.props)));
166            if !inline_root && self.engine.style(root).size.width == Dimension::auto() {
167                let mut style = self.engine.style(root).clone();
168                style.size.width = Dimension::percent(1.0);
169                self.engine.set_style(root, style);
170            }
171            let available = Size { width: AvailableSpace::Definite(vw.max(0.0)), height: if vh > 0.0 { AvailableSpace::Definite(vh) } else { AvailableSpace::MaxContent } };
172            self.compute(root, available, &guesses);
173            self.walk_frames(root, 0.0, 0.0, &mut frames);
174        }
175        let main_h = self.main_root.map(|r| frames[r as usize].h).unwrap_or(0.0);
176        let vh_eff = if vh > 0.0 { vh } else { self.max_height.unwrap_or(main_h) };
177        let ins = self.settings.insets;
178        let view = View { x: ins.left, y: ins.top, w: (vw - ins.left - ins.right).max(0.0), h: (vh_eff - ins.top - ins.bottom).max(0.0) };
179        let mut toast_stack = 0.0f32;
180        for k in 0..self.layers.len() {
181            let p = self.place_layer(k, view, vw, vh_eff, &mut frames, &guesses, &mut toast_stack);
182            placed.push(p);
183        }
184        let mut seen = HashSet::new();
185        let pending: Vec<HeightRequest> = guesses.into_inner().into_iter().filter(|r| seen.insert((r.index, MeasureMemo::width_key(r.width)))).collect();
186        (frames, placed, pending)
187    }
188
189    fn finish(&mut self, frames: Vec<Frame>, placed: Vec<Placed>, state: PassState) -> LayoutOutput {
190        let (_, vh) = self.viewport;
191        let bounded_h = if vh > 0.0 { Some(vh) } else { self.max_height };
192        let layout_ns = state.started.elapsed().as_nanos() as u64;
193        self.output(frames, placed, bounded_h, state.rounds, state.upcalls, layout_ns)
194    }
195
196    /// Prepare the tree, then run the batched passes. At most 3 upcall
197    /// rounds (intrinsics, heights, one correction). The measurer is called
198    /// in-process with borrowed requests; [`Self::layout_begin`] is the same
199    /// pass as resumable STEPS for a host that must not be called back
200    /// while the surface is locked (the FFI).
201    pub fn layout(&mut self, measure: &mut dyn Measure) -> LayoutOutput {
202        let mut state = PassState { started: Instant::now(), upcalls: 0, rounds: 0, missing: Vec::new(), heights: Vec::new() };
203        self.prepare(measure.measure_id());
204        loop {
205            state.rounds += 1;
206            let missing = self.missing_leaves();
207            if !missing.is_empty() {
208                let requests: Vec<LeafRequest> = missing.iter().map(|&i| self.leaf_request(i)).collect();
209                let answers = measure.measure_intrinsics(&requests);
210                state.upcalls += 1;
211                self.apply_intrinsics(&missing, &answers);
212            }
213            let (frames, placed, pending) = self.compute_all();
214            if pending.is_empty() || state.rounds >= 3 {
215                return self.finish(frames, placed, state);
216            }
217            let leaves: Vec<LeafRequest> = pending.iter().map(|r| self.leaf_request(r.index as usize)).collect();
218            let heights = measure.measure_heights(&leaves, &pending);
219            state.upcalls += 1;
220            self.apply_heights(&pending, &heights);
221        }
222    }
223
224    fn owned_leaves(&self, indices: impl Iterator<Item = usize>) -> Vec<LeafData> {
225        indices
226            .map(|i| {
227                let n = &self.nodes[i];
228                let s = &self.node_state[i];
229                LeafData { index: i as u32, id: n.id.clone(), component: n.component.clone(), part: n.part.clone(), owner_component: n.owner_component.clone(), props: n.props.clone(), text_style: s.text.clone(), control: s.control, lines: n.lines }
230            })
231            .collect()
232    }
233
234    /// Start a layout pass as STEPS: answer [`LayoutStep::Intrinsics`] with
235    /// [`Self::layout_intrinsics`] and [`LayoutStep::Heights`] with
236    /// [`Self::layout_heights`] until [`LayoutStep::Done`]. Same rounds and
237    /// upcalls as [`Self::layout`]; the host measures between the steps
238    /// without holding the surface.
239    pub fn layout_begin(&mut self, measure_id: u64) -> LayoutStep {
240        self.prepare(measure_id);
241        self.step_state = Some(PassState { started: Instant::now(), upcalls: 0, rounds: 0, missing: Vec::new(), heights: Vec::new() });
242        self.step_round()
243    }
244
245    fn step_round(&mut self) -> LayoutStep {
246        let missing = self.missing_leaves();
247        let Some(state) = self.step_state.as_mut() else { return LayoutStep::Idle };
248        state.rounds += 1;
249        if !missing.is_empty() {
250            let leaves = self.owned_leaves(missing.iter().copied());
251            self.step_state.as_mut().expect("state").missing = missing;
252            return LayoutStep::Intrinsics(leaves);
253        }
254        self.step_compute()
255    }
256
257    fn step_compute(&mut self) -> LayoutStep {
258        let (frames, placed, pending) = self.compute_all();
259        let Some(mut state) = self.step_state.take() else { return LayoutStep::Idle };
260        if pending.is_empty() || state.rounds >= 3 {
261            return LayoutStep::Done(Box::new(self.finish(frames, placed, state)));
262        }
263        let leaves = self.owned_leaves(pending.iter().map(|r| r.index as usize));
264        state.heights = pending.clone();
265        self.step_state = Some(state);
266        LayoutStep::Heights(leaves, pending)
267    }
268
269    /// Abandon a stepped pass (the host's measurer failed): the next
270    /// `layout_begin` starts clean; the memo keeps what was answered.
271    pub fn layout_abort(&mut self) {
272        self.step_state = None;
273    }
274
275    /// Is a stepped pass in progress?
276    pub fn layout_in_progress(&self) -> bool {
277        self.step_state.is_some()
278    }
279
280    /// The answers to [`LayoutStep::Intrinsics`], in request order.
281    pub fn layout_intrinsics(&mut self, answers: &[Intrinsics]) -> LayoutStep {
282        let Some(state) = self.step_state.as_mut() else { return LayoutStep::Idle };
283        state.upcalls += 1;
284        let missing = std::mem::take(&mut state.missing);
285        self.apply_intrinsics(&missing, answers);
286        self.step_compute()
287    }
288
289    /// The answers to [`LayoutStep::Heights`], in request order.
290    pub fn layout_heights(&mut self, heights: &[f32]) -> LayoutStep {
291        let Some(state) = self.step_state.as_mut() else { return LayoutStep::Idle };
292        state.upcalls += 1;
293        let requests = std::mem::take(&mut state.heights);
294        self.apply_heights(&requests, heights);
295        self.step_round()
296    }
297
298    /// Lay out the tree under `root`, answering leaves from the memo.
299    fn compute(&mut self, root: u32, available: Size<AvailableSpace>, guesses: &RefCell<Vec<HeightRequest>>) {
300        let Surface { engine, memo, nodes, .. } = self;
301        let mut measure_fn = |index: u32, known: Size<Option<f32>>, avail: Size<AvailableSpace>| -> LeafAnswer {
302            if nodes[index as usize].hidden {
303                return LeafAnswer::default();
304            }
305            let req = MeasureRequest { index, known_width: known.width, known_height: known.height, available_width: avail.width, available_height: avail.height };
306            let (size, guessed, baseline) = memo.answer(&req);
307            if guessed {
308                guesses.borrow_mut().push(HeightRequest { index, width: size.width });
309            }
310            LeafAnswer { size, baseline }
311        };
312        engine.compute(root, available, &mut measure_fn);
313    }
314
315    fn walk_frames(&self, slot: u32, ox: f32, oy: f32, out: &mut [Frame]) {
316        let l = self.engine.layout(slot);
317        let x = ox + l.location.x;
318        let y = oy + l.location.y;
319        out[slot as usize] = Frame { x, y, w: l.size.width, h: l.size.height };
320        for &c in &self.nodes[slot as usize].children {
321            self.walk_frames(c, x, y, out);
322        }
323    }
324
325    /// A layer root's style from its restyled flat map plus `extra` edits
326    /// (size bounds, safe-area padding) — recomputed each pass so the edits
327    /// never accumulate.
328    fn layer_root_style(&mut self, root: u32, pad_extra: [f32; 4], edit: impl FnOnce(&mut Style)) {
329        let mut flat = self.node_state[root as usize].flat.clone();
330        if pad_extra.iter().any(|p| *p != 0.0) {
331            let base = style::sides_px(&flat, "padding", "paddingHorizontal", "paddingVertical", ["paddingTop", "paddingRight", "paddingBottom", "paddingLeft"]).unwrap_or([0.0; 4]);
332            for (k, key) in ["paddingTop", "paddingRight", "paddingBottom", "paddingLeft"].iter().enumerate() {
333                flat.insert((*key).to_string(), serde_json::json!(base[k] + pad_extra[k]));
334            }
335        }
336        let mut s = style::to_taffy(&flat, self.direction, BoxKind::Container).unwrap_or_default();
337        edit(&mut s);
338        self.engine.set_style(root, s);
339    }
340
341    #[allow(clippy::too_many_arguments)]
342    fn place_layer(&mut self, k: usize, view: View, vw: f32, vh: f32, frames: &mut [Frame], guesses: &RefCell<Vec<HeightRequest>>, toast_stack: &mut f32) -> Placed {
343        let spec = self.layers[k].clone();
344        let root = spec.root;
345        let pad = OVERLAY_PADDING as f32;
346        let rtl = self.direction == Direction::Rtl;
347        let ins = self.settings.insets;
348        let finite = vh.is_finite() && vh > 0.0;
349        let body_shrinks = spec.scroll_body.is_some();
350        let (placement, position, anchor_frame, x, y) = match spec.placement {
351            LayerPlacement::Centered => {
352                let max_h = finite.then_some((view.h - 2.0 * pad).max(0.0));
353                self.layer_root_style(root, [0.0; 4], |s| {
354                    if let (Some(h), true) = (max_h, body_shrinks) {
355                        s.max_size.height = length(h);
356                    }
357                });
358                let width = (view.w - 2.0 * pad).clamp(0.0, DIALOG_MAX_WIDTH);
359                self.compute(root, Size { width: AvailableSpace::Definite(width), height: AvailableSpace::MaxContent }, guesses);
360                let size = self.engine.layout(root).size;
361                let x = view.x + ((view.w - size.width) / 2.0).max(pad);
362                let y = if finite { view.y + ((view.h - size.height) / 2.0).max(pad) } else { view.y + pad };
363                (None, "centered".to_string(), None, x, y)
364            }
365            LayerPlacement::Edge(side) => {
366                let vertical = matches!(side, OverlaySide::Top | OverlaySide::Bottom);
367                // The sheet reaches the edge; its content stays clear of the
368                // safe area.
369                let extra = match side {
370                    OverlaySide::Bottom => [0.0, ins.right, ins.bottom, ins.left],
371                    OverlaySide::Top => [ins.top, ins.right, 0.0, ins.left],
372                    OverlaySide::Left => [ins.top, 0.0, ins.bottom, ins.left],
373                    OverlaySide::Right => [ins.top, ins.right, ins.bottom, 0.0],
374                };
375                self.layer_root_style(root, extra, |s| {
376                    if vertical && finite && body_shrinks {
377                        s.max_size.height = length((vh * SHEET_MAX_HEIGHT).max(0.0));
378                    }
379                });
380                let available = if vertical {
381                    Size { width: AvailableSpace::Definite(vw), height: AvailableSpace::MaxContent }
382                } else {
383                    Size { width: AvailableSpace::Definite((vw - 2.0 * pad).clamp(0.0, DRAWER_MAX_WIDTH)), height: if finite { AvailableSpace::Definite(vh) } else { AvailableSpace::MaxContent } }
384                };
385                self.compute(root, available, guesses);
386                let size = self.engine.layout(root).size;
387                let (x, y) = match side {
388                    OverlaySide::Top | OverlaySide::Left => (0.0, 0.0),
389                    OverlaySide::Bottom => (0.0, if finite { vh - size.height } else { 0.0 }),
390                    OverlaySide::Right => (vw - size.width, 0.0),
391                };
392                (None, side.as_str().to_string(), None, x, y)
393            }
394            LayerPlacement::Anchored { anchor, side, align } => {
395                let a = frames.get(anchor as usize).copied().unwrap_or_default();
396                let side = if spec.mirror && rtl {
397                    match side {
398                        OverlaySide::Left => OverlaySide::Right,
399                        OverlaySide::Right => OverlaySide::Left,
400                        s => s,
401                    }
402                } else {
403                    side
404                };
405                let size = self.shrink_to_content(root, view, pad, spec.match_anchor_width.then_some(a.w), guesses);
406                let p = self.anchor_place(a, size, view, side, align, rtl);
407                let pos = p.side.as_str().to_string();
408                (Some(p), pos, Some(a), p.x as f32, p.y as f32)
409            }
410            LayerPlacement::AtPoint { x, y } => {
411                let a = Frame { x, y, w: 0.0, h: 0.0 };
412                let size = self.shrink_to_content(root, view, pad, None, guesses);
413                let p = self.anchor_place(a, size, view, OverlaySide::Bottom, OverlayAlign::Start, rtl);
414                (Some(p), "point".to_string(), Some(a), p.x as f32, p.y as f32)
415            }
416            LayerPlacement::Toast { .. } => {
417                let max = (view.w - 2.0 * pad).clamp(0.0, TOAST_MAX_WIDTH);
418                self.compute(root, Size { width: AvailableSpace::MaxContent, height: AvailableSpace::MaxContent }, guesses);
419                let natural = self.engine.layout(root).size.width;
420                if natural > max {
421                    self.compute(root, Size { width: AvailableSpace::Definite(max), height: AvailableSpace::MaxContent }, guesses);
422                }
423                let size = self.engine.layout(root).size;
424                let wide = self.breakpoints().get("md").is_some_and(|md| vw >= *md);
425                let x = if wide {
426                    if rtl {
427                        view.x + pad
428                    } else {
429                        view.x + view.w - pad - size.width
430                    }
431                } else {
432                    view.x + (view.w - size.width) / 2.0
433                };
434                let y = view.y + view.h - pad - *toast_stack - size.height;
435                *toast_stack += size.height + TOAST_GAP;
436                (None, "toast".to_string(), None, x, y)
437            }
438        };
439        self.walk_frames(root, x, y, frames);
440        (placement, position, anchor_frame)
441    }
442
443    /// Lay a popup out at its natural width, clamped to the view (and at
444    /// least `min_width`).
445    fn shrink_to_content(&mut self, root: u32, view: View, pad: f32, min_width: Option<f32>, guesses: &RefCell<Vec<HeightRequest>>) -> OSize {
446        self.layer_root_style(root, [0.0; 4], |s| {
447            if let Some(w) = min_width {
448                s.min_size.width = length(w);
449            }
450        });
451        let max = (view.w - 2.0 * pad).max(0.0);
452        self.compute(root, Size { width: AvailableSpace::MaxContent, height: AvailableSpace::MaxContent }, guesses);
453        let natural = self.engine.layout(root).size.width;
454        if natural > max {
455            self.compute(root, Size { width: AvailableSpace::Definite(max), height: AvailableSpace::MaxContent }, guesses);
456        }
457        let size = self.engine.layout(root).size;
458        OSize { width: size.width as f64, height: size.height as f64 }
459    }
460
461    /// `place_overlay` inside the safe area (align start/end follow the
462    /// direction).
463    fn anchor_place(&self, a: Frame, size: OSize, view: View, side: OverlaySide, align: OverlayAlign, rtl: bool) -> OverlayPlacement {
464        let align = match (align, rtl) {
465            (OverlayAlign::Start, true) => OverlayAlign::End,
466            (OverlayAlign::End, true) => OverlayAlign::Start,
467            (a, _) => a,
468        };
469        let anchor = ORect { x: (a.x - view.x) as f64, y: (a.y - view.y) as f64, width: a.w as f64, height: a.h as f64 };
470        let vp = OSize { width: view.w as f64, height: if view.h > 0.0 { view.h as f64 } else { f64::INFINITY } };
471        let mut p = place_overlay(&anchor, &size, &vp, &PlaceOptions { side, align, ..PlaceOptions::default() });
472        p.x += view.x as f64;
473        p.y += view.y as f64;
474        p
475    }
476
477    fn preorder(&self, slot: u32, out: &mut Vec<u32>) {
478        out.push(slot);
479        for &c in &self.nodes[slot as usize].children {
480            self.preorder(c, out);
481        }
482    }
483
484    /// Round 2 (§5): where a list's viewport comes from — itself (bounded
485    /// with taller content on its axis), its nearest scrolling ancestor, or
486    /// the host viewport — and how long it is.
487    fn list_view(&self, spec: &crate::layout_tree::ListSpec, frames: &[Frame], scrolls: &[ScrollOutput]) -> crate::layout_tree::ListView {
488        use crate::layout_tree::{ListView, ListViewSource};
489        let h = spec.horizontal;
490        let ext = |f: Frame| if h { f.w } else { f.h };
491        let start = |f: Frame| if h { f.x } else { f.y };
492        let scrolls_on_axis = |idx: u32| {
493            scrolls.iter().any(|s| s.index == idx && if h { s.scroll_x && s.content_width > frames[idx as usize].w + 0.5 } else { s.scroll_y && s.content_height > frames[idx as usize].h + 0.5 })
494        };
495        let f = frames[spec.node as usize];
496        if scrolls_on_axis(spec.node) {
497            return ListView { source: ListViewSource::Own, viewport: ext(f) };
498        }
499        let mut cur = self.nodes[spec.node as usize].parent;
500        while let Some(p) = cur {
501            if scrolls_on_axis(p) {
502                let pf = frames[p as usize];
503                return ListView { source: ListViewSource::Ancestor { id: self.scroll_key(p), rel: start(f) - start(pf) }, viewport: ext(pf) };
504            }
505            cur = self.nodes[p as usize].parent;
506        }
507        let viewport = if h {
508            self.viewport.0
509        } else if self.viewport.1 > 0.0 {
510            self.viewport.1
511        } else {
512            self.max_height.unwrap_or(2000.0)
513        };
514        ListView { source: ListViewSource::Host { rel: start(f) }, viewport }
515    }
516
517    /// Round 2: the paint offsets of `position: sticky` nodes (pinned by
518    /// their insets inside the nearest scroller while their parent is in
519    /// view) and of pinned List section headers.
520    fn sticky_offsets(&mut self, frames: &[Frame], scrolls: &[ScrollOutput], drawn: &[u32]) -> Vec<super::StickyOutput> {
521        let mut out = Vec::new();
522        // Pinned section headers (on the window's memoized offsets).
523        for spec in self.lists.iter().filter(|l| l.sticky && !l.headers.is_empty()) {
524            let Some(window) = self.local.lists.get_mut(&spec.id) else { continue };
525            let Some(view) = self.local.list_views.get(&spec.id) else { continue };
526            let axis = |p: (f32, f32)| if spec.horizontal { p.0 } else { p.1 };
527            let scroll = match &view.source {
528                crate::layout_tree::ListViewSource::Own => self.local.scroll.get(&spec.id).copied().map(axis).unwrap_or(0.0),
529                crate::layout_tree::ListViewSource::Ancestor { id, rel } => self.local.scroll.get(id).copied().map(axis).unwrap_or(0.0) - rel,
530                crate::layout_tree::ListViewSource::Host { rel } => self.local.surface_scroll.map(|p| axis(p) - rel).unwrap_or_else(|| self.local.scroll.get(&spec.id).copied().map(axis).unwrap_or(0.0)),
531            }
532            .max(0.0);
533            window.gap = spec.gap;
534            let Some((pin, row_offset)) = window.sticky_header_cached(&spec.keys, &spec.headers, scroll as f64) else { continue };
535            let key = &spec.keys[pin.row];
536            let Some(&header) = self.nodes[spec.node as usize].children.iter().find(|c| self.nodes[**c as usize].props.get("key").and_then(serde_json::Value::as_str) == Some(key.as_str())) else { continue };
537            let delta = (pin.offset - row_offset) as f32;
538            // The header's frame already sits at its row offset (in flow or
539            // absolute); `delta` moves it to the pinned position.
540            let (dx, dy) = if spec.horizontal { (delta, 0.0) } else { (0.0, delta) };
541            if dx.abs() > 0.01 || dy.abs() > 0.01 {
542                out.push(super::StickyOutput { index: header, dx, dy });
543            }
544        }
545        // `position: sticky`.
546        for &i in drawn {
547            let flat = &self.node_state[i as usize].flat;
548            if flat.get("position").and_then(serde_json::Value::as_str) != Some("sticky") {
549                continue;
550            }
551            // A physical side, else the `inset` shorthand (logical insets
552            // are physical by now).
553            let all = flat.get("inset").and_then(style::px);
554            let inset = |k: &str| flat.get(k).and_then(style::px).or(all);
555            let (top, bottom, left, right) = (inset("top"), inset("bottom"), inset("left"), inset("right"));
556            let n = frames[i as usize];
557            let parent_index = self.nodes[i as usize].parent;
558            let mut parent = parent_index.map(|p| frames[p as usize]).unwrap_or(n);
559            // The nearest scroller: an ancestor that scrolls, else the host
560            // viewport (its offset = `set_surface_scroll`).
561            let mut cur = parent_index;
562            let mut port: Option<(Frame, f32, f32)> = None;
563            while let Some(p) = cur {
564                if let Some(s) = scrolls.iter().find(|s| s.index == p && ((s.scroll_y && s.content_height > frames[p as usize].h + 0.5) || (s.scroll_x && s.content_width > frames[p as usize].w + 0.5))) {
565                    port = Some((frames[p as usize], s.offset_x, s.offset_y));
566                    // A direct child of the scroller is bounded by its whole
567                    // scrollable content (CSS: a sticky header in a scrolling
568                    // pane stays pinned all the way down).
569                    if Some(p) == parent_index {
570                        parent = Frame { w: s.content_width.max(parent.w), h: s.content_height.max(parent.h), ..parent };
571                    }
572                    break;
573                }
574                cur = self.nodes[p as usize].parent;
575            }
576            let (pf, ox, oy) = port.unwrap_or((Frame { x: 0.0, y: 0.0, w: self.viewport.0, h: self.viewport.1 }, self.local.surface_scroll.unwrap_or_default().0, self.local.surface_scroll.unwrap_or_default().1));
577            // The start edges pin against the scroll offset alone; the end
578            // edges need the scroller's extent (an auto-height host surface
579            // has none: bottom/right never pin there).
580            let mut dy = 0.0f32;
581            if let Some(t) = top {
582                let edge = pf.y + oy + t;
583                if n.y < edge {
584                    dy = (edge.min(parent.y + parent.h - n.h) - n.y).max(0.0);
585                }
586            } else if let (Some(b), true) = (bottom, pf.h > 0.0) {
587                let edge = pf.y + oy + pf.h - b - n.h;
588                if n.y > edge {
589                    dy = (edge.max(parent.y) - n.y).min(0.0);
590                }
591            }
592            let mut dx = 0.0f32;
593            if let Some(l) = left {
594                let edge = pf.x + ox + l;
595                if n.x < edge {
596                    dx = (edge.min(parent.x + parent.w - n.w) - n.x).max(0.0);
597                }
598            } else if let (Some(r), true) = (right, pf.w > 0.0) {
599                let edge = pf.x + ox + pf.w - r - n.w;
600                if n.x > edge {
601                    dx = (edge.max(parent.x) - n.x).min(0.0);
602                }
603            }
604            if dx.abs() > 0.01 || dy.abs() > 0.01 {
605                out.push(super::StickyOutput { index: i, dx, dy });
606            }
607        }
608        out
609    }
610
611    fn output(&mut self, frames: Vec<Frame>, placed: Vec<Placed>, bounded_h: Option<f32>, rounds: u32, upcalls: u32, layout_ns: u64) -> LayoutOutput {
612        let mut main = Vec::new();
613        let mut drawn: Vec<u32> = Vec::new();
614        if let Some(root) = self.main_root {
615            self.preorder(root, &mut drawn);
616        }
617        for &i in &drawn {
618            let f = frames[i as usize];
619            main.push(PlacedFrame { index: i, x: f.x, y: f.y, w: f.w, h: f.h });
620        }
621        let mut layers = Vec::new();
622        for (spec, (placement, position, anchor_frame)) in self.layers.clone().into_iter().zip(placed) {
623            let mut order = Vec::new();
624            self.preorder(spec.root, &mut order);
625            let lf = order.iter().map(|&i| {
626                let f = frames[i as usize];
627                PlacedFrame { index: i, x: f.x, y: f.y, w: f.w, h: f.h }
628            });
629            let frames_out: Vec<PlacedFrame> = lf.collect();
630            drawn.extend(order);
631            layers.push(Layer { layer: spec.layer, kind: spec.kind.clone(), owner: spec.owner.clone(), root: spec.root, anchor_frame, placement, position, class: spec.class, modal: spec.modal, dismissible: spec.dismissible, frames: frames_out });
632        }
633        // `%` radii against the laid-out boxes.
634        for &i in &drawn {
635            let s = &self.node_state[i as usize];
636            let Some(pct) = s.radius_pct else { continue };
637            let f = frames[i as usize];
638            let side = f.w.min(f.h);
639            let mut v = s.visual.clone();
640            if let Some(all) = pct.all {
641                v.border_radius = Some(all * side);
642            }
643            if pct.corners.iter().any(Option::is_some) {
644                let base = v.border_radius.unwrap_or(0.0);
645                let mut radii = v.corner_radii.unwrap_or([base; 4]);
646                for (k, c) in pct.corners.iter().enumerate() {
647                    if let Some(c) = c {
648                        radii[k] = c * side;
649                    }
650                }
651                v.corner_radii = Some(radii);
652            }
653            if v != s.visual {
654                self.node_state[i as usize].visual = v;
655                self.visuals_dirty.insert(i);
656            }
657        }
658        // Scroll containers: offsets clamped to the content.
659        let mut scrolls = Vec::new();
660        let mut reclamped = false;
661        for &i in &drawn {
662            let s = &self.node_state[i as usize];
663            let (sx, sy) = style::is_scroll_container(&s.flat);
664            if !sx && !sy {
665                continue;
666            }
667            let l = self.engine.layout(i);
668            let f = frames[i as usize];
669            let content_w = l.content_size.width.max(f.w);
670            let content_h = l.content_size.height.max(f.h);
671            let id = self.scroll_key(i);
672            let asked = self.local.scroll.get(&id).copied();
673            let (ox, oy) = asked.unwrap_or((0.0, 0.0));
674            let ox = if sx { ox.clamp(0.0, (content_w - f.w).max(0.0)) } else { 0.0 };
675            let oy = if sy { oy.clamp(0.0, (content_h - f.h).max(0.0)) } else { 0.0 };
676            // The clamped offset IS the state (content shrank, a fling
677            // overshot): a windowed list re-windows at it next pass. A
678            // container whose content fits is not its own scroller (a page
679            // scrolls it: the host's offset positions the window) and keeps
680            // the offset it was given.
681            let own_x = content_w > f.w + 0.5;
682            let own_y = content_h > f.h + 0.5;
683            if let Some((ax, ay)) = asked {
684                let (kx, ky) = (if own_x && sx { ox } else { ax }, if own_y && sy { oy } else { ay });
685                if (ax - kx).abs() > 0.01 || (ay - ky).abs() > 0.01 {
686                    let (ox, oy) = (kx, ky);
687                    self.local.scroll.insert(id.clone(), (ox, oy));
688                    if let Some(w) = self.local.lists.get_mut(&id) {
689                        w.scroll_offset = oy;
690                        if (ay - oy).abs() > 0.01 && self.lists.iter().any(|l| l.id == id && l.windowed) {
691                            self.subtree_pending.insert(id.clone());
692                            reclamped = true;
693                        }
694                    }
695                }
696            }
697            scrolls.push(ScrollOutput { index: i, offset_x: ox, offset_y: oy, content_width: content_w, content_height: content_h, scroll_x: sx, scroll_y: sy });
698        }
699        // Windowed lists (round 2): remember the items' real extents on the
700        // list's axis, then window against the list's own, its nearest
701        // scrolling ancestor's or the host's viewport.
702        let mut lists = Vec::new();
703        let mut rewindow = false;
704        for spec in self.lists.clone() {
705            let view = self.list_view(&spec, &frames, &scrolls);
706            self.local.list_views.insert(spec.id.clone(), view.clone());
707            let axis = |p: (f32, f32)| if spec.horizontal { p.0 } else { p.1 };
708            let scroll = match &view.source {
709                crate::layout_tree::ListViewSource::Own => self.local.scroll.get(&spec.id).copied().map(axis).unwrap_or(0.0),
710                crate::layout_tree::ListViewSource::Ancestor { id, rel } => self.local.scroll.get(id).copied().map(axis).unwrap_or(0.0) - rel,
711                crate::layout_tree::ListViewSource::Host { rel } => self.local.surface_scroll.map(|p| axis(p) - rel).unwrap_or_else(|| self.local.scroll.get(&spec.id).copied().map(axis).unwrap_or(0.0)),
712            }
713            .max(0.0);
714            let estimate = self.effective.as_ref().and_then(|t| t.tokens.control.get("row").copied()).map(|v| v as f32).unwrap_or(crate::list::DEFAULT_ESTIMATED_ITEM_HEIGHT);
715            let window = self.local.lists.entry(spec.id.clone()).or_insert_with(|| crate::list::ListWindow::new(estimate, crate::list::DEFAULT_OVERSCAN, spec.gap));
716            let mut changed = false;
717            for child in &self.nodes[spec.node as usize].children {
718                let c = &self.nodes[*child as usize];
719                if matches!(c.part.as_deref(), Some("divider" | "empty" | "spacer")) {
720                    continue;
721                }
722                let key = c.props.get("key").and_then(serde_json::Value::as_str).filter(|_| matches!(c.part.as_deref(), Some("row" | "section"))).map(str::to_string).unwrap_or_else(|| c.id.clone());
723                let f = frames[*child as usize];
724                changed |= window.record(&key, if spec.horizontal { f.w } else { f.h });
725            }
726            if spec.windowed {
727                window.scroll_offset = scroll;
728                let offsets = window.offsets_cached(&spec.keys);
729                let range = window.visible_range(&offsets, view.viewport.max(1.0));
730                let count = spec.keys.len();
731                let pad = |o: &[f32], r: crate::list::VisibleRange| (o[r.start], o[count] - o[r.end.min(count)]);
732                let covered = range.start >= spec.range.start && range.end <= spec.range.end;
733                // Re-window when the measured extents move the window or
734                // the spacers, or when the viewport now shows items the
735                // window does not hold.
736                if (changed && (range != spec.range || pad(&offsets, range) != pad(&spec.offsets, spec.range))) || !covered {
737                    self.subtree_pending.insert(spec.id.clone());
738                    rewindow |= !covered;
739                }
740            }
741            let content_height = window.offsets_cached(&spec.keys).last().copied().unwrap_or(0.0);
742            lists.push(ListOutput { id: spec.id.clone(), node: spec.node, content_height, start: spec.range.start as u32, end: spec.range.end as u32, count: spec.keys.len() as u32, windowed: spec.windowed, horizontal: spec.horizontal });
743        }
744        if rewindow && !self.pending.iter().any(|e| matches!(e, super::OutEvent::Relayout)) {
745            self.pending.push(super::OutEvent::Relayout);
746        }
747        let sticky = self.sticky_offsets(&frames, &scrolls, &drawn);
748        self.local.sticky_any = drawn.iter().any(|&i| self.node_state[i as usize].flat.get("position").and_then(serde_json::Value::as_str) == Some("sticky"));
749        self.scrolls = scrolls.clone();
750        if reclamped && !self.pending.iter().any(|e| matches!(e, super::OutEvent::Relayout)) {
751            // The window moved under the host: one more pass shows the rows.
752            self.pending.push(super::OutEvent::Relayout);
753        }
754        let root_frame = self.main_root.map(|r| frames[r as usize]).unwrap_or_default();
755        self.last_frames = frames;
756        let mut visual_changes: Vec<u32> = self.visuals_dirty.drain().collect();
757        visual_changes.sort_unstable();
758        let delta = NodeDelta {
759            added: std::mem::take(&mut self.delta_added).into_iter().collect(),
760            removed: std::mem::take(&mut self.delta_removed).into_iter().collect(),
761            changed: std::mem::take(&mut self.delta_changed).into_iter().collect(),
762            renumbered: std::mem::take(&mut self.renumbered),
763        };
764        LayoutOutput {
765            frames: main,
766            layers,
767            lists,
768            scrolls,
769            sticky,
770            toasts: self.toasts.clone(),
771            visual_changes,
772            delta,
773            structure_version: self.structure_version,
774            surface_width: root_frame.w,
775            surface_height: root_frame.h,
776            overflow: bounded_h.is_some_and(|h| root_frame.h > h + 0.5),
777            direction: if self.direction == Direction::Rtl { "rtl".into() } else { "ltr".into() },
778            breakpoint: self.breakpoint.clone(),
779            measure_rounds: rounds,
780            upcalls,
781            restyled: std::mem::take(&mut self.restyled),
782            rebuilt: std::mem::take(&mut self.rebuilt),
783            built_nodes: std::mem::take(&mut self.built_nodes),
784            layout_ns,
785        }
786    }
787}