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