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::{LocalName, 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, FlexDirection, LayoutPartialTree, NodeId, ResolveOrZero, RoundTree, Style,
17    TraversePartialTree, TraverseTree, compute_block_layout, compute_cached_layout,
18    compute_flexbox_layout, compute_grid_layout, compute_leaf_layout, prelude::*,
19};
20
21pub(crate) mod construct;
22pub(crate) mod damage;
23pub(crate) mod inline;
24pub(crate) mod list;
25pub(crate) mod replaced;
26pub(crate) mod table;
27
28use self::replaced::{
29    IntrinsicSizes, ReplacedContext, compute_replaced_layout, is_replaced_element,
30};
31use self::table::TableTreeWrapper;
32
33/// The default object size for replaced elements
34/// (https://drafts.csswg.org/css-images/#default-object-size).
35const DEFAULT_OBJECT_SIZE: taffy::Size<f32> = taffy::Size {
36    width: 300.0,
37    height: 150.0,
38};
39
40/// The intrinsic dimensions and default object size for a replaced element
41/// whose intrinsic dimensions are determined by its tag: an image with no
42/// loaded resource has no intrinsic dimensions and a zero default object size;
43/// a canvas has an intrinsic size and aspect ratio given by its width/height
44/// attributes (defaulting to 300x150); other replaced elements (video, iframe,
45/// embed) have no intrinsic dimensions and the 300x150 default object size.
46fn tag_intrinsic_sizes(
47    tag_name: &LocalName,
48    attr_size: taffy::Size<Option<f32>>,
49) -> (IntrinsicSizes, taffy::Size<f32>) {
50    if *tag_name == local_name!("img") || *tag_name == local_name!("svg") {
51        return (IntrinsicSizes::default(), taffy::Size::ZERO);
52    }
53    if *tag_name == local_name!("canvas") {
54        let width = attr_size.width.unwrap_or(300.0);
55        let height = attr_size.height.unwrap_or(150.0);
56        return (
57            IntrinsicSizes {
58                width: Some(width),
59                height: Some(height),
60                ratio: Some(width / height),
61            },
62            DEFAULT_OBJECT_SIZE,
63        );
64    }
65    (IntrinsicSizes::default(), DEFAULT_OBJECT_SIZE)
66}
67
68pub(crate) fn resolve_calc_value(calc_ptr: *const (), parent_size: f32) -> f32 {
69    let calc = unsafe { &*(calc_ptr as *const CalcLengthPercentage) };
70    let result = calc.resolve(CSSPixelLength::new(parent_size));
71    result.px()
72}
73
74impl BaseDocument {
75    fn node_from_id(&self, node_id: taffy::prelude::NodeId) -> &Node {
76        &self.nodes[dom_node_id(node_id)]
77    }
78    fn node_from_id_mut(&mut self, node_id: taffy::prelude::NodeId) -> &mut Node {
79        &mut self.nodes[dom_node_id(node_id)]
80    }
81}
82
83impl BaseDocument {
84    fn compute_child_layout_internal(
85        &mut self,
86        node_id: NodeId,
87        inputs: taffy::tree::LayoutInput,
88        block_ctx: Option<&mut BlockContext<'_>>,
89    ) -> taffy::tree::LayoutOutput {
90        let node = &mut self.nodes[dom_node_id(node_id)];
91
92        let font_styles = node.primary_styles().map(|style| {
93            use style::values::computed::font::LineHeight;
94
95            let font_size = style.clone_font_size().used_size().px();
96            let line_height = match style.clone_line_height() {
97                LineHeight::Normal => font_size * 1.2,
98                LineHeight::Number(num) => font_size * num.0,
99                LineHeight::Length(value) => value.0.px(),
100            };
101
102            (font_size, line_height)
103        });
104        let font_size = font_styles.map(|s| s.0);
105        let resolved_line_height = font_styles.map(|s| s.1);
106
107        match &mut node.data {
108            NodeData::Text(data) => {
109                // With the new "inline context" architecture all text nodes should be wrapped in an "inline layout context"
110                // and should therefore never be measured individually.
111                #[cfg(feature = "tracing")]
112                tracing::error!(
113                    node_id = ?dom_node_id(node_id),
114                    data = ?data,
115                    "Tried to lay out text node individually",
116                );
117
118                #[cfg(not(feature = "tracing"))]
119                let _ = data;
120
121                taffy::LayoutOutput::HIDDEN
122                // unreachable!();
123
124                // compute_leaf_layout(inputs, &node.style, |known_dimensions, available_space| {
125                //     let context = TextContext {
126                //         text_content: &data.content.trim(),
127                //         writing_mode: WritingMode::Horizontal,
128                //     };
129                //     let font_metrics = FontMetrics {
130                //         char_width: 8.0,
131                //         char_height: 16.0,
132                //     };
133                //     text_measure_function(
134                //         known_dimensions,
135                //         available_space,
136                //         &context,
137                //         &font_metrics,
138                //     )
139                // })
140            }
141            NodeData::Element(element_data) | NodeData::AnonymousBlock(element_data) => {
142                // TODO: deduplicate with single-line text input
143                if *element_data.name.local == *"textarea" {
144                    let rows = element_data
145                        .attr(local_name!("rows"))
146                        .and_then(|val| val.parse::<f32>().ok())
147                        .unwrap_or(2.0);
148
149                    let cols = element_data
150                        .attr(local_name!("cols"))
151                        .and_then(|val| val.parse::<f32>().ok());
152
153                    return compute_leaf_layout(
154                        inputs,
155                        node.style(),
156                        resolve_calc_value,
157                        |_known_size, _available_space| taffy::Size {
158                            width: cols
159                                .map(|cols| cols * font_size.unwrap_or(16.0) * 0.6)
160                                .unwrap_or(300.0),
161                            height: resolved_line_height.unwrap_or(16.0) * rows,
162                        },
163                    );
164                }
165
166                if *element_data.name.local == *"input" {
167                    match element_data.attr(local_name!("type")) {
168                        // if the input type is hidden, hide it
169                        Some("hidden") => {
170                            node.style_mut().display = Display::None;
171                            return taffy::LayoutOutput::HIDDEN;
172                        }
173                        Some("checkbox") => {
174                            return compute_leaf_layout(
175                                inputs,
176                                node.style(),
177                                resolve_calc_value,
178                                |_known_size, _available_space| {
179                                    let width = node.style().size.width.resolve_or_zero(
180                                        inputs.parent_size.width,
181                                        resolve_calc_value,
182                                    );
183                                    let height = node.style().size.height.resolve_or_zero(
184                                        inputs.parent_size.height,
185                                        resolve_calc_value,
186                                    );
187                                    let min_size = width.min(height);
188                                    taffy::Size {
189                                        width: min_size,
190                                        height: min_size,
191                                    }
192                                },
193                            );
194                        }
195                        None | Some("text" | "password" | "email" | "tel" | "url" | "search") => {
196                            return compute_leaf_layout(
197                                inputs,
198                                node.style(),
199                                resolve_calc_value,
200                                |_known_size, _available_space| taffy::Size {
201                                    width: match inputs.available_space.width {
202                                        AvailableSpace::Definite(limit) => limit.min(300.0),
203                                        AvailableSpace::MinContent => 0.0,
204                                        AvailableSpace::MaxContent => 300.0,
205                                    },
206                                    height: resolved_line_height.unwrap_or(16.0),
207                                },
208                            );
209                        }
210                        _ => {}
211                    }
212                }
213
214                if is_replaced_element(&element_data.name.local) {
215                    // Width/height attributes are presentational hints mapped to the CSS
216                    // width/height properties by
217                    // `synthesize_presentational_hints_for_legacy_attributes`, so they
218                    // are already part of the style. They are only read here for the
219                    // elements whose attributes determine their *intrinsic* size
220                    // (canvas, and custom widgets on canvas tags).
221                    let attr_size = taffy::Size {
222                        width: element_data
223                            .attr(local_name!("width"))
224                            .and_then(|val| val.parse::<f32>().ok()),
225                        height: element_data
226                            .attr(local_name!("height"))
227                            .and_then(|val| val.parse::<f32>().ok()),
228                    };
229
230                    // Get the element's intrinsic dimensions and default object size
231                    let (intrinsic_sizes, default_object_size) = match &element_data.special_data {
232                        SpecialElementData::Image(image_data) => match &**image_data {
233                            ImageData::Raster(image) => {
234                                let (width, height) = (image.width as f32, image.height as f32);
235                                (
236                                    IntrinsicSizes {
237                                        width: Some(width),
238                                        height: Some(height),
239                                        ratio: Some(width / height),
240                                    },
241                                    DEFAULT_OBJECT_SIZE,
242                                )
243                            }
244                            #[cfg(feature = "svg")]
245                            ImageData::Svg(svg) => {
246                                let mut width = svg.intrinsic_width();
247                                let mut height = svg.intrinsic_height();
248                                // An SVG with no declared dimensions and no viewBox has no
249                                // intrinsic dimensions per CSS, but usvg still resolves a
250                                // concrete size; use it in place of the default object size.
251                                if width.is_none()
252                                    && height.is_none()
253                                    && svg.viewbox_aspect_ratio().is_none()
254                                {
255                                    let size = svg.tree.size();
256                                    width = Some(size.width());
257                                    height = Some(size.height());
258                                }
259                                (
260                                    IntrinsicSizes {
261                                        width,
262                                        height,
263                                        ratio: Some(svg.aspect_ratio()),
264                                    },
265                                    DEFAULT_OBJECT_SIZE,
266                                )
267                            }
268                            ImageData::None => (IntrinsicSizes::default(), taffy::Size::ZERO),
269                        },
270                        SpecialElementData::Canvas(_)
271                        | SpecialElementData::SubDocument(_)
272                        | SpecialElementData::None => {
273                            tag_intrinsic_sizes(&element_data.name.local, attr_size)
274                        }
275                        #[cfg(feature = "custom-widget")]
276                        SpecialElementData::CustomWidget(widget_data) => {
277                            let (fallback, default_object_size) =
278                                tag_intrinsic_sizes(&element_data.name.local, attr_size);
279                            // A canvas's content attributes determine its intrinsic size,
280                            // overriding the widget-reported one; the widget-reported size
281                            // in turn overrides the tag's fallback.
282                            let attr_intrinsic =
283                                if *element_data.name.local == local_name!("canvas") {
284                                    attr_size
285                                } else {
286                                    taffy::Size::NONE
287                                };
288                            let attr_ratio = match (attr_intrinsic.width, attr_intrinsic.height) {
289                                (Some(w), Some(h)) => Some(w / h),
290                                _ => None,
291                            };
292                            let widget_sizes = widget_data.widget.intrinsic_sizes();
293                            (
294                                IntrinsicSizes {
295                                    width: attr_intrinsic
296                                        .width
297                                        .or(widget_sizes.width)
298                                        .or(fallback.width),
299                                    height: attr_intrinsic
300                                        .height
301                                        .or(widget_sizes.height)
302                                        .or(fallback.height),
303                                    ratio: attr_ratio.or(widget_sizes.ratio).or(fallback.ratio),
304                                },
305                                default_object_size,
306                            )
307                        }
308                        _ => unreachable!(),
309                    };
310
311                    let replaced_context = ReplacedContext {
312                        intrinsic_sizes,
313                        default_object_size,
314                    };
315
316                    return compute_replaced_layout(
317                        inputs,
318                        node.style(),
319                        resolve_calc_value,
320                        &replaced_context,
321                    );
322                }
323
324                if node.flags.is_table_root() {
325                    let SpecialElementData::TableRoot(context) = &self.nodes[dom_node_id(node_id)]
326                        .data
327                        .downcast_element()
328                        .unwrap()
329                        .special_data
330                    else {
331                        panic!("Node marked as table root but doesn't have TableContext");
332                    };
333                    let context = Arc::clone(context);
334
335                    let mut table_wrapper = TableTreeWrapper {
336                        doc: self,
337                        ctx: context,
338                    };
339                    let mut output = compute_grid_layout(&mut table_wrapper, node_id, inputs);
340
341                    // HACK: Cap scrollable overflow at node size to prevent scrolling
342                    output.scrollable_overflow_rect.left = 0.0;
343                    output.scrollable_overflow_rect.top = 0.0;
344                    output.scrollable_overflow_rect.right =
345                        output.scrollable_overflow_rect.right.min(output.size.width);
346                    output.scrollable_overflow_rect.bottom = output
347                        .scrollable_overflow_rect
348                        .bottom
349                        .min(output.size.height);
350
351                    return output;
352                }
353
354                if node.flags.is_inline_root() {
355                    return self.compute_inline_layout(dom_node_id(node_id), inputs, block_ctx);
356                }
357
358                // The default CSS file will set
359                match node.style().display {
360                    Display::Block => compute_block_layout(self, node_id, inputs, block_ctx),
361                    Display::FlowRoot => compute_block_layout(self, node_id, inputs, None),
362                    Display::Flex => compute_flexbox_layout(self, node_id, inputs),
363                    Display::Grid => compute_grid_layout(self, node_id, inputs),
364                    Display::None => taffy::LayoutOutput::HIDDEN,
365                }
366            }
367            NodeData::Document(_) => compute_block_layout(self, node_id, inputs, None),
368
369            _ => taffy::LayoutOutput::HIDDEN,
370        }
371    }
372}
373
374impl TraversePartialTree for BaseDocument {
375    type ChildIter<'a> = RefCellChildIter<'a>;
376
377    fn child_ids(&self, node_id: NodeId) -> Self::ChildIter<'_> {
378        let layout_children = self.node_from_id(node_id).layout_children.borrow(); //.unwrap().as_ref();
379        RefCellChildIter::new(Ref::map(layout_children, |children| {
380            children.as_ref().map(|c| c.as_slice()).unwrap_or(&[])
381        }))
382    }
383
384    fn child_count(&self, node_id: NodeId) -> usize {
385        self.node_from_id(node_id)
386            .layout_children
387            .borrow()
388            .as_ref()
389            .map(|c| c.len())
390            .unwrap_or(0)
391    }
392
393    fn get_child_id(&self, node_id: NodeId, index: usize) -> NodeId {
394        taffy_node_id(
395            self.node_from_id(node_id)
396                .layout_children
397                .borrow()
398                .as_ref()
399                .unwrap()[index],
400        )
401    }
402}
403impl TraverseTree for BaseDocument {}
404
405impl LayoutPartialTree for BaseDocument {
406    type CoreContainerStyle<'a>
407        = &'a taffy::Style<Atom>
408    where
409        Self: 'a;
410
411    type CustomIdent = Atom;
412
413    fn get_core_container_style(&self, node_id: NodeId) -> &Style<Atom> {
414        self.node_from_id(node_id).style()
415    }
416
417    fn set_unrounded_layout(&mut self, node_id: NodeId, layout: &Layout) {
418        *self.node_from_id_mut(node_id).unrounded_layout_mut() = *layout;
419    }
420
421    fn resolve_calc_value(&self, calc_ptr: *const (), parent_size: f32) -> f32 {
422        resolve_calc_value(calc_ptr, parent_size)
423    }
424
425    #[inline(always)]
426    fn compute_child_layout(
427        &mut self,
428        node_id: NodeId,
429        inputs: taffy::LayoutInput,
430    ) -> taffy::LayoutOutput {
431        compute_cached_layout(self, node_id, inputs, |tree, node_id, inputs| {
432            tree.compute_child_layout_internal(node_id, inputs, None)
433        })
434    }
435}
436
437impl taffy::CacheTree for BaseDocument {
438    #[inline]
439    fn cache_get(
440        &mut self,
441        node_id: NodeId,
442        inputs: &taffy::LayoutInput,
443    ) -> Option<taffy::LayoutOutput> {
444        self.node_from_id_mut(node_id).cache_mut().get(inputs)
445    }
446
447    #[inline]
448    fn cache_store(
449        &mut self,
450        node_id: NodeId,
451        inputs: &taffy::LayoutInput,
452        layout_output: taffy::LayoutOutput,
453    ) {
454        self.node_from_id_mut(node_id)
455            .cache_mut()
456            .store(inputs, layout_output);
457    }
458
459    #[inline]
460    fn cache_clear(&mut self, node_id: NodeId) {
461        self.node_from_id_mut(node_id).cache_mut().clear();
462    }
463}
464
465impl taffy::LayoutBlockContainer for BaseDocument {
466    type BlockContainerStyle<'a>
467        = &'a Style<Atom>
468    where
469        Self: 'a;
470
471    type BlockItemStyle<'a>
472        = &'a Style<Atom>
473    where
474        Self: 'a;
475
476    fn get_block_container_style(&self, node_id: NodeId) -> Self::BlockContainerStyle<'_> {
477        self.get_core_container_style(node_id)
478    }
479
480    fn get_block_child_style(&self, child_node_id: NodeId) -> Self::BlockItemStyle<'_> {
481        self.get_core_container_style(child_node_id)
482    }
483
484    #[inline(always)]
485    fn compute_block_child_layout(
486        &mut self,
487        node_id: NodeId,
488        inputs: taffy::LayoutInput,
489        block_ctx: Option<&mut BlockContext<'_>>,
490    ) -> taffy::LayoutOutput {
491        compute_cached_layout(self, node_id, inputs, |tree, node_id, inputs| {
492            tree.compute_child_layout_internal(node_id, inputs, block_ctx)
493        })
494    }
495}
496
497impl taffy::LayoutFlexboxContainer for BaseDocument {
498    type FlexboxContainerStyle<'a>
499        = &'a Style<Atom>
500    where
501        Self: 'a;
502
503    type FlexboxItemStyle<'a>
504        = &'a Style<Atom>
505    where
506        Self: 'a;
507
508    fn get_flexbox_container_style(&self, node_id: NodeId) -> Self::FlexboxContainerStyle<'_> {
509        self.get_core_container_style(node_id)
510    }
511
512    fn get_flexbox_child_style(&self, child_node_id: NodeId) -> Self::FlexboxItemStyle<'_> {
513        self.get_core_container_style(child_node_id)
514    }
515}
516
517impl taffy::LayoutGridContainer for BaseDocument {
518    type GridContainerStyle<'a>
519        = &'a Style<Atom>
520    where
521        Self: 'a;
522
523    type GridItemStyle<'a>
524        = &'a Style<Atom>
525    where
526        Self: 'a;
527
528    fn get_grid_container_style(&self, node_id: NodeId) -> Self::GridContainerStyle<'_> {
529        self.get_core_container_style(node_id)
530    }
531
532    fn get_grid_child_style(&self, child_node_id: NodeId) -> Self::GridItemStyle<'_> {
533        self.get_core_container_style(child_node_id)
534    }
535
536    fn set_detailed_grid_info(
537        &mut self,
538        node_id: NodeId,
539        detailed_grid_info: taffy::DetailedGridInfo<Atom>,
540    ) {
541        let node = self.node_from_id_mut(node_id);
542        if let Some(element) = node.element_data_mut() {
543            element.detailed_grid_info = Some(Box::new(detailed_grid_info));
544        }
545    }
546}
547
548impl RoundTree for BaseDocument {
549    fn get_unrounded_layout(&self, node_id: NodeId) -> Layout {
550        *self.node_from_id(node_id).unrounded_layout()
551    }
552
553    fn set_final_layout(&mut self, node_id: NodeId, layout: &Layout) {
554        *self.node_from_id_mut(node_id).final_layout_mut() = *layout;
555    }
556}
557
558impl PrintTree for BaseDocument {
559    fn get_debug_label(&self, node_id: NodeId) -> &'static str {
560        let node = &self.node_from_id(node_id);
561
562        match node.data {
563            NodeData::Document(_) => "DOCUMENT",
564            // NodeData::Doctype { .. } => return "DOCTYPE",
565            NodeData::Text { .. } => node.node_debug_str().leak(),
566            NodeData::Comment { .. } => "COMMENT",
567            NodeData::AnonymousBlock(_) => "ANONYMOUS BLOCK",
568            NodeData::Element(_) => {
569                let style = node.style();
570                let display = match style.display {
571                    Display::Flex => match style.flex_direction {
572                        FlexDirection::Row | FlexDirection::RowReverse => "FLEX ROW",
573                        FlexDirection::Column | FlexDirection::ColumnReverse => "FLEX COL",
574                    },
575                    Display::Grid => "GRID",
576                    Display::Block => "BLOCK",
577                    Display::FlowRoot => "FLOW ROOT",
578                    Display::None => "NONE",
579                };
580                format!("{} ({})", node.node_debug_str(), display).leak()
581            } // NodeData::ProcessingInstruction { .. } => return "PROCESSING INSTRUCTION",
582        }
583    }
584
585    fn get_final_layout(&self, node_id: NodeId) -> Layout {
586        *self.node_from_id(node_id).final_layout()
587    }
588}
589
590// pub struct ChildIter<'a>(std::slice::Iter<'a, usize>);
591// impl<'a> Iterator for ChildIter<'a> {
592//     type Item = NodeId;
593//     fn next(&mut self) -> Option<Self::Item> {
594//         self.0.next().copied().map(NodeId::from)
595//     }
596// }
597
598pub struct RefCellChildIter<'a> {
599    items: Ref<'a, [crate::NodeId]>,
600    idx: usize,
601}
602impl<'a> RefCellChildIter<'a> {
603    fn new(items: Ref<'a, [crate::NodeId]>) -> RefCellChildIter<'a> {
604        RefCellChildIter { items, idx: 0 }
605    }
606}
607
608impl Iterator for RefCellChildIter<'_> {
609    type Item = NodeId;
610    fn next(&mut self) -> Option<Self::Item> {
611        self.items.get(self.idx).map(|id| {
612            self.idx += 1;
613            taffy_node_id(*id)
614        })
615    }
616}