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