Skip to main content

blitz_dom/layout/
mod.rs

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