Skip to main content

bliss_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 Bliss, 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, node::Node};
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, NodeId, ResolveOrZero,
17    RoundTree, Style, TraversePartialTree, TraverseTree, compute_block_layout,
18    compute_cached_layout, compute_flexbox_layout, compute_grid_layout, compute_leaf_layout,
19    prelude::*,
20};
21
22pub(crate) mod construct;
23pub(crate) mod damage;
24pub(crate) mod inline;
25pub(crate) mod list;
26pub(crate) mod replaced;
27pub(crate) mod table;
28
29use self::replaced::{ReplacedContext, replaced_measure_function};
30use self::table::TableTreeWrapper;
31
32pub(crate) fn resolve_calc_value(calc_ptr: *const (), parent_size: f32) -> f32 {
33    let calc = unsafe { &*(calc_ptr as *const CalcLengthPercentage) };
34    let result = calc.resolve(CSSPixelLength::new(parent_size));
35    result.px()
36}
37
38impl BaseDocument {
39    fn node_from_id(&self, node_id: taffy::prelude::NodeId) -> &Node {
40        &self.nodes[node_id.into()]
41    }
42    fn node_from_id_mut(&mut self, node_id: taffy::prelude::NodeId) -> &mut Node {
43        &mut self.nodes[node_id.into()]
44    }
45}
46
47impl BaseDocument {
48    fn compute_child_layout_internal(
49        &mut self,
50        node_id: NodeId,
51        inputs: taffy::tree::LayoutInput,
52        block_ctx: Option<&mut BlockContext<'_>>,
53    ) -> taffy::tree::LayoutOutput {
54        let node = &mut self.nodes[node_id.into()];
55
56        let font_styles = node.primary_styles().map(|style| {
57            use style::values::computed::font::LineHeight;
58
59            let font_size = style.clone_font_size().used_size().px();
60            let line_height = match style.clone_line_height() {
61                LineHeight::Normal => font_size * 1.2,
62                LineHeight::Number(num) => font_size * num.0,
63                LineHeight::Length(value) => value.0.px(),
64            };
65
66            (font_size, line_height)
67        });
68        let font_size = font_styles.map(|s| s.0);
69        let resolved_line_height = font_styles.map(|s| s.1);
70
71        match &mut node.data {
72            NodeData::Text(_data) => {
73                // With the new "inline context" architecture all text nodes should be wrapped in an "inline layout context"
74                // and should therefore never be measured individually.
75                eprintln!(
76                    "bliss-dom: text node {} laid out individually (should be wrapped in inline context)",
77                    usize::from(node_id)
78                );
79                taffy::LayoutOutput::HIDDEN
80                // unreachable!();
81
82                // compute_leaf_layout(inputs, &node.style, |known_dimensions, available_space| {
83                //     let context = TextContext {
84                //         text_content: &data.content.trim(),
85                //         writing_mode: WritingMode::Horizontal,
86                //     };
87                //     let font_metrics = FontMetrics {
88                //         char_width: 8.0,
89                //         char_height: 16.0,
90                //     };
91                //     text_measure_function(
92                //         known_dimensions,
93                //         available_space,
94                //         &context,
95                //         &font_metrics,
96                //     )
97                // })
98            }
99            NodeData::Element(element_data) | NodeData::AnonymousBlock(element_data) => {
100                // TODO: deduplicate with single-line text input
101                if *element_data.name.local == *"textarea" {
102                    let rows = element_data
103                        .attr(local_name!("rows"))
104                        .and_then(|val| val.parse::<f32>().ok())
105                        .unwrap_or(2.0);
106
107                    let cols = element_data
108                        .attr(local_name!("cols"))
109                        .and_then(|val| val.parse::<f32>().ok());
110
111                    return compute_leaf_layout(
112                        inputs,
113                        &node.style,
114                        resolve_calc_value,
115                        |_known_size, _available_space| taffy::Size {
116                            width: cols
117                                .map(|cols| cols * font_size.unwrap_or(16.0) * 0.6)
118                                .unwrap_or(300.0),
119                            height: resolved_line_height.unwrap_or(16.0) * rows,
120                        },
121                    );
122                }
123
124                if *element_data.name.local == *"input" {
125                    match element_data.attr(local_name!("type")) {
126                        // if the input type is hidden, hide it
127                        Some("hidden") => {
128                            node.style.display = Display::None;
129                            return taffy::LayoutOutput::HIDDEN;
130                        }
131                        Some("checkbox") => {
132                            return compute_leaf_layout(
133                                inputs,
134                                &node.style,
135                                resolve_calc_value,
136                                |_known_size, _available_space| {
137                                    let width = node.style.size.width.resolve_or_zero(
138                                        inputs.parent_size.width,
139                                        resolve_calc_value,
140                                    );
141                                    let height = node.style.size.height.resolve_or_zero(
142                                        inputs.parent_size.height,
143                                        resolve_calc_value,
144                                    );
145                                    let min_size = width.min(height);
146                                    taffy::Size {
147                                        width: min_size,
148                                        height: min_size,
149                                    }
150                                },
151                            );
152                        }
153                        None | Some("text" | "password" | "email" | "tel" | "url" | "search") => {
154                            return compute_leaf_layout(
155                                inputs,
156                                &node.style,
157                                resolve_calc_value,
158                                |_known_size, _available_space| taffy::Size {
159                                    width: 300.0,
160                                    height: resolved_line_height.unwrap_or(16.0),
161                                },
162                            );
163                        }
164                        _ => {}
165                    }
166                }
167
168                if *element_data.name.local == *"img"
169                    || *element_data.name.local == *"canvas"
170                    || (cfg!(feature = "svg") && *element_data.name.local == *"svg")
171                {
172                    // Get width and height attributes on image element
173                    //
174                    // TODO: smarter sizing using these (depending on object-fit, they shouldn't
175                    // necessarily just override the native size)
176                    let attr_size = taffy::Size {
177                        width: element_data
178                            .attr(local_name!("width"))
179                            .and_then(|val| val.parse::<f32>().ok()),
180                        height: element_data
181                            .attr(local_name!("height"))
182                            .and_then(|val| val.parse::<f32>().ok()),
183                    };
184
185                    // Get image's native sizespecial_data
186                    let inherent_size = match &element_data.special_data {
187                        SpecialElementData::Image(image_data) => match &**image_data {
188                            ImageData::Raster(image) => taffy::Size {
189                                width: image.width as f32,
190                                height: image.height as f32,
191                            },
192                            #[cfg(feature = "svg")]
193                            ImageData::Svg(svg) => {
194                                let size = svg.size();
195                                taffy::Size {
196                                    width: size.width(),
197                                    height: size.height(),
198                                }
199                            }
200                            ImageData::None => taffy::Size::ZERO,
201                        },
202                        SpecialElementData::Canvas(_) => taffy::Size::ZERO,
203                        SpecialElementData::None => taffy::Size::ZERO,
204                        _ => {
205                            // Unexpected special_data on img/canvas/svg element —
206                            // malformed DOM. Fall back to zero-size.
207                            taffy::Size::ZERO
208                        }
209                    };
210
211                    let replaced_context = ReplacedContext {
212                        inherent_size,
213                        attr_size,
214                    };
215
216                    let computed = replaced_measure_function(
217                        inputs.known_dimensions,
218                        inputs.parent_size,
219                        inputs.available_space,
220                        &replaced_context,
221                        &node.style,
222                        false,
223                    );
224
225                    return taffy::LayoutOutput {
226                        size: computed,
227                        content_size: computed,
228                        first_baselines: taffy::Point::NONE,
229                        top_margin: CollapsibleMarginSet::ZERO,
230                        bottom_margin: CollapsibleMarginSet::ZERO,
231                        margins_can_collapse_through: false,
232                    };
233                }
234
235                if node.flags.is_table_root() {
236                    let Some(el) = self.nodes[node_id.into()].data.downcast_element() else {
237                        // Element marked as table root but can't downcast —
238                        // malformed DOM. Skip table layout gracefully.
239                        return taffy::LayoutOutput::HIDDEN;
240                    };
241                    let SpecialElementData::TableRoot(ref ctx) = el.special_data else {
242                        // Node is marked as table root but has no TableContext —
243                        // malformed DOM. Skip table layout gracefully.
244                        return taffy::LayoutOutput::HIDDEN;
245                    };
246                    let context = Arc::clone(ctx);
247
248                    let mut table_wrapper = TableTreeWrapper {
249                        doc: self,
250                        ctx: context,
251                    };
252                    let mut output = compute_grid_layout(&mut table_wrapper, node_id, inputs);
253
254                    // HACK: Cap content size at node size to prevent scrolling
255                    output.content_size.width = output.content_size.width.min(output.size.width);
256                    output.content_size.height = output.content_size.height.min(output.size.height);
257
258                    return output;
259                }
260
261                if node.flags.is_inline_root() {
262                    return self.compute_inline_layout(usize::from(node_id), inputs, block_ctx);
263                }
264
265                // The default CSS file will set
266                match node.style.display {
267                    Display::Block => compute_block_layout(self, node_id, inputs, block_ctx),
268                    Display::Flex => compute_flexbox_layout(self, node_id, inputs),
269                    Display::Grid => compute_grid_layout(self, node_id, inputs),
270                    Display::None => taffy::LayoutOutput::HIDDEN,
271                }
272            }
273            NodeData::Document => compute_block_layout(self, node_id, inputs, None),
274
275            _ => taffy::LayoutOutput::HIDDEN,
276        }
277    }
278}
279
280impl TraversePartialTree for BaseDocument {
281    type ChildIter<'a> = RefCellChildIter<'a>;
282
283    fn child_ids(&self, node_id: NodeId) -> Self::ChildIter<'_> {
284        let layout_children = self.node_from_id(node_id).layout_children.borrow(); //.unwrap().as_ref();
285        RefCellChildIter::new(Ref::map(layout_children, |children| {
286            children.as_ref().map(|c| c.as_slice()).unwrap_or(&[])
287        }))
288    }
289
290    fn child_count(&self, node_id: NodeId) -> usize {
291        self.node_from_id(node_id)
292            .layout_children
293            .borrow()
294            .as_ref()
295            .map(|c| c.len())
296            .unwrap_or(0)
297    }
298
299    fn get_child_id(&self, node_id: NodeId, index: usize) -> NodeId {
300        NodeId::from(
301            self.node_from_id(node_id)
302                .layout_children
303                .borrow()
304                .as_ref()
305                .unwrap()[index],
306        )
307    }
308}
309impl TraverseTree for BaseDocument {}
310
311impl LayoutPartialTree for BaseDocument {
312    type CoreContainerStyle<'a>
313        = &'a taffy::Style<Atom>
314    where
315        Self: 'a;
316
317    type CustomIdent = Atom;
318
319    fn get_core_container_style(&self, node_id: NodeId) -> &Style<Atom> {
320        &self.node_from_id(node_id).style
321    }
322
323    fn set_unrounded_layout(&mut self, node_id: NodeId, layout: &Layout) {
324        self.node_from_id_mut(node_id).unrounded_layout = *layout;
325    }
326
327    fn resolve_calc_value(&self, calc_ptr: *const (), parent_size: f32) -> f32 {
328        resolve_calc_value(calc_ptr, parent_size)
329    }
330
331    #[inline(always)]
332    fn compute_child_layout(
333        &mut self,
334        node_id: NodeId,
335        inputs: taffy::LayoutInput,
336    ) -> taffy::LayoutOutput {
337        compute_cached_layout(self, node_id, inputs, |tree, node_id, inputs| {
338            tree.compute_child_layout_internal(node_id, inputs, None)
339        })
340    }
341}
342
343impl taffy::CacheTree for BaseDocument {
344    #[inline]
345    fn cache_get(
346        &self,
347        node_id: NodeId,
348        inputs: &taffy::LayoutInput,
349    ) -> Option<taffy::LayoutOutput> {
350        self.node_from_id(node_id).cache.get(inputs)
351    }
352
353    #[inline]
354    fn cache_store(
355        &mut self,
356        node_id: NodeId,
357        inputs: &taffy::LayoutInput,
358        layout_output: taffy::LayoutOutput,
359    ) {
360        self.node_from_id_mut(node_id).cache.store(inputs, layout_output);
361    }
362
363    #[inline]
364    fn cache_clear(&mut self, node_id: NodeId) {
365        self.node_from_id_mut(node_id).cache.clear();
366    }
367}
368
369impl taffy::LayoutBlockContainer for BaseDocument {
370    type BlockContainerStyle<'a>
371        = &'a Style<Atom>
372    where
373        Self: 'a;
374
375    type BlockItemStyle<'a>
376        = &'a Style<Atom>
377    where
378        Self: 'a;
379
380    fn get_block_container_style(&self, node_id: NodeId) -> Self::BlockContainerStyle<'_> {
381        self.get_core_container_style(node_id)
382    }
383
384    fn get_block_child_style(&self, child_node_id: NodeId) -> Self::BlockItemStyle<'_> {
385        self.get_core_container_style(child_node_id)
386    }
387
388    #[inline(always)]
389    fn compute_block_child_layout(
390        &mut self,
391        node_id: NodeId,
392        inputs: taffy::LayoutInput,
393        block_ctx: Option<&mut BlockContext<'_>>,
394    ) -> taffy::LayoutOutput {
395        compute_cached_layout(self, node_id, inputs, |tree, node_id, inputs| {
396            tree.compute_child_layout_internal(node_id, inputs, block_ctx)
397        })
398    }
399}
400
401impl taffy::LayoutFlexboxContainer for BaseDocument {
402    type FlexboxContainerStyle<'a>
403        = &'a Style<Atom>
404    where
405        Self: 'a;
406
407    type FlexboxItemStyle<'a>
408        = &'a Style<Atom>
409    where
410        Self: 'a;
411
412    fn get_flexbox_container_style(&self, node_id: NodeId) -> Self::FlexboxContainerStyle<'_> {
413        self.get_core_container_style(node_id)
414    }
415
416    fn get_flexbox_child_style(&self, child_node_id: NodeId) -> Self::FlexboxItemStyle<'_> {
417        self.get_core_container_style(child_node_id)
418    }
419}
420
421impl taffy::LayoutGridContainer for BaseDocument {
422    type GridContainerStyle<'a>
423        = &'a Style<Atom>
424    where
425        Self: 'a;
426
427    type GridItemStyle<'a>
428        = &'a Style<Atom>
429    where
430        Self: 'a;
431
432    fn get_grid_container_style(&self, node_id: NodeId) -> Self::GridContainerStyle<'_> {
433        self.get_core_container_style(node_id)
434    }
435
436    fn get_grid_child_style(&self, child_node_id: NodeId) -> Self::GridItemStyle<'_> {
437        self.get_core_container_style(child_node_id)
438    }
439}
440
441impl RoundTree for BaseDocument {
442    fn get_unrounded_layout(&self, node_id: NodeId) -> Layout {
443        self.node_from_id(node_id).unrounded_layout
444    }
445
446    fn set_final_layout(&mut self, node_id: NodeId, layout: &Layout) {
447        self.node_from_id_mut(node_id).final_layout = *layout;
448    }
449}
450
451impl PrintTree for BaseDocument {
452    fn get_debug_label(&self, node_id: NodeId) -> &'static str {
453        let node = &self.node_from_id(node_id);
454        let style = &node.style;
455
456        match node.data {
457            NodeData::Document => "DOCUMENT",
458            // NodeData::Doctype { .. } => return "DOCTYPE",
459            NodeData::Text { .. } => node.node_debug_str().leak(),
460            NodeData::Comment => "COMMENT",
461            NodeData::AnonymousBlock(_) => "ANONYMOUS BLOCK",
462            NodeData::ShadowRoot { .. } => "SHADOW ROOT",
463            NodeData::Element(_) => {
464                let display = match style.display {
465                    Display::Flex => match style.flex_direction {
466                        FlexDirection::Row | FlexDirection::RowReverse => "FLEX ROW",
467                        FlexDirection::Column | FlexDirection::ColumnReverse => "FLEX COL",
468                    },
469                    Display::Grid => "GRID",
470                    Display::Block => "BLOCK",
471                    Display::None => "NONE",
472                };
473                format!("{} ({})", node.node_debug_str(), display).leak()
474            } // NodeData::ProcessingInstruction { .. } => return "PROCESSING INSTRUCTION",
475        }
476    }
477
478    fn get_final_layout(&self, node_id: NodeId) -> Layout {
479        self.node_from_id(node_id).final_layout
480    }
481}
482
483// pub struct ChildIter<'a>(std::slice::Iter<'a, usize>);
484// impl<'a> Iterator for ChildIter<'a> {
485//     type Item = NodeId;
486//     fn next(&mut self) -> Option<Self::Item> {
487//         self.0.next().copied().map(NodeId::from)
488//     }
489// }
490
491pub struct RefCellChildIter<'a> {
492    items: Ref<'a, [usize]>,
493    idx: usize,
494}
495impl<'a> RefCellChildIter<'a> {
496    fn new(items: Ref<'a, [usize]>) -> RefCellChildIter<'a> {
497        RefCellChildIter { items, idx: 0 }
498    }
499}
500
501impl Iterator for RefCellChildIter<'_> {
502    type Item = NodeId;
503    fn next(&mut self) -> Option<Self::Item> {
504        self.items.get(self.idx).map(|id| {
505            self.idx += 1;
506            NodeId::from(*id)
507        })
508    }
509}