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