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 construct;
269pub(crate) mod damage;
270pub(crate) mod inline;
271pub(crate) mod list;
272pub(crate) mod replaced;
273pub(crate) mod table;
274
275use self::replaced::{ReplacedContext, is_replaced_element, replaced_measure_function};
276use self::table::TableTreeWrapper;
277
278pub(crate) fn resolve_calc_value(calc_ptr: *const (), parent_size: f32) -> f32 {
279    let calc = unsafe { &*(calc_ptr as *const CalcLengthPercentage) };
280    let result = calc.resolve(CSSPixelLength::new(parent_size));
281    result.px()
282}
283
284impl BaseDocument {
285    fn node_from_id(&self, node_id: taffy::prelude::NodeId) -> &Node {
286        &self.nodes[dom_node_id(node_id)]
287    }
288    fn node_from_id_mut(&mut self, node_id: taffy::prelude::NodeId) -> &mut Node {
289        &mut self.nodes[dom_node_id(node_id)]
290    }
291
292    /// One line naming an element well enough to find it in the source that
293    /// produced it: the tag, its `id`, its classes, and the sizes that were
294    /// being resolved when layout entered it. See [`layout_panic_probe`].
295    #[cfg(not(target_arch = "wasm32"))]
296    fn describe_node_for_panic(
297        &self,
298        node_id: blitz_traits::node_id::NodeId,
299        inputs: &taffy::LayoutInput,
300    ) -> String {
301        let Some(node) = self.nodes.get(node_id) else {
302            return format!("node {node_id} (gone)");
303        };
304        let Some(element) = node.data.downcast_element() else {
305            return format!("node {node_id} <{:?}>", node.data.kind());
306        };
307        let attr = |name: &str| -> Option<&str> {
308            element
309                .attrs
310                .iter()
311                .find(|a| a.name.local.as_ref() == name)
312                .map(|a| a.value.as_ref())
313        };
314        // Not the computed style's own width and height: `CompactLength`'s
315        // `Debug` is a tagged pointer, which reads as noise. What the resolve
316        // was actually given is what matters here anyway.
317        format!(
318            "node {node_id} <{}{}{}> known={:?}x{:?} avail={:?}x{:?} mode={:?}/{:?}",
319            element.name.local,
320            attr("id").map(|v| format!(" id={v}")).unwrap_or_default(),
321            attr("class")
322                .map(|v| format!(" class=\"{}\"", &v[..v.len().min(160)]))
323                .unwrap_or_default(),
324            inputs.known_dimensions.width,
325            inputs.known_dimensions.height,
326            inputs.available_space.width,
327            inputs.available_space.height,
328            inputs.run_mode,
329            inputs.axis,
330        )
331    }
332}
333
334/// The widest option label, in characters, and the number of visible rows, for
335/// a `<select>`. `None` for anything else.
336///
337/// A select has no in-flow content: `option { display: none }` in the user-agent
338/// sheet sees to that, and nothing replaces it. Without a measure of its own it
339/// laid out at zero and no site's country picker, currency picker or language
340/// picker had a box to press.
341///
342/// Still a character count rather than shaped text, because the options never
343/// reach layout and so are never shaped. What it counts is now the option's
344/// *label*, which is the string the control actually shows: a `label`
345/// attribute overrides the element's text, and the text is collapsed the way it
346/// would be rendered. Counting `text_content().trim()` measured the hidden text
347/// of a labelled option, and `trim()` only strips the ends, so a label whose own
348/// words were split across source lines counted the newline and the indentation
349/// before the next word and came out that much too wide.
350fn select_metrics_of(
351    doc: &BaseDocument,
352    node_id: blitz_traits::node_id::NodeId,
353) -> Option<(usize, f32)> {
354    let node = doc.nodes.get(node_id)?;
355    let element = node.data.downcast_element()?;
356    if element.name.local != local_name!("select") {
357        return None;
358    }
359
360    let widest = doc
361        .select_options(node_id)
362        .into_iter()
363        .map(|option_id| doc.option_label(option_id).chars().count())
364        .max()
365        .unwrap_or(0);
366
367    // A dropdown shows one row. `size` names the row count for a list box, and
368    // `multiple` without `size` shows four, which is what browsers settled on.
369    let rows = element
370        .attr(local_name!("size"))
371        .and_then(|size| size.parse::<f32>().ok())
372        .filter(|rows| *rows >= 1.0)
373        .unwrap_or(if element.attr(local_name!("multiple")).is_some() {
374            4.0
375        } else {
376            1.0
377        });
378
379    Some((widest, rows))
380}
381
382impl BaseDocument {
383    fn select_metrics(&self, node_id: blitz_traits::node_id::NodeId) -> Option<(usize, f32)> {
384        select_metrics_of(self, node_id)
385    }
386
387    fn compute_child_layout_internal(
388        &mut self,
389        node_id: NodeId,
390        inputs: taffy::tree::LayoutInput,
391        block_ctx: Option<&mut BlockContext<'_>>,
392    ) -> taffy::tree::LayoutOutput {
393        // Counted, not timed. The layout phase dominates a script-forced
394        // resolve, and the two explanations (a few nodes that are each slow, or
395        // the whole tree recomputing) call for opposite fixes. Only the blast
396        // radius separates them, and a cache hit never reaches this function.
397        #[cfg(feature = "log-phase-times")]
398        layout_counters::note_computed(dom_node_id(node_id));
399
400        // Read before the node is borrowed mutably: a `<select>` is sized from
401        // its options, which are other nodes.
402        let select_metrics = self.select_metrics(dom_node_id(node_id));
403
404        let node = &mut self.nodes[dom_node_id(node_id)];
405
406        let font_styles = node.primary_styles().map(|style| {
407            use style::values::computed::font::LineHeight;
408
409            let font_size = style.clone_font_size().used_size().px();
410            let line_height = match style.clone_line_height() {
411                LineHeight::Normal => font_size * 1.2,
412                LineHeight::Number(num) => font_size * num.0,
413                LineHeight::Length(value) => value.0.px(),
414            };
415
416            (font_size, line_height)
417        });
418        let font_size = font_styles.map(|s| s.0);
419        let resolved_line_height = font_styles.map(|s| s.1);
420
421        match &mut node.data {
422            NodeData::Text(data) => {
423                // With the new "inline context" architecture all text nodes should be wrapped in an "inline layout context"
424                // and should therefore never be measured individually.
425                #[cfg(feature = "tracing")]
426                tracing::error!(
427                    node_id = ?dom_node_id(node_id),
428                    data = ?data,
429                    "Tried to lay out text node individually",
430                );
431
432                #[cfg(not(feature = "tracing"))]
433                let _ = data;
434
435                taffy::LayoutOutput::HIDDEN
436                // unreachable!();
437
438                // compute_leaf_layout(inputs, &node.style, |known_dimensions, available_space| {
439                //     let context = TextContext {
440                //         text_content: &data.content.trim(),
441                //         writing_mode: WritingMode::Horizontal,
442                //     };
443                //     let font_metrics = FontMetrics {
444                //         char_width: 8.0,
445                //         char_height: 16.0,
446                //     };
447                //     text_measure_function(
448                //         known_dimensions,
449                //         available_space,
450                //         &context,
451                //         &font_metrics,
452                //     )
453                // })
454            }
455            NodeData::Element(element_data) | NodeData::AnonymousBlock(element_data) => {
456                // A `<select>` is measured from its options, which are not in
457                // flow. The character-count estimate is the same one the
458                // `cols` attribute of a textarea uses below: a select's label
459                // is not laid out as text anywhere yet, so there is no real
460                // measurement to take. An authored width or height still wins,
461                // this only supplies the content size.
462                if let Some((widest_label, rows)) = select_metrics {
463                    let advance = font_size.unwrap_or(16.0) * 0.6;
464                    let line_height = resolved_line_height.unwrap_or(16.0);
465                    return compute_leaf_layout(
466                        inputs,
467                        node.style(),
468                        resolve_calc_value,
469                        |_known_size, _available_space| taffy::Size {
470                            width: widest_label as f32 * advance,
471                            height: line_height * rows,
472                        },
473                    );
474                }
475
476                // TODO: deduplicate with single-line text input
477                if *element_data.name.local == *"textarea" {
478                    let rows = element_data
479                        .attr(local_name!("rows"))
480                        .and_then(|val| val.parse::<f32>().ok())
481                        .unwrap_or(2.0);
482
483                    let cols = element_data
484                        .attr(local_name!("cols"))
485                        .and_then(|val| val.parse::<f32>().ok());
486
487                    let intrinsic_height = resolved_line_height.unwrap_or(16.0) * rows;
488
489                    // Give the editor the width it has to lay out within, so a
490                    // long line wraps instead of running off the side. Without
491                    // this the editor is built with `set_width(None)` and never
492                    // told otherwise: `wrap="soft"` and `overflow-wrap` in the
493                    // stylesheet have nothing to act on, and typing past the
494                    // right edge walks the text out of the box and out of sight.
495                    //
496                    // The node's own `width` comes first. `known_dimensions` is
497                    // what the parent has decided so far and does not yet
498                    // include this element's style size, so reading only that
499                    // hands the editor the parent's width and it wraps, when it
500                    // wraps at all, to the wrong measure.
501                    let content_width = node
502                        .style()
503                        .size
504                        .width
505                        .maybe_resolve(inputs.parent_size.width, resolve_calc_value)
506                        .or(inputs.known_dimensions.width)
507                        .or(match inputs.available_space.width {
508                            taffy::AvailableSpace::Definite(width) => Some(width),
509                            _ => None,
510                        })
511                        .map(|width| {
512                            let inset = node
513                                .style()
514                                .padding
515                                .resolve_or_zero(inputs.parent_size, resolve_calc_value)
516                                .horizontal_components()
517                                .sum()
518                                + node
519                                    .style()
520                                    .border
521                                    .resolve_or_zero(inputs.parent_size, resolve_calc_value)
522                                    .horizontal_components()
523                                    .sum();
524                            (width - inset).max(0.0)
525                        });
526
527                    // The wrapped text may be taller than the box. That excess
528                    // is exactly what `scrollHeight` reports and what an
529                    // autosizing composer grows by, so it has to reach Taffy as
530                    // content size rather than be rounded away into the box
531                    // height.
532                    let mut content_height = intrinsic_height;
533                    if let Some(width) = content_width.filter(|width| *width > 0.0) {
534                        let font_ctx = self.font_ctx.clone();
535                        let layout_ctx = &mut self.layout_ctx;
536                        let node = &mut self.nodes[dom_node_id(node_id)];
537                        if let Some(input) = node
538                            .data
539                            .downcast_element_mut()
540                            .and_then(|el| el.text_input_data_mut())
541                        {
542                            input.sync_multiline_width(
543                                &mut font_ctx.lock().unwrap(),
544                                layout_ctx,
545                                width,
546                            );
547                            if let Some(layout) = input.editor.try_layout() {
548                                content_height = content_height.max(layout.height());
549                            }
550                        }
551                    }
552
553                    let node = &mut self.nodes[dom_node_id(node_id)];
554                    let mut output = compute_leaf_layout(
555                        inputs,
556                        node.style(),
557                        resolve_calc_value,
558                        |_known_size, _available_space| taffy::Size {
559                            width: cols
560                                .map(|cols| cols * font_size.unwrap_or(16.0) * 0.6)
561                                .unwrap_or(300.0),
562                            height: intrinsic_height,
563                        },
564                    );
565                    output.content_size.height = output.content_size.height.max(content_height);
566                    output.content_size.width = output.content_size.width.max(output.size.width);
567                    return output;
568                }
569
570                if *element_data.name.local == *"input" {
571                    match element_data.attr(local_name!("type")) {
572                        // if the input type is hidden, hide it
573                        Some("hidden") => {
574                            node.style_mut().display = Display::None;
575                            return taffy::LayoutOutput::HIDDEN;
576                        }
577                        Some("checkbox") => {
578                            return compute_leaf_layout(
579                                inputs,
580                                node.style(),
581                                resolve_calc_value,
582                                |_known_size, _available_space| {
583                                    let width = node.style().size.width.resolve_or_zero(
584                                        inputs.parent_size.width,
585                                        resolve_calc_value,
586                                    );
587                                    let height = node.style().size.height.resolve_or_zero(
588                                        inputs.parent_size.height,
589                                        resolve_calc_value,
590                                    );
591                                    let min_size = width.min(height);
592                                    taffy::Size {
593                                        width: min_size,
594                                        height: min_size,
595                                    }
596                                },
597                            );
598                        }
599                        // Kept in step with the list in
600                        // `layout::construct::collect_layout_children`, which
601                        // decides which inputs get a text editor. A type that
602                        // is on that list and not this one gets an editor and
603                        // no content box: `number` measured 6x6, its padding
604                        // and border alone, against 306x25.2 for every other
605                        // text-like type.
606                        None
607                        | Some(
608                            "text" | "password" | "email" | "number" | "tel" | "url" | "search",
609                        ) => {
610                            return compute_leaf_layout(
611                                inputs,
612                                node.style(),
613                                resolve_calc_value,
614                                |_known_size, _available_space| taffy::Size {
615                                    width: match inputs.available_space.width {
616                                        AvailableSpace::Definite(limit) => limit.min(300.0),
617                                        AvailableSpace::MinContent => 0.0,
618                                        AvailableSpace::MaxContent => 300.0,
619                                    },
620                                    height: resolved_line_height.unwrap_or(16.0),
621                                },
622                            );
623                        }
624                        _ => {}
625                    }
626                }
627
628                if is_replaced_element(&element_data.name.local) {
629                    // Get width and height attributes on image element
630                    //
631                    // TODO: smarter sizing using these (depending on object-fit, they shouldn't
632                    // necessarily just override the native size)
633                    //
634                    // Only the `svg` arm below writes to it.
635                    #[cfg_attr(not(feature = "svg"), allow(unused_mut))]
636                    let mut attr_size = taffy::Size {
637                        width: element_data
638                            .attr(local_name!("width"))
639                            .and_then(|val| val.parse::<f32>().ok()),
640                        height: element_data
641                            .attr(local_name!("height"))
642                            .and_then(|val| val.parse::<f32>().ok()),
643                    };
644
645                    // Get the element's intrinsic size and aspect ratio
646                    let (inherent_size, inherent_ratio) = match &element_data.special_data {
647                        SpecialElementData::Image(image_data) => match &**image_data {
648                            ImageData::Raster(image) => {
649                                let size = taffy::Size {
650                                    width: image.width as f32,
651                                    height: image.height as f32,
652                                };
653                                (size, Some(size.width / size.height))
654                            }
655                            #[cfg(feature = "svg")]
656                            ImageData::Svg(svg) => {
657                                // For an inline `<svg>` element the width/height attributes are
658                                // presentation attributes: percentages resolve against the
659                                // containing block. For SVG loaded as an image the intrinsic
660                                // dimensions are context-free.
661                                if *element_data.name.local == local_name!("svg") {
662                                    attr_size = taffy::Size {
663                                        width: svg.resolved_width(inputs.parent_size.width),
664                                        height: svg.resolved_height(inputs.parent_size.height),
665                                    };
666                                }
667                                let (mut width, mut height) = svg.intrinsic_size();
668                                // A replaced element with only an intrinsic aspect ratio uses the
669                                // stretch-fit width in normal flow (CSS2 ยง10.3.2): fill the
670                                // definite available width and derive the height from the ratio.
671                                // Shrink-to-fit contexts (floats, abspos) keep the default object
672                                // size that `intrinsic_size` already applied.
673                                if svg.intrinsic_width().is_none()
674                                    && svg.intrinsic_height().is_none()
675                                {
676                                    if let (
677                                        Some(ratio),
678                                        AvailableSpace::Definite(available_width),
679                                    ) =
680                                        (svg.viewbox_aspect_ratio(), inputs.available_space.width)
681                                    {
682                                        width = available_width;
683                                        height = available_width / ratio;
684                                    }
685                                }
686                                (taffy::Size { width, height }, Some(svg.aspect_ratio()))
687                            }
688                            ImageData::None => (taffy::Size::ZERO, None),
689                        },
690                        // Canvas has an intrinsic size and aspect ratio given by its
691                        // width/height attributes, defaulting to 300x150. Other replaced
692                        // elements without intrinsic dimensions (video, iframe, embed) use
693                        // the 300x150 default object size but have no intrinsic ratio.
694                        SpecialElementData::Canvas(_)
695                        | SpecialElementData::SubDocument(_)
696                        | SpecialElementData::None => {
697                            let tag_name = &element_data.name.local;
698                            if *tag_name == local_name!("img") || *tag_name == local_name!("svg") {
699                                (taffy::Size::ZERO, None)
700                            } else {
701                                let size = taffy::Size {
702                                    width: attr_size.width.unwrap_or(300.0),
703                                    height: attr_size.height.unwrap_or(150.0),
704                                };
705                                let ratio = (*tag_name == local_name!("canvas"))
706                                    .then(|| size.width / size.height);
707                                (size, ratio)
708                            }
709                        }
710                        _ => unreachable!(),
711                    };
712
713                    let replaced_context = ReplacedContext {
714                        inherent_size,
715                        attr_size,
716                        inherent_ratio,
717                    };
718
719                    let computed = replaced_measure_function(
720                        inputs.known_dimensions,
721                        inputs.parent_size,
722                        inputs.available_space,
723                        &replaced_context,
724                        node.style(),
725                        inputs.sizing_mode,
726                        inputs.axis,
727                    );
728
729                    return taffy::LayoutOutput {
730                        size: computed,
731                        content_size: computed,
732                        first_baselines: taffy::Point::NONE,
733                        top_margin: CollapsibleMarginSet::ZERO,
734                        bottom_margin: CollapsibleMarginSet::ZERO,
735                        margins_can_collapse_through: false,
736                    };
737                }
738
739                if node.flags.is_table_root() {
740                    let SpecialElementData::TableRoot(context) = &self.nodes[dom_node_id(node_id)]
741                        .data
742                        .downcast_element()
743                        .unwrap()
744                        .special_data
745                    else {
746                        panic!("Node marked as table root but doesn't have TableContext");
747                    };
748                    let context = Arc::clone(context);
749
750                    let mut table_wrapper = TableTreeWrapper {
751                        doc: self,
752                        ctx: context,
753                    };
754                    let mut output = compute_grid_layout(&mut table_wrapper, node_id, inputs);
755
756                    // HACK: Cap content size at node size to prevent scrolling
757                    output.content_size.width = output.content_size.width.min(output.size.width);
758                    output.content_size.height = output.content_size.height.min(output.size.height);
759
760                    return output;
761                }
762
763                if node.flags.is_inline_root() {
764                    return self.compute_inline_layout(dom_node_id(node_id), inputs, block_ctx);
765                }
766
767                // The default CSS file will set
768                match node.style().display {
769                    Display::Block => compute_block_layout(self, node_id, inputs, block_ctx),
770                    Display::FlowRoot => compute_block_layout(self, node_id, inputs, None),
771                    Display::Flex => compute_flexbox_layout(self, node_id, inputs),
772                    Display::Grid => compute_grid_layout(self, node_id, inputs),
773                    Display::None => taffy::LayoutOutput::HIDDEN,
774                }
775            }
776            NodeData::Document(_) => compute_block_layout(self, node_id, inputs, None),
777
778            _ => taffy::LayoutOutput::HIDDEN,
779        }
780    }
781}
782
783impl TraversePartialTree for BaseDocument {
784    type ChildIter<'a> = RefCellChildIter<'a>;
785
786    fn child_ids(&self, node_id: NodeId) -> Self::ChildIter<'_> {
787        let layout_children = self.node_from_id(node_id).layout_children.borrow(); //.unwrap().as_ref();
788        RefCellChildIter::new(Ref::map(layout_children, |children| {
789            children.as_ref().map(|c| c.as_slice()).unwrap_or(&[])
790        }))
791    }
792
793    fn child_count(&self, node_id: NodeId) -> usize {
794        self.node_from_id(node_id)
795            .layout_children
796            .borrow()
797            .as_ref()
798            .map(|c| c.len())
799            .unwrap_or(0)
800    }
801
802    fn get_child_id(&self, node_id: NodeId, index: usize) -> NodeId {
803        taffy_node_id(
804            self.node_from_id(node_id)
805                .layout_children
806                .borrow()
807                .as_ref()
808                .unwrap()[index],
809        )
810    }
811}
812impl TraverseTree for BaseDocument {}
813
814impl LayoutPartialTree for BaseDocument {
815    type CoreContainerStyle<'a>
816        = &'a taffy::Style<Atom>
817    where
818        Self: 'a;
819
820    type CustomIdent = Atom;
821
822    fn get_core_container_style(&self, node_id: NodeId) -> &Style<Atom> {
823        self.node_from_id(node_id).style()
824    }
825
826    fn set_unrounded_layout(&mut self, node_id: NodeId, layout: &Layout) {
827        *self.node_from_id_mut(node_id).unrounded_layout_mut() = *layout;
828    }
829
830    fn resolve_calc_value(&self, calc_ptr: *const (), parent_size: f32) -> f32 {
831        resolve_calc_value(calc_ptr, parent_size)
832    }
833
834    #[inline(always)]
835    fn compute_child_layout(
836        &mut self,
837        node_id: NodeId,
838        inputs: taffy::LayoutInput,
839    ) -> taffy::LayoutOutput {
840        #[cfg(not(target_arch = "wasm32"))]
841        let probing = layout_panic_probe::enabled();
842        #[cfg(not(target_arch = "wasm32"))]
843        if probing {
844            layout_panic_probe::push(
845                dom_node_id(node_id),
846                self.describe_node_for_panic(dom_node_id(node_id), &inputs),
847            );
848        }
849
850        let output = compute_cached_layout(self, node_id, inputs, |tree, node_id, inputs| {
851            tree.compute_child_layout_internal(node_id, inputs, None)
852        });
853
854        // Only on the way out, so a panic leaves the stack standing for the
855        // hook to read. Nothing here runs after an abort.
856        #[cfg(not(target_arch = "wasm32"))]
857        if probing {
858            layout_panic_probe::pop();
859        }
860        output
861    }
862}
863
864impl taffy::CacheTree for BaseDocument {
865    #[inline]
866    fn cache_get(
867        &self,
868        node_id: NodeId,
869        inputs: &taffy::LayoutInput,
870    ) -> Option<taffy::LayoutOutput> {
871        let found = self.node_from_id(node_id).cache().get(inputs);
872        #[cfg(feature = "log-phase-times")]
873        layout_counters::note_lookup(found.is_some());
874        found
875    }
876
877    #[inline]
878    fn cache_store(
879        &mut self,
880        node_id: NodeId,
881        inputs: &taffy::LayoutInput,
882        layout_output: taffy::LayoutOutput,
883    ) {
884        self.node_from_id_mut(node_id)
885            .cache_mut()
886            .store(inputs, layout_output);
887    }
888
889    #[inline]
890    fn cache_clear(&mut self, node_id: NodeId) {
891        // Release rather than empty in place. `clear()` would zero 1616 bytes
892        // and keep them; dropping the box hands the memory back, and a node
893        // that is invalidated and never re-measured stops paying for a cache
894        // it does not use. Re-measuring reallocates on the first store.
895        self.node_from_id_mut(node_id).cache_release();
896    }
897}
898
899impl taffy::LayoutBlockContainer for BaseDocument {
900    type BlockContainerStyle<'a>
901        = &'a Style<Atom>
902    where
903        Self: 'a;
904
905    type BlockItemStyle<'a>
906        = &'a Style<Atom>
907    where
908        Self: 'a;
909
910    fn get_block_container_style(&self, node_id: NodeId) -> Self::BlockContainerStyle<'_> {
911        self.get_core_container_style(node_id)
912    }
913
914    fn get_block_child_style(&self, child_node_id: NodeId) -> Self::BlockItemStyle<'_> {
915        self.get_core_container_style(child_node_id)
916    }
917
918    #[inline(always)]
919    fn compute_block_child_layout(
920        &mut self,
921        node_id: NodeId,
922        inputs: taffy::LayoutInput,
923        block_ctx: Option<&mut BlockContext<'_>>,
924    ) -> taffy::LayoutOutput {
925        compute_cached_layout(self, node_id, inputs, |tree, node_id, inputs| {
926            tree.compute_child_layout_internal(node_id, inputs, block_ctx)
927        })
928    }
929}
930
931impl taffy::LayoutFlexboxContainer for BaseDocument {
932    type FlexboxContainerStyle<'a>
933        = &'a Style<Atom>
934    where
935        Self: 'a;
936
937    type FlexboxItemStyle<'a>
938        = &'a Style<Atom>
939    where
940        Self: 'a;
941
942    fn get_flexbox_container_style(&self, node_id: NodeId) -> Self::FlexboxContainerStyle<'_> {
943        self.get_core_container_style(node_id)
944    }
945
946    fn get_flexbox_child_style(&self, child_node_id: NodeId) -> Self::FlexboxItemStyle<'_> {
947        self.get_core_container_style(child_node_id)
948    }
949}
950
951impl taffy::LayoutGridContainer for BaseDocument {
952    type GridContainerStyle<'a>
953        = &'a Style<Atom>
954    where
955        Self: 'a;
956
957    type GridItemStyle<'a>
958        = &'a Style<Atom>
959    where
960        Self: 'a;
961
962    fn get_grid_container_style(&self, node_id: NodeId) -> Self::GridContainerStyle<'_> {
963        self.get_core_container_style(node_id)
964    }
965
966    fn get_grid_child_style(&self, child_node_id: NodeId) -> Self::GridItemStyle<'_> {
967        self.get_core_container_style(child_node_id)
968    }
969
970    fn set_detailed_grid_info(
971        &mut self,
972        node_id: NodeId,
973        detailed_grid_info: taffy::DetailedGridInfo,
974    ) {
975        let node = self.node_from_id_mut(node_id);
976        if let Some(element) = node.element_data_mut() {
977            element.detailed_grid_info = Some(Box::new(detailed_grid_info));
978        }
979    }
980}
981
982impl RoundTree for BaseDocument {
983    fn get_unrounded_layout(&self, node_id: NodeId) -> Layout {
984        *self.node_from_id(node_id).unrounded_layout()
985    }
986
987    fn set_final_layout(&mut self, node_id: NodeId, layout: &Layout) {
988        *self.node_from_id_mut(node_id).final_layout_mut() = *layout;
989    }
990}
991
992impl PrintTree for BaseDocument {
993    fn get_debug_label(&self, node_id: NodeId) -> &'static str {
994        let node = &self.node_from_id(node_id);
995
996        match node.data {
997            NodeData::Document(_) => "DOCUMENT",
998            // NodeData::Doctype { .. } => return "DOCTYPE",
999            NodeData::Text { .. } => node.node_debug_str().leak(),
1000            NodeData::Comment { .. } => "COMMENT",
1001            NodeData::DocumentFragment => "FRAGMENT",
1002            NodeData::ShadowRoot(_) => "SHADOW ROOT",
1003            NodeData::AnonymousBlock(_) => "ANONYMOUS BLOCK",
1004            NodeData::Element(_) => {
1005                let style = node.style();
1006                let display = match style.display {
1007                    Display::Flex => match style.flex_direction {
1008                        FlexDirection::Row | FlexDirection::RowReverse => "FLEX ROW",
1009                        FlexDirection::Column | FlexDirection::ColumnReverse => "FLEX COL",
1010                    },
1011                    Display::Grid => "GRID",
1012                    Display::Block => "BLOCK",
1013                    Display::FlowRoot => "FLOW ROOT",
1014                    Display::None => "NONE",
1015                };
1016                format!("{} ({})", node.node_debug_str(), display).leak()
1017            } // NodeData::ProcessingInstruction { .. } => return "PROCESSING INSTRUCTION",
1018        }
1019    }
1020
1021    fn get_final_layout(&self, node_id: NodeId) -> Layout {
1022        *self.node_from_id(node_id).final_layout()
1023    }
1024}
1025
1026// pub struct ChildIter<'a>(std::slice::Iter<'a, usize>);
1027// impl<'a> Iterator for ChildIter<'a> {
1028//     type Item = NodeId;
1029//     fn next(&mut self) -> Option<Self::Item> {
1030//         self.0.next().copied().map(NodeId::from)
1031//     }
1032// }
1033
1034pub struct RefCellChildIter<'a> {
1035    items: Ref<'a, [crate::NodeId]>,
1036    idx: usize,
1037}
1038impl<'a> RefCellChildIter<'a> {
1039    fn new(items: Ref<'a, [crate::NodeId]>) -> RefCellChildIter<'a> {
1040        RefCellChildIter { items, idx: 0 }
1041    }
1042}
1043
1044impl Iterator for RefCellChildIter<'_> {
1045    type Item = NodeId;
1046    fn next(&mut self) -> Option<Self::Item> {
1047        self.items.get(self.idx).map(|id| {
1048            self.idx += 1;
1049            taffy_node_id(*id)
1050        })
1051    }
1052}