Skip to main content

layout/taffy/
layout.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use app_units::Au;
6use atomic_refcell::{AtomicRef, AtomicRefCell};
7use style::logical_geometry::Direction;
8use style::properties::ComputedValues;
9use style::values::computed::CSSPixelLength;
10use style::values::computed::length_percentage::CalcLengthPercentage;
11use style::values::specified::align::AlignFlags;
12use style::values::specified::box_::DisplayInside;
13use style::{Atom, Zero};
14use taffy::style_helpers::{TaffyMaxContent, TaffyMinContent};
15use taffy::{AvailableSpace, MaybeMath, RequestedAxis, RunMode};
16
17use super::{
18    SpecificTaffyGridInfo, TaffyContainer, TaffyItemBox, TaffyItemBoxInner, TaffyStyloStyle,
19};
20use crate::cell::ArcRefCell;
21use crate::context::LayoutContext;
22use crate::dom::WeakLayoutBox;
23use crate::formatting_contexts::{Baselines, IndependentFormattingContext};
24use crate::fragment_tree::{
25    BoxFragment, CollapsedBlockMargins, Fragment, FragmentFlags, SpecificLayoutInfo,
26};
27use crate::geom::{LogicalVec2, PhysicalPoint, PhysicalRect, PhysicalSides, PhysicalSize};
28use crate::layout_box_base::IndependentFormattingContextLayoutResult;
29use crate::positioned::{AbsolutelyPositionedBox, PositioningContext, PositioningContextLength};
30use crate::sizing::{
31    ComputeInlineContentSizes, ContentSizes, InlineContentSizesResult, LazySize, Size,
32    SizeConstraint,
33};
34use crate::style_ext::{ContentBoxSizesAndPBM, LayoutStyle};
35use crate::{ConstraintSpace, ContainingBlock, ContainingBlockSize, IndefiniteContainingBlock};
36
37const DUMMY_NODE_ID: taffy::NodeId = taffy::NodeId::new(u64::MAX);
38
39fn resolve_content_size(constraint: AvailableSpace, content_sizes: ContentSizes) -> f32 {
40    match constraint {
41        AvailableSpace::Definite(limit) => {
42            let min = content_sizes.min_content.to_f32_px();
43            let max = content_sizes.max_content.to_f32_px();
44            limit.min(max).max(min)
45        },
46        AvailableSpace::MinContent => content_sizes.min_content.to_f32_px(),
47        AvailableSpace::MaxContent => content_sizes.max_content.to_f32_px(),
48    }
49}
50
51#[inline(always)]
52fn with_independent_formatting_context<T>(
53    item: &mut TaffyItemBoxInner,
54    cb: impl FnOnce(&IndependentFormattingContext) -> T,
55) -> T {
56    match item {
57        TaffyItemBoxInner::InFlowBox(context) => cb(context),
58        TaffyItemBoxInner::OutOfFlowAbsolutelyPositionedBox(abspos_box) => {
59            cb(&AtomicRefCell::borrow(abspos_box).context)
60        },
61    }
62}
63
64/// Layout parameters and intermediate results about a taffy container,
65/// grouped to avoid passing around many parameters
66struct TaffyContainerContext<'a> {
67    source_child_nodes: &'a [ArcRefCell<TaffyItemBox>],
68    layout_context: &'a LayoutContext<'a>,
69    positioning_context: &'a mut PositioningContext,
70    content_box_size_override: &'a ContainingBlock<'a>,
71    style: &'a ComputedValues,
72    specific_layout_info: Option<SpecificLayoutInfo>,
73
74    /// Temporary location for children specific info, which will be moved into child fragments
75    child_specific_layout_infos: Vec<Option<SpecificLayoutInfo>>,
76}
77
78struct ChildIter(std::ops::Range<usize>);
79impl Iterator for ChildIter {
80    type Item = taffy::NodeId;
81    fn next(&mut self) -> Option<Self::Item> {
82        self.0.next().map(taffy::NodeId::from)
83    }
84
85    fn size_hint(&self) -> (usize, Option<usize>) {
86        (self.0.len(), Some(self.0.len()))
87    }
88}
89
90impl taffy::TraversePartialTree for TaffyContainerContext<'_> {
91    type ChildIter<'a>
92        = ChildIter
93    where
94        Self: 'a;
95
96    fn child_ids(&self, _node_id: taffy::NodeId) -> Self::ChildIter<'_> {
97        ChildIter(0..self.source_child_nodes.len())
98    }
99
100    fn child_count(&self, _node_id: taffy::NodeId) -> usize {
101        self.source_child_nodes.len()
102    }
103
104    fn get_child_id(&self, _node_id: taffy::NodeId, index: usize) -> taffy::NodeId {
105        taffy::NodeId::from(index)
106    }
107}
108
109impl taffy::LayoutPartialTree for TaffyContainerContext<'_> {
110    type CustomIdent = Atom;
111
112    type CoreContainerStyle<'a>
113        = TaffyStyloStyle<&'a ComputedValues>
114    where
115        Self: 'a;
116
117    fn get_core_container_style(&self, _node_id: taffy::NodeId) -> Self::CoreContainerStyle<'_> {
118        TaffyStyloStyle::new(self.style, false /* is_replaced */)
119    }
120
121    fn set_unrounded_layout(&mut self, node_id: taffy::NodeId, layout: &taffy::Layout) {
122        let id = usize::from(node_id);
123        (*self.source_child_nodes[id]).borrow_mut().taffy_layout = *layout;
124    }
125
126    #[expect(unsafe_code)]
127    fn resolve_calc_value(&self, val: *const (), basis: f32) -> f32 {
128        // SAFETY:
129        // - The calc `val` here is the same pointer we return to Taffy in `convert::length_percentage`
130        //   so it is safe to cast the type back to `*const CalcLengthPercentage`
131        // - Taffy guarantees that it never retains style values beyond the scope of it's style
132        //   computation methods, so we can be sure that the pointer we have passed it is still valid.
133        // - The reference we create here has a lifetime that does not escape this function, so it does
134        //   not matter if the pointer is later destroyed.
135        let calc = unsafe { &*(val as *const CalcLengthPercentage) };
136        calc.resolve(CSSPixelLength::new(basis)).px()
137    }
138
139    fn compute_child_layout(
140        &mut self,
141        node_id: taffy::NodeId,
142        inputs: taffy::LayoutInput,
143    ) -> taffy::LayoutOutput {
144        let mut child = (*self.source_child_nodes[usize::from(node_id)]).borrow_mut();
145        let child = &mut *child;
146
147        let output = with_independent_formatting_context(
148            &mut child.taffy_level_box,
149            |independent_context| -> taffy::LayoutOutput {
150                // TODO: re-evaluate sizing constraint conversions in light of recent layout changes
151                let containing_block = self.content_box_size_override;
152                let style = independent_context.style();
153
154                let child_containing_block = IndefiniteContainingBlock {
155                    size: LogicalVec2 {
156                        inline: inputs.parent_size.width.map(Au::from_f32_px),
157                        block: inputs.parent_size.height.map(Au::from_f32_px),
158                    },
159                    style: containing_block.style,
160                };
161
162                // Adjust known_dimensions from border box to content box
163                let ContentBoxSizesAndPBM {
164                    content_box_sizes,
165                    pbm,
166                    ..
167                } = independent_context
168                    .layout_style()
169                    .content_box_sizes_and_padding_border_margin(&child_containing_block);
170                let pb_sum = pbm.padding_border_sums.map(|v| v.to_f32_px());
171                let margin_sum = pbm.margin.auto_is(Au::zero).sum().map(|v| v.to_f32_px());
172                let content_box_inset = pb_sum + margin_sum;
173                let content_box_known_dimensions = taffy::Size {
174                    width: inputs
175                        .known_dimensions
176                        .width
177                        .map(|width| width - pb_sum.inline),
178                    height: inputs
179                        .known_dimensions
180                        .height
181                        .map(|height| height - pb_sum.block),
182                };
183                let preferred_aspect_ratio =
184                    independent_context.preferred_aspect_ratio(&pbm.padding_border_sums);
185
186                // TODO: pass min- and max- size
187                let tentative_block_size = content_box_known_dimensions
188                    .height
189                    .map(Au::from_f32_px)
190                    .map_or_else(SizeConstraint::default, SizeConstraint::Definite);
191
192                // Compute inline size
193                let inline_size = content_box_known_dimensions.width.unwrap_or_else(|| {
194                    let constraint_space = ConstraintSpace {
195                        block_size: tentative_block_size,
196                        style,
197                        preferred_aspect_ratio,
198                    };
199
200                    // TODO: pass min- and max- size
201                    let result = independent_context
202                        .inline_content_sizes(self.layout_context, &constraint_space);
203                    let adjusted_available_space = inputs
204                        .available_space
205                        .width
206                        .map_definite_value(|width| width - content_box_inset.inline);
207
208                    resolve_content_size(adjusted_available_space, result.sizes)
209                });
210
211                // Return early if only inline content sizes are requested
212                if inputs.run_mode == RunMode::ComputeSize &&
213                    inputs.axis == RequestedAxis::Horizontal
214                {
215                    return taffy::LayoutOutput::from_outer_size(taffy::Size {
216                        width: inline_size + pb_sum.inline,
217                        // If RequestedAxis is Horizontal then height will be ignored.
218                        height: 0.0,
219                    });
220                }
221
222                let content_box_size_override = ContainingBlock {
223                    size: ContainingBlockSize {
224                        inline: Au::from_f32_px(inline_size),
225                        block: tentative_block_size,
226                    },
227                    style,
228                };
229
230                let lazy_block_size = match content_box_known_dimensions.height {
231                    None => LazySize::new(
232                        &content_box_sizes.block,
233                        Direction::Block,
234                        Size::FitContent,
235                        Au::zero,
236                        None,
237                        independent_context.layout_style().is_table(),
238                    ),
239                    Some(height) => Au::from_f32_px(height).into(),
240                };
241
242                child.positioning_context = PositioningContext::default();
243                let layout = independent_context.layout(
244                    self.layout_context,
245                    &mut child.positioning_context,
246                    &content_box_size_override,
247                    containing_block,
248                    preferred_aspect_ratio,
249                    &lazy_block_size,
250                );
251
252                child.child_fragments = layout.fragments;
253                self.child_specific_layout_infos[usize::from(node_id)] =
254                    layout.specific_layout_info;
255
256                let block_size = lazy_block_size
257                    .resolve(|| layout.content_block_size)
258                    .to_f32_px();
259
260                let computed_size = taffy::Size {
261                    width: inline_size + pb_sum.inline,
262                    height: block_size + pb_sum.block,
263                };
264                let size = inputs.known_dimensions.unwrap_or(computed_size);
265
266                taffy::LayoutOutput {
267                    size,
268                    baselines: taffy::Baselines {
269                        first: layout.baselines.first.map(|baseline| {
270                            (baseline + pbm.padding.block_start + pbm.border.block_start)
271                                .to_f32_px()
272                        }),
273                        last: layout.baselines.last.map(|baseline| {
274                            (baseline + pbm.padding.block_start + pbm.border.block_start)
275                                .to_f32_px()
276                        }),
277                    },
278                    ..taffy::LayoutOutput::DEFAULT
279                }
280            },
281        );
282        child.taffy_baselines = output.baselines;
283        output
284    }
285}
286
287impl taffy::LayoutGridContainer for TaffyContainerContext<'_> {
288    type GridContainerStyle<'a>
289        = TaffyStyloStyle<&'a ComputedValues>
290    where
291        Self: 'a;
292
293    type GridItemStyle<'a>
294        = TaffyStyloStyle<AtomicRef<'a, ComputedValues>>
295    where
296        Self: 'a;
297
298    fn get_grid_container_style(
299        &self,
300        _node_id: taffy::prelude::NodeId,
301    ) -> Self::GridContainerStyle<'_> {
302        TaffyStyloStyle::new(self.style, false /* is_replaced */)
303    }
304
305    fn get_grid_child_style(
306        &self,
307        child_node_id: taffy::prelude::NodeId,
308    ) -> Self::GridItemStyle<'_> {
309        let id = usize::from(child_node_id);
310        let child = (*self.source_child_nodes[id]).borrow();
311        // TODO: account for non-replaced elements that are "compressible replaced"
312        let is_replaced = child.is_in_flow_replaced();
313        let stylo_style = AtomicRef::map(child, |c| &*c.style);
314        TaffyStyloStyle::new(stylo_style, is_replaced)
315    }
316
317    fn set_detailed_grid_info(
318        &mut self,
319        _node_id: taffy::NodeId,
320        specific_layout_info: taffy::DetailedGridInfo<Atom>,
321    ) {
322        self.specific_layout_info = Some(SpecificLayoutInfo::Grid(Box::new(
323            SpecificTaffyGridInfo::from_detailed_grid_layout(specific_layout_info),
324        )));
325    }
326}
327
328impl ComputeInlineContentSizes for TaffyContainer {
329    fn compute_inline_content_sizes(
330        &self,
331        layout_context: &LayoutContext,
332        _constraint_space: &ConstraintSpace,
333    ) -> InlineContentSizesResult {
334        let style = &self.style;
335
336        let max_content_inputs = taffy::LayoutInput {
337            run_mode: taffy::RunMode::ComputeSize,
338            sizing_mode: taffy::SizingMode::InherentSize,
339            axis: taffy::RequestedAxis::Horizontal,
340            vertical_margins_are_collapsible: taffy::Line::FALSE,
341
342            known_dimensions_are_definite: taffy::Size {
343                width: true,
344                height: true,
345            },
346            known_dimensions: taffy::Size::NONE,
347            parent_size: taffy::Size::NONE,
348            available_space: taffy::Size::MAX_CONTENT,
349        };
350
351        let min_content_inputs = taffy::LayoutInput {
352            available_space: taffy::Size::MIN_CONTENT,
353            ..max_content_inputs
354        };
355
356        let containing_block = &ContainingBlock {
357            size: ContainingBlockSize {
358                inline: Au::zero(),
359                block: SizeConstraint::default(),
360            },
361            style,
362        };
363
364        let mut grid_context = TaffyContainerContext {
365            layout_context,
366            positioning_context: &mut PositioningContext::default(),
367            content_box_size_override: containing_block,
368            style,
369            source_child_nodes: &self.children,
370            specific_layout_info: None,
371            child_specific_layout_infos: vec![None; self.children.len()],
372        };
373
374        let (max_content_output, min_content_output) = match style.get_display().inside() {
375            DisplayInside::Grid => {
376                let max_content_output = taffy::compute_grid_layout(
377                    &mut grid_context,
378                    DUMMY_NODE_ID,
379                    max_content_inputs,
380                );
381                let min_content_output = taffy::compute_grid_layout(
382                    &mut grid_context,
383                    DUMMY_NODE_ID,
384                    min_content_inputs,
385                );
386                (max_content_output, min_content_output)
387            },
388            _ => panic!("Servo is only configured to use Taffy for CSS Grid layout"),
389        };
390
391        let pb_sums = self
392            .layout_style()
393            .padding_border_margin(containing_block)
394            .padding_border_sums;
395
396        InlineContentSizesResult {
397            sizes: ContentSizes {
398                max_content: Au::from_f32_px(max_content_output.size.width) - pb_sums.inline,
399                min_content: Au::from_f32_px(min_content_output.size.width) - pb_sums.inline,
400            },
401
402            // TODO: determine this accurately
403            //
404            // "true" is a safe default as it will prevent Servo from performing optimizations based
405            // on the assumption that the node's size does not depend on block constraints.
406            depends_on_block_constraints: true,
407        }
408    }
409}
410
411impl TaffyContainer {
412    /// <https://drafts.csswg.org/css-grid/#layout-algorithm>
413    pub(crate) fn layout(
414        &self,
415        layout_context: &LayoutContext,
416        positioning_context: &mut PositioningContext,
417        content_box_size_override: &ContainingBlock,
418        containing_block: &ContainingBlock,
419    ) -> IndependentFormattingContextLayoutResult {
420        let mut container_ctx = TaffyContainerContext {
421            layout_context,
422            positioning_context,
423            content_box_size_override,
424            style: content_box_size_override.style,
425            source_child_nodes: &self.children,
426            specific_layout_info: None,
427            child_specific_layout_infos: vec![None; self.children.len()],
428        };
429
430        let container_style = &content_box_size_override.style;
431        let align_items = container_style.get_align_items();
432        let justify_items = container_style.get_justify_items();
433        let pbm = self.layout_style().padding_border_margin(containing_block);
434
435        let known_dimensions = taffy::Size {
436            width: Some(
437                (content_box_size_override.size.inline + pbm.padding_border_sums.inline)
438                    .to_f32_px(),
439            ),
440            height: content_box_size_override
441                .size
442                .block
443                .to_definite()
444                .map(Au::to_f32_px)
445                .maybe_add(pbm.padding_border_sums.block.to_f32_px()),
446        };
447
448        let taffy_containing_block = taffy::Size {
449            width: Some(containing_block.size.inline.to_f32_px()),
450            height: containing_block.size.block.to_definite().map(Au::to_f32_px),
451        };
452
453        let layout_input = taffy::LayoutInput {
454            run_mode: taffy::RunMode::PerformLayout,
455            sizing_mode: taffy::SizingMode::InherentSize,
456            axis: taffy::RequestedAxis::Vertical,
457            vertical_margins_are_collapsible: taffy::Line::FALSE,
458
459            known_dimensions_are_definite: taffy::Size {
460                width: true,
461                height: true,
462            },
463            known_dimensions,
464            parent_size: taffy_containing_block,
465            available_space: taffy_containing_block.map(AvailableSpace::from),
466        };
467
468        let output = match container_ctx.style.get_display().inside() {
469            DisplayInside::Grid => {
470                taffy::compute_grid_layout(&mut container_ctx, DUMMY_NODE_ID, layout_input)
471            },
472            _ => panic!("Servo is only configured to use Taffy for CSS Grid layout"),
473        };
474
475        // Convert `taffy::Layout` into Servo `Fragment`s
476        // with container_ctx.child_specific_layout_infos will also moved to the corresponding `Fragment`s
477        let fragments: Vec<Fragment> = self
478            .children
479            .iter()
480            .map(|child| (**child).borrow_mut())
481            .enumerate()
482            .map(|(child_id, mut child)| {
483                fn rect_to_physical_sides<T>(rect: taffy::Rect<T>) -> PhysicalSides<T> {
484                    PhysicalSides::new(rect.top, rect.right, rect.bottom, rect.left)
485                }
486
487                fn size_and_pos_to_logical_rect<T: Default>(
488                    position: taffy::Point<T>,
489                    size: taffy::Size<T>,
490                ) -> PhysicalRect<T> {
491                    PhysicalRect::new(
492                        PhysicalPoint::new(position.x, position.y),
493                        PhysicalSize::new(size.width, size.height),
494                    )
495                }
496
497                let layout = &child.taffy_layout;
498
499                let padding = rect_to_physical_sides(layout.padding.map(Au::from_f32_px));
500                let border = rect_to_physical_sides(layout.border.map(Au::from_f32_px));
501                let margin = rect_to_physical_sides(layout.margin.map(Au::from_f32_px));
502
503                // Compute content box size and position.
504                //
505                // For the x/y position we have to correct for the difference between the
506                // content box and the border box for both the parent and the child.
507                let content_size = size_and_pos_to_logical_rect(
508                    taffy::Point {
509                        x: Au::from_f32_px(
510                            layout.location.x + layout.padding.left + layout.border.left,
511                        ) - pbm.padding.inline_start -
512                            pbm.border.inline_start,
513                        y: Au::from_f32_px(
514                            layout.location.y + layout.padding.top + layout.border.top,
515                        ) - pbm.padding.block_start -
516                            pbm.border.block_start,
517                    },
518                    taffy::Size {
519                        width: layout.size.width -
520                            layout.padding.left -
521                            layout.padding.right -
522                            layout.border.left -
523                            layout.border.right,
524                        height: layout.size.height -
525                            layout.padding.top -
526                            layout.padding.bottom -
527                            layout.border.top -
528                            layout.border.bottom,
529                    }
530                    .map(Au::from_f32_px),
531                );
532
533                let child_specific_layout_info: Option<SpecificLayoutInfo> =
534                    std::mem::take(&mut container_ctx.child_specific_layout_infos[child_id]);
535
536                let fragment = match &mut child.taffy_level_box {
537                    TaffyItemBoxInner::InFlowBox(independent_box) => {
538                        let mut fragment_info = independent_box.base_fragment_info();
539                        fragment_info
540                            .flags
541                            .insert(FragmentFlags::IS_FLEX_OR_GRID_ITEM);
542                        let mut box_fragment = BoxFragment::new(
543                            fragment_info,
544                            independent_box.style().clone(),
545                            std::mem::take(&mut child.child_fragments),
546                            content_size,
547                            padding,
548                            border,
549                            margin,
550                            child_specific_layout_info,
551                        )
552                        .with_baselines(Baselines {
553                            first: child.taffy_baselines.first.map(|baseline| {
554                                Au::from_f32_px(baseline) - padding.top - border.top
555                            }),
556                            last: child.taffy_baselines.last.map(|baseline| {
557                                Au::from_f32_px(baseline) - padding.top - border.top
558                            }),
559                        });
560
561                        child.positioning_context.layout_collected_children(
562                            container_ctx.layout_context,
563                            &mut box_fragment,
564                        );
565
566                        child
567                            .positioning_context
568                            .adjust_static_position_of_hoisted_fragments_with_offset(
569                                &box_fragment.content_rect().origin.to_vector(),
570                                PositioningContextLength::zero(),
571                            );
572                        container_ctx
573                            .positioning_context
574                            .append(std::mem::take(&mut child.positioning_context));
575
576                        Fragment::Box(box_fragment.into())
577                    },
578                    TaffyItemBoxInner::OutOfFlowAbsolutelyPositionedBox(abs_pos_box) => {
579                        fn resolve_alignment(value: AlignFlags, auto: AlignFlags) -> AlignFlags {
580                            match value {
581                                AlignFlags::AUTO => auto,
582                                AlignFlags::NORMAL => AlignFlags::STRETCH,
583                                value => value,
584                            }
585                        }
586
587                        let hoisted_box = AbsolutelyPositionedBox::to_hoisted(
588                            abs_pos_box.clone(),
589                            content_size,
590                            LogicalVec2 {
591                                inline: resolve_alignment(
592                                    child.style.get_align_self().0,
593                                    align_items.0,
594                                ),
595                                block: resolve_alignment(
596                                    child.style.get_justify_self().0,
597                                    justify_items.computed.0.0,
598                                ),
599                            },
600                            container_ctx.style.writing_mode,
601                        );
602                        let hoisted_fragment = hoisted_box.fragment.clone();
603                        container_ctx.positioning_context.push(hoisted_box);
604                        Fragment::AbsoluteOrFixedPositionedPlaceholder(hoisted_fragment)
605                    },
606                };
607
608                if let TaffyItemBoxInner::InFlowBox(independent_formatting_context) =
609                    &child.taffy_level_box
610                {
611                    independent_formatting_context
612                        .base
613                        .set_fragment(fragment.clone());
614                }
615                fragment
616            })
617            .collect();
618
619        IndependentFormattingContextLayoutResult {
620            fragments,
621            content_block_size: Au::from_f32_px(output.size.height) - pbm.padding_border_sums.block,
622            content_inline_size_for_table: None,
623            baselines: Baselines {
624                first: output.baselines.first.map(|baseline| {
625                    Au::from_f32_px(baseline) - pbm.padding.block_start - pbm.border.block_start
626                }),
627                last: output.baselines.last.map(|baseline| {
628                    Au::from_f32_px(baseline) - pbm.padding.block_start - pbm.border.block_start
629                }),
630            },
631
632            // TODO: determine this accurately
633            //
634            // "true" is a safe default as it will prevent Servo from performing optimizations based
635            // on the assumption that the node's size does not depend on block constraints.
636            depends_on_block_constraints: true,
637            specific_layout_info: container_ctx.specific_layout_info,
638            collapsible_margins_in_children: CollapsedBlockMargins::zero(),
639        }
640    }
641
642    #[inline]
643    pub(crate) fn layout_style(&self) -> LayoutStyle<'_> {
644        LayoutStyle::Default(&self.style)
645    }
646
647    pub(crate) fn attached_to_tree(&self, layout_box: WeakLayoutBox) {
648        for child in &self.children {
649            child.borrow_mut().with_base_mut(|base| {
650                base.parent_box.replace(layout_box.clone());
651            });
652        }
653    }
654
655    pub(crate) fn subtree_size(&self) -> usize {
656        self.children
657            .iter()
658            .map(|child| child.borrow().with_base(|base| base.subtree_size()))
659            .sum()
660    }
661}