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