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