Skip to main content

blitz_dom/layout/
mod.rs

1//! Enable the dom to lay itself out using taffy
2//!
3//! In servo, style and layout happen together during traversal
4//! However, in Blitz, we do a style pass then a layout pass.
5//! This is slower, yes, but happens fast enough that it's not a huge issue.
6
7use crate::node::{ImageData, NodeData, SpecialElementData};
8use crate::{document::BaseDocument, dom_node_id, node::Node, taffy_node_id};
9use markup5ever::local_name;
10use std::cell::Ref;
11use std::sync::Arc;
12use style::Atom;
13use style::values::computed::CSSPixelLength;
14use style::values::computed::length_percentage::CalcLengthPercentage;
15use taffy::{
16    BlockContext, CollapsibleMarginSet, FlexDirection, LayoutPartialTree, MaybeResolve, NodeId,
17    ResolveOrZero, RoundTree, Style, TraversePartialTree, TraverseTree, compute_block_layout,
18    compute_cached_layout, compute_flexbox_layout, compute_grid_layout, compute_leaf_layout,
19    prelude::*,
20};
21
22/// Name the element a layout panic happened on. `BLITZ_TRACE_LAYOUT_PANIC=1`.
23///
24/// Layout runs percentages, `calc()` and every length through stylo, and when
25/// stylo gives up it does so with `unreachable!()` deep inside its own value
26/// types. The message names a line in a registry crate and not one frame of
27/// ours, and a release backtrace is 78 frames of `__mh_execute_header`, so the
28/// log says a value was impossible without saying which value, on which
29/// element, in which document. AgencyZero 0.6.1 aborted two seconds after boot
30/// on exactly that and the log could not narrow it past "stylo".
31///
32/// This keeps a stack of one-line element descriptions for the nodes currently
33/// being laid out and prints the innermost few from a panic hook. Off unless
34/// the variable is set: it formats a string per node, which is far too much for
35/// a shipping build and nothing at all for a debugging run.
36#[cfg(not(target_arch = "wasm32"))]
37pub(crate) mod layout_panic_probe {
38    use std::cell::RefCell;
39    use std::sync::OnceLock;
40
41    thread_local! {
42        static IN_FLIGHT: RefCell<Vec<String>> = const { RefCell::new(Vec::new()) };
43        /// Deepest node entered, kept past the unwind on purpose: `pop` only
44        /// runs on the way out, so after a panic this still names the culprit.
45        static INNERMOST: std::cell::Cell<Option<blitz_traits::node_id::NodeId>> =
46            const { std::cell::Cell::new(None) };
47    }
48
49    pub(crate) fn enabled() -> bool {
50        static ENABLED: OnceLock<bool> = OnceLock::new();
51        *ENABLED.get_or_init(|| {
52            let on = std::env::var_os("BLITZ_TRACE_LAYOUT_PANIC").is_some();
53            if on {
54                install_hook();
55            }
56            on
57        })
58    }
59
60    /// Chained, never replacing: the hook already installed is what writes the
61    /// panic to the application's log file, and an app whose stderr goes
62    /// nowhere loses the message entirely if this takes that job over.
63    fn install_hook() {
64        let previous = std::panic::take_hook();
65        std::panic::set_hook(Box::new(move |info| {
66            IN_FLIGHT.with(|stack| {
67                let stack = stack.borrow();
68                if stack.is_empty() {
69                    eprintln!("[blitz-layout-panic] no layout in flight on this thread");
70                } else {
71                    eprintln!("[blitz-layout-panic] innermost first:");
72                    for entry in stack.iter().rev().take(12) {
73                        eprintln!("[blitz-layout-panic]   {entry}");
74                    }
75                    eprintln!("[blitz-layout-panic] ({} deep)", stack.len());
76                }
77            });
78            previous(info);
79        }));
80    }
81
82    /// Deeper than any real document nests. A page that reaches this is
83    /// recursing, not laying out.
84    const RUNAWAY_DEPTH: usize = 512;
85
86    /// The node whose layout was in flight when everything stopped, so the
87    /// caller that still holds the document can serialize its markup.
88    pub(crate) fn innermost_node() -> Option<blitz_traits::node_id::NodeId> {
89        INNERMOST.with(std::cell::Cell::get)
90    }
91
92    pub(crate) fn push(node_id: blitz_traits::node_id::NodeId, description: String) {
93        INNERMOST.with(|cell| cell.set(Some(node_id)));
94        IN_FLIGHT.with(|stack| {
95            let mut stack = stack.borrow_mut();
96            stack.push(description);
97            if stack.len() == RUNAWAY_DEPTH {
98                // Reported here rather than left to the panic hook, because
99                // runaway layout does not reliably panic: it exhausts the
100                // stack, and what comes back is a `SIGSEGV` on the guard page
101                // or "fatal runtime error: stack overflow", neither of which
102                // runs a hook or leaves a line in the log. This is the last
103                // moment the evidence still exists.
104                eprintln!(
105                    "[blitz-layout-panic] runaway: {RUNAWAY_DEPTH} nested layouts, innermost first:"
106                );
107                for entry in stack.iter().rev().take(24) {
108                    eprintln!("[blitz-layout-panic]   {entry}");
109                }
110            }
111        });
112    }
113
114    pub(crate) fn pop() {
115        IN_FLIGHT.with(|stack| {
116            stack.borrow_mut().pop();
117        });
118    }
119}
120
121/// How much of the tree a single resolve actually recomputed.
122///
123/// Phase timings say layout is expensive; they cannot say whether that is a
124/// handful of slow nodes or the whole tree missing its cache. These counters
125/// answer that, and a wrong answer sends the fix to the wrong place entirely.
126/// Thread-local and read once per resolve, so the counting itself is free.
127#[cfg(feature = "log-phase-times")]
128pub mod layout_counters {
129    use blitz_traits::node_id::NodeId;
130    use std::cell::Cell;
131
132    thread_local! {
133        static ACTIVE: Cell<bool> = const { Cell::new(false) };
134        static COMPUTED: Cell<u64> = const { Cell::new(0) };
135        static CACHES_CLEARED: Cell<u64> = const { Cell::new(0) };
136        static LOOKUPS: Cell<u64> = const { Cell::new(0) };
137        static HITS: Cell<u64> = const { Cell::new(0) };
138        /// Distinct nodes recomputed, to tell "the whole tree once" apart from
139        /// "a few nodes many times". Those have completely different fixes and
140        /// the totals alone cannot distinguish them.
141        static DISTINCT: std::cell::RefCell<std::collections::HashMap<NodeId, u32>> =
142            std::cell::RefCell::new(std::collections::HashMap::new());
143    }
144
145    /// Select collection once for the whole resolve and reset its scratch data.
146    pub(crate) fn begin(active: bool) {
147        ACTIVE.with(|enabled| enabled.set(active));
148        if !active {
149            return;
150        }
151        COMPUTED.with(|count| count.set(0));
152        CACHES_CLEARED.with(|count| count.set(0));
153        LOOKUPS.with(|count| count.set(0));
154        HITS.with(|count| count.set(0));
155        DISTINCT.with(|seen| seen.borrow_mut().clear());
156    }
157
158    #[inline(always)]
159    fn active() -> bool {
160        ACTIVE.with(Cell::get)
161    }
162
163    pub(crate) fn note_computed(node_id: NodeId) {
164        if !active() {
165            return;
166        }
167        COMPUTED.with(|count| count.set(count.get() + 1));
168        DISTINCT.with(|seen| {
169            *seen.borrow_mut().entry(node_id).or_insert(0u32) += 1;
170        });
171    }
172
173    /// The nodes recomputed most often, worst first.
174    ///
175    /// Totals say the work is concentrated; only the identities say where. A
176    /// node recomputed a hundred times is either being measured under a hundred
177    /// different constraints or sitting under a container that re-descends, and
178    /// naming it is the difference between fixing that and guessing again.
179    pub(crate) fn worst_offenders(limit: usize) -> Vec<(NodeId, u32)> {
180        DISTINCT.with(|seen| {
181            let mut rows: Vec<(NodeId, u32)> = seen
182                .borrow()
183                .iter()
184                .map(|(id, count)| (*id, *count))
185                .collect();
186            rows.sort_by_key(|(_, count)| std::cmp::Reverse(*count));
187            rows.truncate(limit);
188            rows
189        })
190    }
191
192    pub(crate) fn note_cache_cleared() {
193        if !active() {
194            return;
195        }
196        CACHES_CLEARED.with(|count| count.set(count.get() + 1));
197    }
198
199    pub(crate) fn note_lookup(hit: bool) {
200        if !active() {
201            return;
202        }
203        LOOKUPS.with(|count| count.set(count.get() + 1));
204        if hit {
205            HITS.with(|count| count.set(count.get() + 1));
206        }
207    }
208
209    /// Public so a test or a harness can read what a single resolve cost without
210    /// scraping the per-frame stdout line. Feature-gated with the counting itself,
211    /// so a release build has neither.
212    #[derive(Clone, Copy)]
213    pub struct LayoutCounts {
214        pub computed: u64,
215        pub distinct: usize,
216        pub caches_cleared: u64,
217        pub lookups: u64,
218        pub hits: u64,
219    }
220
221    impl LayoutCounts {
222        const ZERO: Self = Self {
223            computed: 0,
224            distinct: 0,
225            caches_cleared: 0,
226            lookups: 0,
227            hits: 0,
228        };
229    }
230
231    thread_local! {
232        /// A copy of the most recent `take`, because the per-frame printer
233        /// takes them at the end of every resolve: without this, anything else
234        /// reading them always sees zero.
235        static LAST: Cell<LayoutCounts> = const { Cell::new(LayoutCounts::ZERO) };
236    }
237
238    /// The counts from the most recent `take`, without resetting anything.
239    #[must_use]
240    pub fn last() -> LayoutCounts {
241        LAST.with(Cell::get)
242    }
243
244    /// Counts since the last call, then reset.
245    pub fn take() -> LayoutCounts {
246        if !active() {
247            LAST.with(|last| last.set(LayoutCounts::ZERO));
248            return LayoutCounts::ZERO;
249        }
250        let counts = LayoutCounts {
251            computed: COMPUTED.with(|count| count.replace(0)),
252            distinct: DISTINCT.with(|seen| {
253                let mut seen = seen.borrow_mut();
254                let len = seen.len();
255                seen.clear();
256                len
257            }),
258            caches_cleared: CACHES_CLEARED.with(|count| count.replace(0)),
259            lookups: LOOKUPS.with(|count| count.replace(0)),
260            hits: HITS.with(|count| count.replace(0)),
261        };
262        ACTIVE.with(|active| active.set(false));
263        LAST.with(|last| last.set(counts));
264        counts
265    }
266}
267
268pub(crate) mod abspos;
269pub mod cache;
270pub(crate) mod construct;
271pub(crate) mod damage;
272pub(crate) mod inline;
273pub(crate) mod list;
274pub(crate) mod replaced;
275#[cfg(feature = "svg")]
276pub(crate) mod svg_geometry;
277pub(crate) mod table;
278
279use self::replaced::{ReplacedContext, is_replaced_element, replaced_measure_function};
280use self::table::TableTreeWrapper;
281
282pub(crate) fn resolve_calc_value(calc_ptr: *const (), parent_size: f32) -> f32 {
283    let calc = unsafe { &*(calc_ptr as *const CalcLengthPercentage) };
284    let result = calc.resolve(CSSPixelLength::new(parent_size));
285    result.px()
286}
287
288impl BaseDocument {
289    fn node_from_id(&self, node_id: taffy::prelude::NodeId) -> &Node {
290        &self.nodes[dom_node_id(node_id)]
291    }
292    fn node_from_id_mut(&mut self, node_id: taffy::prelude::NodeId) -> &mut Node {
293        &mut self.nodes[dom_node_id(node_id)]
294    }
295
296    /// One line naming an element well enough to find it in the source that
297    /// produced it: the tag, its `id`, its classes, and the sizes that were
298    /// being resolved when layout entered it. See [`layout_panic_probe`].
299    #[cfg(not(target_arch = "wasm32"))]
300    fn describe_node_for_panic(
301        &self,
302        node_id: blitz_traits::node_id::NodeId,
303        inputs: &taffy::LayoutInput,
304    ) -> String {
305        let Some(node) = self.nodes.get(node_id) else {
306            return format!("node {node_id} (gone)");
307        };
308        let Some(element) = node.data.downcast_element() else {
309            return format!("node {node_id} <{:?}>", node.data.kind());
310        };
311        let attr = |name: &str| -> Option<&str> {
312            element
313                .attrs
314                .iter()
315                .find(|a| a.name.local.as_ref() == name)
316                .map(|a| a.value.as_ref())
317        };
318        // Not the computed style's own width and height: `CompactLength`'s
319        // `Debug` is a tagged pointer, which reads as noise. What the resolve
320        // was actually given is what matters here anyway.
321        format!(
322            "node {node_id} <{}{}{}> known={:?}x{:?} avail={:?}x{:?} mode={:?}/{:?}",
323            element.name.local,
324            attr("id").map(|v| format!(" id={v}")).unwrap_or_default(),
325            attr("class")
326                .map(|v| format!(" class=\"{}\"", &v[..v.len().min(160)]))
327                .unwrap_or_default(),
328            inputs.known_dimensions.width,
329            inputs.known_dimensions.height,
330            inputs.available_space.width,
331            inputs.available_space.height,
332            inputs.run_mode,
333            inputs.axis,
334        )
335    }
336}
337
338/// The widest option label, in characters, and the number of visible rows, for
339/// a `<select>`. `None` for anything else.
340///
341/// A select has no in-flow content: `option { display: none }` in the user-agent
342/// sheet sees to that, and nothing replaces it. Without a measure of its own it
343/// laid out at zero and no site's country picker, currency picker or language
344/// picker had a box to press.
345///
346/// Still a character count rather than shaped text, because the options never
347/// reach layout and so are never shaped. What it counts is now the option's
348/// *label*, which is the string the control actually shows: a `label`
349/// attribute overrides the element's text, and the text is collapsed the way it
350/// would be rendered. Counting `text_content().trim()` measured the hidden text
351/// of a labelled option, and `trim()` only strips the ends, so a label whose own
352/// words were split across source lines counted the newline and the indentation
353/// before the next word and came out that much too wide.
354fn select_metrics_of(
355    doc: &BaseDocument,
356    node_id: blitz_traits::node_id::NodeId,
357) -> Option<(usize, f32)> {
358    let node = doc.nodes.get(node_id)?;
359    let element = node.data.downcast_element()?;
360    if element.name.local != local_name!("select") {
361        return None;
362    }
363
364    let widest = doc
365        .select_options(node_id)
366        .into_iter()
367        .map(|option_id| doc.option_label(option_id).chars().count())
368        .max()
369        .unwrap_or(0);
370
371    // A dropdown shows one row. `size` names the row count for a list box, and
372    // `multiple` without `size` shows four, which is what browsers settled on.
373    let rows = element
374        .attr(local_name!("size"))
375        .and_then(|size| size.parse::<f32>().ok())
376        .filter(|rows| *rows >= 1.0)
377        .unwrap_or(if element.attr(local_name!("multiple")).is_some() {
378            4.0
379        } else {
380            1.0
381        });
382
383    Some((widest, rows))
384}
385
386impl BaseDocument {
387    fn select_metrics(&self, node_id: blitz_traits::node_id::NodeId) -> Option<(usize, f32)> {
388        select_metrics_of(self, node_id)
389    }
390
391    fn compute_child_layout_internal(
392        &mut self,
393        node_id: NodeId,
394        inputs: taffy::tree::LayoutInput,
395        block_ctx: Option<&mut BlockContext<'_>>,
396    ) -> taffy::tree::LayoutOutput {
397        // Counted, not timed. The layout phase dominates a script-forced
398        // resolve, and the two explanations (a few nodes that are each slow, or
399        // the whole tree recomputing) call for opposite fixes. Only the blast
400        // radius separates them, and a cache hit never reaches this function.
401        #[cfg(feature = "log-phase-times")]
402        layout_counters::note_computed(dom_node_id(node_id));
403
404        if inputs.run_mode == taffy::RunMode::PerformLayout
405            && dom_node_id(node_id) == self.root_element().id
406        {
407            self.abspos_normal_layout = true;
408        }
409
410        // Read before the node is borrowed mutably: a `<select>` is sized from
411        // its options, which are other nodes.
412        let select_metrics = self.select_metrics(dom_node_id(node_id));
413
414        let node = &mut self.nodes[dom_node_id(node_id)];
415
416        let font_styles = node.primary_styles().map(|style| {
417            use style::values::computed::font::LineHeight;
418
419            let font_size = style.clone_font_size().used_size().px();
420            let line_height = match style.clone_line_height() {
421                LineHeight::Normal => font_size * 1.2,
422                LineHeight::Number(num) => font_size * num.0,
423                LineHeight::Length(value) => value.0.px(),
424            };
425
426            (font_size, line_height)
427        });
428        let font_size = font_styles.map(|s| s.0);
429        let resolved_line_height = font_styles.map(|s| s.1);
430
431        match &mut node.data {
432            NodeData::Text(data) => {
433                // With the new "inline context" architecture all text nodes should be wrapped in an "inline layout context"
434                // and should therefore never be measured individually.
435                #[cfg(feature = "tracing")]
436                tracing::error!(
437                    node_id = ?dom_node_id(node_id),
438                    data = ?data,
439                    "Tried to lay out text node individually",
440                );
441
442                #[cfg(not(feature = "tracing"))]
443                let _ = data;
444
445                taffy::LayoutOutput::HIDDEN
446                // unreachable!();
447
448                // compute_leaf_layout(inputs, &node.style, |known_dimensions, available_space| {
449                //     let context = TextContext {
450                //         text_content: &data.content.trim(),
451                //         writing_mode: WritingMode::Horizontal,
452                //     };
453                //     let font_metrics = FontMetrics {
454                //         char_width: 8.0,
455                //         char_height: 16.0,
456                //     };
457                //     text_measure_function(
458                //         known_dimensions,
459                //         available_space,
460                //         &context,
461                //         &font_metrics,
462                //     )
463                // })
464            }
465            NodeData::Element(element_data) | NodeData::AnonymousBlock(element_data) => {
466                // A `<select>` is measured from its options, which are not in
467                // flow. The character-count estimate is the same one the
468                // `cols` attribute of a textarea uses below: a select's label
469                // is not laid out as text anywhere yet, so there is no real
470                // measurement to take. An authored width or height still wins,
471                // this only supplies the content size.
472                if let Some((widest_label, rows)) = select_metrics {
473                    let advance = font_size.unwrap_or(16.0) * 0.6;
474                    let line_height = resolved_line_height.unwrap_or(16.0);
475                    return compute_leaf_layout(
476                        inputs,
477                        node.style(),
478                        resolve_calc_value,
479                        |_known_size, _available_space| taffy::Size {
480                            width: widest_label as f32 * advance,
481                            height: line_height * rows,
482                        },
483                    );
484                }
485
486                // TODO: deduplicate with single-line text input
487                if *element_data.name.local == *"textarea" {
488                    let rows = element_data
489                        .attr(local_name!("rows"))
490                        .and_then(|val| val.parse::<f32>().ok())
491                        .unwrap_or(2.0);
492
493                    let cols = element_data
494                        .attr(local_name!("cols"))
495                        .and_then(|val| val.parse::<f32>().ok());
496
497                    let intrinsic_height = resolved_line_height.unwrap_or(16.0) * rows;
498
499                    // Give the editor the width it has to lay out within, so a
500                    // long line wraps instead of running off the side. Without
501                    // this the editor is built with `set_width(None)` and never
502                    // told otherwise: `wrap="soft"` and `overflow-wrap` in the
503                    // stylesheet have nothing to act on, and typing past the
504                    // right edge walks the text out of the box and out of sight.
505                    //
506                    // The node's own `width` comes first. `known_dimensions` is
507                    // what the parent has decided so far and does not yet
508                    // include this element's style size, so reading only that
509                    // hands the editor the parent's width and it wraps, when it
510                    // wraps at all, to the wrong measure.
511                    let content_width = node
512                        .style()
513                        .size
514                        .width
515                        .maybe_resolve(inputs.parent_size.width, resolve_calc_value)
516                        .or(inputs.known_dimensions.width)
517                        .or(match inputs.available_space.width {
518                            taffy::AvailableSpace::Definite(width) => Some(width),
519                            _ => None,
520                        })
521                        .map(|width| {
522                            let inset = node
523                                .style()
524                                .padding
525                                .resolve_or_zero(inputs.parent_size, resolve_calc_value)
526                                .horizontal_components()
527                                .sum()
528                                + node
529                                    .style()
530                                    .border
531                                    .resolve_or_zero(inputs.parent_size, resolve_calc_value)
532                                    .horizontal_components()
533                                    .sum();
534                            (width - inset).max(0.0)
535                        });
536
537                    // The wrapped text may be taller than the box. That excess
538                    // is exactly what `scrollHeight` reports and what an
539                    // autosizing composer grows by, so it has to reach Taffy as
540                    // content size rather than be rounded away into the box
541                    // height.
542                    let mut content_height = intrinsic_height;
543                    if let Some(width) = content_width.filter(|width| *width > 0.0) {
544                        let font_ctx = self.font_ctx.clone();
545                        let layout_ctx = &mut self.layout_ctx;
546                        let node = &mut self.nodes[dom_node_id(node_id)];
547                        if let Some(input) = node
548                            .data
549                            .downcast_element_mut()
550                            .and_then(|el| el.text_input_data_mut())
551                        {
552                            input.sync_multiline_width(
553                                &mut font_ctx.lock().unwrap(),
554                                layout_ctx,
555                                width,
556                            );
557                            if let Some(layout) = input.editor.try_layout() {
558                                content_height = content_height.max(layout.height());
559                            }
560                        }
561                    }
562
563                    let node = &mut self.nodes[dom_node_id(node_id)];
564                    let mut output = compute_leaf_layout(
565                        inputs,
566                        node.style(),
567                        resolve_calc_value,
568                        |_known_size, _available_space| taffy::Size {
569                            width: cols
570                                .map(|cols| cols * font_size.unwrap_or(16.0) * 0.6)
571                                .unwrap_or(300.0),
572                            height: intrinsic_height,
573                        },
574                    );
575                    output.content_size.height = output.content_size.height.max(content_height);
576                    output.content_size.width = output.content_size.width.max(output.size.width);
577                    return output;
578                }
579
580                if *element_data.name.local == *"input" {
581                    match element_data.attr(local_name!("type")) {
582                        // if the input type is hidden, hide it
583                        Some("hidden") => {
584                            node.style_mut().display = Display::None;
585                            return taffy::LayoutOutput::HIDDEN;
586                        }
587                        Some("checkbox") => {
588                            return compute_leaf_layout(
589                                inputs,
590                                node.style(),
591                                resolve_calc_value,
592                                |_known_size, _available_space| {
593                                    let width = node.style().size.width.resolve_or_zero(
594                                        inputs.parent_size.width,
595                                        resolve_calc_value,
596                                    );
597                                    let height = node.style().size.height.resolve_or_zero(
598                                        inputs.parent_size.height,
599                                        resolve_calc_value,
600                                    );
601                                    let min_size = width.min(height);
602                                    taffy::Size {
603                                        width: min_size,
604                                        height: min_size,
605                                    }
606                                },
607                            );
608                        }
609                        // Kept in step with the list in
610                        // `layout::construct::collect_layout_children`, which
611                        // decides which inputs get a text editor. A type that
612                        // is on that list and not this one gets an editor and
613                        // no content box: `number` measured 6x6, its padding
614                        // and border alone, against 306x25.2 for every other
615                        // text-like type.
616                        None
617                        | Some(
618                            "text" | "password" | "email" | "number" | "tel" | "url" | "search",
619                        ) => {
620                            return compute_leaf_layout(
621                                inputs,
622                                node.style(),
623                                resolve_calc_value,
624                                |_known_size, _available_space| taffy::Size {
625                                    width: match inputs.available_space.width {
626                                        AvailableSpace::Definite(limit) => limit.min(300.0),
627                                        AvailableSpace::MinContent => 0.0,
628                                        AvailableSpace::MaxContent => 300.0,
629                                    },
630                                    height: resolved_line_height.unwrap_or(16.0),
631                                },
632                            );
633                        }
634                        _ => {}
635                    }
636                }
637
638                if is_replaced_element(&element_data.name.local) {
639                    // Get width and height attributes on image element
640                    //
641                    // TODO: smarter sizing using these (depending on object-fit, they shouldn't
642                    // necessarily just override the native size)
643                    //
644                    // Only the `svg` arm below writes to it.
645                    #[cfg_attr(not(feature = "svg"), allow(unused_mut))]
646                    let mut attr_size = taffy::Size {
647                        width: element_data
648                            .attr(local_name!("width"))
649                            .and_then(|val| val.parse::<f32>().ok()),
650                        height: element_data
651                            .attr(local_name!("height"))
652                            .and_then(|val| val.parse::<f32>().ok()),
653                    };
654
655                    // Get the element's intrinsic size and aspect ratio
656                    let (inherent_size, inherent_ratio) = match &element_data.special_data {
657                        SpecialElementData::Image(image_data) => match &**image_data {
658                            ImageData::Raster(image) => {
659                                let size = taffy::Size {
660                                    width: image.width as f32,
661                                    height: image.height as f32,
662                                };
663                                (size, Some(size.width / size.height))
664                            }
665                            #[cfg(feature = "svg")]
666                            ImageData::Svg(svg) => {
667                                // For an inline `<svg>` element the width/height attributes are
668                                // presentation attributes: percentages resolve against the
669                                // containing block. For SVG loaded as an image the intrinsic
670                                // dimensions are context-free.
671                                if *element_data.name.local == local_name!("svg") {
672                                    attr_size = taffy::Size {
673                                        width: svg.resolved_width(inputs.parent_size.width),
674                                        height: svg.resolved_height(inputs.parent_size.height),
675                                    };
676                                }
677                                let (mut width, mut height) = svg.intrinsic_size();
678                                // A replaced element with only an intrinsic aspect ratio uses the
679                                // stretch-fit width in normal flow (CSS2 ยง10.3.2): fill the
680                                // definite available width and derive the height from the ratio.
681                                // Shrink-to-fit contexts (floats, abspos) keep the default object
682                                // size that `intrinsic_size` already applied.
683                                if svg.intrinsic_width().is_none()
684                                    && svg.intrinsic_height().is_none()
685                                {
686                                    if let (
687                                        Some(ratio),
688                                        AvailableSpace::Definite(available_width),
689                                    ) =
690                                        (svg.viewbox_aspect_ratio(), inputs.available_space.width)
691                                    {
692                                        width = available_width;
693                                        height = available_width / ratio;
694                                    }
695                                }
696                                (taffy::Size { width, height }, Some(svg.aspect_ratio()))
697                            }
698                            ImageData::None => (taffy::Size::ZERO, None),
699                        },
700                        // Canvas has an intrinsic size and aspect ratio given by its
701                        // width/height attributes, defaulting to 300x150. Other replaced
702                        // elements without intrinsic dimensions (video, iframe, embed) use
703                        // the 300x150 default object size but have no intrinsic ratio.
704                        SpecialElementData::Canvas(_)
705                        | SpecialElementData::SubDocument(_)
706                        | SpecialElementData::None => {
707                            let tag_name = &element_data.name.local;
708                            if *tag_name == local_name!("img") || *tag_name == local_name!("svg") {
709                                (taffy::Size::ZERO, None)
710                            } else {
711                                let size = taffy::Size {
712                                    width: attr_size.width.unwrap_or(300.0),
713                                    height: attr_size.height.unwrap_or(150.0),
714                                };
715                                let ratio = (*tag_name == local_name!("canvas"))
716                                    .then(|| size.width / size.height);
717                                (size, ratio)
718                            }
719                        }
720                        _ => unreachable!(),
721                    };
722
723                    let replaced_context = ReplacedContext {
724                        inherent_size,
725                        attr_size,
726                        inherent_ratio,
727                    };
728
729                    let computed = replaced_measure_function(
730                        inputs.known_dimensions,
731                        inputs.parent_size,
732                        inputs.available_space,
733                        &replaced_context,
734                        node.style(),
735                        inputs.sizing_mode,
736                        inputs.axis,
737                    );
738
739                    return taffy::LayoutOutput {
740                        size: computed,
741                        content_size: computed,
742                        first_baselines: taffy::Point::NONE,
743                        top_margin: CollapsibleMarginSet::ZERO,
744                        bottom_margin: CollapsibleMarginSet::ZERO,
745                        margins_can_collapse_through: false,
746                    };
747                }
748
749                if node.flags.is_table_root() {
750                    let SpecialElementData::TableRoot(context) = &self.nodes[dom_node_id(node_id)]
751                        .data
752                        .downcast_element()
753                        .unwrap()
754                        .special_data
755                    else {
756                        panic!("Node marked as table root but doesn't have TableContext");
757                    };
758                    let context = Arc::clone(context);
759
760                    let mut table_wrapper = TableTreeWrapper {
761                        doc: self,
762                        ctx: context,
763                    };
764                    let mut output = compute_grid_layout(&mut table_wrapper, node_id, inputs);
765
766                    // HACK: Cap content size at node size to prevent scrolling
767                    output.content_size.width = output.content_size.width.min(output.size.width);
768                    output.content_size.height = output.content_size.height.min(output.size.height);
769
770                    return output;
771                }
772
773                if node.flags.is_inline_root() {
774                    return self.compute_inline_layout(dom_node_id(node_id), inputs, block_ctx);
775                }
776
777                // The default CSS file will set
778                match node.style().display {
779                    Display::Block => compute_block_layout(self, node_id, inputs, block_ctx),
780                    Display::FlowRoot => compute_block_layout(self, node_id, inputs, None),
781                    Display::Flex => compute_flexbox_layout(self, node_id, inputs),
782                    Display::Grid => compute_grid_layout(self, node_id, inputs),
783                    Display::None => taffy::LayoutOutput::HIDDEN,
784                }
785            }
786            NodeData::Document(_) => compute_block_layout(self, node_id, inputs, None),
787
788            _ => taffy::LayoutOutput::HIDDEN,
789        }
790    }
791}
792
793impl TraversePartialTree for BaseDocument {
794    type ChildIter<'a> = RefCellChildIter<'a>;
795
796    fn child_ids(&self, node_id: NodeId) -> Self::ChildIter<'_> {
797        let layout_children = self.node_from_id(node_id).layout_children.borrow(); //.unwrap().as_ref();
798        RefCellChildIter::new(Ref::map(layout_children, |children| {
799            children.as_ref().map(|c| c.as_slice()).unwrap_or(&[])
800        }))
801    }
802
803    fn child_count(&self, node_id: NodeId) -> usize {
804        self.node_from_id(node_id)
805            .layout_children
806            .borrow()
807            .as_ref()
808            .map(|c| c.len())
809            .unwrap_or(0)
810    }
811
812    fn get_child_id(&self, node_id: NodeId, index: usize) -> NodeId {
813        taffy_node_id(
814            self.node_from_id(node_id)
815                .layout_children
816                .borrow()
817                .as_ref()
818                .unwrap()[index],
819        )
820    }
821}
822impl TraverseTree for BaseDocument {}
823
824impl LayoutPartialTree for BaseDocument {
825    type CoreContainerStyle<'a>
826        = &'a taffy::Style<Atom>
827    where
828        Self: 'a;
829
830    type CustomIdent = Atom;
831
832    fn get_core_container_style(&self, node_id: NodeId) -> &Style<Atom> {
833        self.node_from_id(node_id).style()
834    }
835
836    fn set_unrounded_layout(&mut self, node_id: NodeId, layout: &Layout) {
837        let id = dom_node_id(node_id);
838        if self.abspos_candidate_ids.contains(&id) {
839            self.abspos_written.insert(id);
840        }
841        *self.node_from_id_mut(node_id).unrounded_layout_mut() = *layout;
842    }
843
844    fn resolve_calc_value(&self, calc_ptr: *const (), parent_size: f32) -> f32 {
845        resolve_calc_value(calc_ptr, parent_size)
846    }
847
848    #[inline(always)]
849    fn compute_child_layout(
850        &mut self,
851        node_id: NodeId,
852        inputs: taffy::LayoutInput,
853    ) -> taffy::LayoutOutput {
854        #[cfg(not(target_arch = "wasm32"))]
855        let probing = layout_panic_probe::enabled();
856        #[cfg(not(target_arch = "wasm32"))]
857        if probing {
858            layout_panic_probe::push(
859                dom_node_id(node_id),
860                self.describe_node_for_panic(dom_node_id(node_id), &inputs),
861            );
862        }
863
864        let output = compute_cached_layout(self, node_id, inputs, |tree, node_id, inputs| {
865            tree.compute_child_layout_internal(node_id, inputs, None)
866        });
867
868        // Only on the way out, so a panic leaves the stack standing for the
869        // hook to read. Nothing here runs after an abort.
870        #[cfg(not(target_arch = "wasm32"))]
871        if probing {
872            layout_panic_probe::pop();
873        }
874        output
875    }
876}
877
878impl taffy::CacheTree for BaseDocument {
879    #[inline]
880    fn cache_get(
881        &self,
882        node_id: NodeId,
883        inputs: &taffy::LayoutInput,
884    ) -> Option<taffy::LayoutOutput> {
885        let found = self.node_from_id(node_id).cache().get(inputs);
886        #[cfg(feature = "log-phase-times")]
887        layout_counters::note_lookup(found.is_some());
888        found
889    }
890
891    #[inline]
892    fn cache_store(
893        &mut self,
894        node_id: NodeId,
895        inputs: &taffy::LayoutInput,
896        layout_output: taffy::LayoutOutput,
897    ) {
898        let node = self.node_from_id_mut(node_id);
899        let independent_container = matches!(node.style().display, Display::Flex | Display::Grid)
900            && !node.flags.is_inline_root()
901            && !node.flags.is_table_root();
902        if independent_container {
903            node.cache_mut().store_intrinsic(inputs, layout_output);
904        } else {
905            node.cache_mut().store(inputs, layout_output);
906        }
907    }
908
909    #[inline]
910    fn cache_clear(&mut self, node_id: NodeId) {
911        // Release rather than empty in place. `clear()` would zero 1616 bytes
912        // and keep them; dropping the box hands the memory back, and a node
913        // that is invalidated and never re-measured stops paying for a cache
914        // it does not use. Re-measuring reallocates on the first store.
915        self.node_from_id_mut(node_id).cache_release();
916    }
917}
918
919impl taffy::LayoutBlockContainer for BaseDocument {
920    type BlockContainerStyle<'a>
921        = &'a Style<Atom>
922    where
923        Self: 'a;
924
925    type BlockItemStyle<'a>
926        = &'a Style<Atom>
927    where
928        Self: 'a;
929
930    fn get_block_container_style(&self, node_id: NodeId) -> Self::BlockContainerStyle<'_> {
931        self.get_core_container_style(node_id)
932    }
933
934    fn get_block_child_style(&self, child_node_id: NodeId) -> Self::BlockItemStyle<'_> {
935        self.get_core_container_style(child_node_id)
936    }
937
938    #[inline(always)]
939    fn compute_block_child_layout(
940        &mut self,
941        node_id: NodeId,
942        inputs: taffy::LayoutInput,
943        block_ctx: Option<&mut BlockContext<'_>>,
944    ) -> taffy::LayoutOutput {
945        compute_cached_layout(self, node_id, inputs, |tree, node_id, inputs| {
946            tree.compute_child_layout_internal(node_id, inputs, block_ctx)
947        })
948    }
949}
950
951impl taffy::LayoutFlexboxContainer for BaseDocument {
952    type FlexboxContainerStyle<'a>
953        = &'a Style<Atom>
954    where
955        Self: 'a;
956
957    type FlexboxItemStyle<'a>
958        = &'a Style<Atom>
959    where
960        Self: 'a;
961
962    fn get_flexbox_container_style(&self, node_id: NodeId) -> Self::FlexboxContainerStyle<'_> {
963        self.get_core_container_style(node_id)
964    }
965
966    fn get_flexbox_child_style(&self, child_node_id: NodeId) -> Self::FlexboxItemStyle<'_> {
967        self.get_core_container_style(child_node_id)
968    }
969}
970
971impl taffy::LayoutGridContainer for BaseDocument {
972    type GridContainerStyle<'a>
973        = &'a Style<Atom>
974    where
975        Self: 'a;
976
977    type GridItemStyle<'a>
978        = &'a Style<Atom>
979    where
980        Self: 'a;
981
982    fn get_grid_container_style(&self, node_id: NodeId) -> Self::GridContainerStyle<'_> {
983        self.get_core_container_style(node_id)
984    }
985
986    fn get_grid_child_style(&self, child_node_id: NodeId) -> Self::GridItemStyle<'_> {
987        self.get_core_container_style(child_node_id)
988    }
989
990    fn set_detailed_grid_info(
991        &mut self,
992        node_id: NodeId,
993        detailed_grid_info: taffy::DetailedGridInfo,
994    ) {
995        let node = self.node_from_id_mut(node_id);
996        if let Some(element) = node.element_data_mut() {
997            element.detailed_grid_info = Some(Box::new(detailed_grid_info));
998        }
999    }
1000}
1001
1002impl RoundTree for BaseDocument {
1003    fn get_unrounded_layout(&self, node_id: NodeId) -> Layout {
1004        *self.node_from_id(node_id).unrounded_layout()
1005    }
1006
1007    fn set_final_layout(&mut self, node_id: NodeId, layout: &Layout) {
1008        *self.node_from_id_mut(node_id).final_layout_mut() = *layout;
1009    }
1010}
1011
1012impl PrintTree for BaseDocument {
1013    fn get_debug_label(&self, node_id: NodeId) -> &'static str {
1014        let node = &self.node_from_id(node_id);
1015
1016        match node.data {
1017            NodeData::Document(_) => "DOCUMENT",
1018            // NodeData::Doctype { .. } => return "DOCTYPE",
1019            NodeData::Text { .. } => node.node_debug_str().leak(),
1020            NodeData::Comment { .. } => "COMMENT",
1021            NodeData::DocumentFragment => "FRAGMENT",
1022            NodeData::ShadowRoot(_) => "SHADOW ROOT",
1023            NodeData::AnonymousBlock(_) => "ANONYMOUS BLOCK",
1024            NodeData::Element(_) => {
1025                let style = node.style();
1026                let display = match style.display {
1027                    Display::Flex => match style.flex_direction {
1028                        FlexDirection::Row | FlexDirection::RowReverse => "FLEX ROW",
1029                        FlexDirection::Column | FlexDirection::ColumnReverse => "FLEX COL",
1030                    },
1031                    Display::Grid => "GRID",
1032                    Display::Block => "BLOCK",
1033                    Display::FlowRoot => "FLOW ROOT",
1034                    Display::None => "NONE",
1035                };
1036                format!("{} ({})", node.node_debug_str(), display).leak()
1037            } // NodeData::ProcessingInstruction { .. } => return "PROCESSING INSTRUCTION",
1038        }
1039    }
1040
1041    fn get_final_layout(&self, node_id: NodeId) -> Layout {
1042        *self.node_from_id(node_id).final_layout()
1043    }
1044}
1045
1046// pub struct ChildIter<'a>(std::slice::Iter<'a, usize>);
1047// impl<'a> Iterator for ChildIter<'a> {
1048//     type Item = NodeId;
1049//     fn next(&mut self) -> Option<Self::Item> {
1050//         self.0.next().copied().map(NodeId::from)
1051//     }
1052// }
1053
1054pub struct RefCellChildIter<'a> {
1055    items: Ref<'a, [crate::NodeId]>,
1056    idx: usize,
1057}
1058impl<'a> RefCellChildIter<'a> {
1059    fn new(items: Ref<'a, [crate::NodeId]>) -> RefCellChildIter<'a> {
1060        RefCellChildIter { items, idx: 0 }
1061    }
1062}
1063
1064impl Iterator for RefCellChildIter<'_> {
1065    type Item = NodeId;
1066    fn next(&mut self) -> Option<Self::Item> {
1067        self.items.get(self.idx).map(|id| {
1068            self.idx += 1;
1069            taffy_node_id(*id)
1070        })
1071    }
1072}