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