Skip to main content

layout/flow/
same_formatting_context_block.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#![allow(rustdoc::private_intra_doc_links)]
5
6//! Same-formatting context blocks. This represents a block in block flow that does not
7//! establish a new formatting context.
8
9use std::sync::Arc;
10
11use app_units::Au;
12use malloc_size_of_derive::MallocSizeOf;
13use script::layout_dom::ServoLayoutNode;
14use servo_arc::Arc as ServoArc;
15use style::Zero;
16use style::context::SharedStyleContext;
17use style::logical_geometry::Direction;
18use style::properties::ComputedValues;
19use style::servo::selector_parser::PseudoElement;
20
21use crate::context::LayoutContext;
22use crate::flow::float::{Clear, ContainingBlockPositionInfo, SequentialLayoutState};
23use crate::flow::{
24    BlockContainer, CollapsibleWithParentStartMargin, ContainingBlockPaddingAndBorder,
25    ResolvedMargins, solve_containing_block_padding_and_border_for_in_flow_box, solve_margins,
26};
27use crate::fragment_tree::{BoxFragment, CollapsedBlockMargins, CollapsedMargin, FragmentFlags};
28use crate::geom::{LogicalRect, LogicalSides1D, LogicalVec2};
29use crate::layout_box_base::LayoutBoxBase;
30use crate::positioned::PositioningContext;
31use crate::sizing::{InlineContentSizesResult, Size};
32use crate::style_ext::LayoutStyle;
33use crate::{ConstraintSpace, ContainingBlock};
34
35/// A block in block flow that does not establish a new formatting context.
36#[derive(Debug, MallocSizeOf)]
37pub(crate) struct SameFormattingContextBlock {
38    pub base: LayoutBoxBase,
39    pub contents: BlockContainer,
40    pub contains_floats: bool,
41}
42
43impl SameFormattingContextBlock {
44    pub(crate) fn new(
45        base: LayoutBoxBase,
46        contents: BlockContainer,
47        contains_floats: bool,
48    ) -> Self {
49        Self {
50            base,
51            contents,
52            contains_floats,
53        }
54    }
55
56    pub(crate) fn layout_style(&self) -> LayoutStyle<'_> {
57        self.contents.layout_style(&self.base)
58    }
59
60    pub(crate) fn repair_style(
61        &mut self,
62        context: &SharedStyleContext,
63        node: &ServoLayoutNode,
64        new_style: &ServoArc<ComputedValues>,
65    ) {
66        self.base.repair_style(new_style);
67        self.contents.repair_style(context, node, new_style);
68    }
69
70    pub(crate) fn inline_content_sizes(
71        &self,
72        layout_context: &LayoutContext,
73        constraint_space: &ConstraintSpace,
74    ) -> InlineContentSizesResult {
75        self.base
76            .inline_content_sizes(layout_context, constraint_space, &self.contents)
77    }
78
79    /// Lay out a normal flow non-replaced [`SameFormattingContextBlock`], properly taking
80    /// into account relative positioning. This version also handles caching the layout
81    /// results and fetching the results from the cache, if they are still valid.
82    ///
83    /// - <https://drafts.csswg.org/css2/visudet.html#blockwidth>
84    /// - <https://drafts.csswg.org/css2/visudet.html#normal-block>
85    #[expect(clippy::too_many_arguments)]
86    pub(crate) fn layout_in_flow_non_replaced_block_level_cached(
87        &self,
88        layout_context: &LayoutContext<'_>,
89        positioning_context: &mut PositioningContext,
90        containing_block: &ContainingBlock<'_>,
91        sequential_layout_state: Option<&mut SequentialLayoutState>,
92        collapsible_with_parent_start_margin: Option<CollapsibleWithParentStartMargin>,
93        ignore_block_margins_for_stretch: LogicalSides1D<bool>,
94        has_inline_parent: bool,
95    ) -> Arc<BoxFragment> {
96        let mut allows_caching = sequential_layout_state.is_none();
97
98        if allows_caching &&
99            let Some(cached_result) = self
100                .base
101                .cached_same_formatting_context_block_if_applicable(
102                    containing_block,
103                    collapsible_with_parent_start_margin,
104                    ignore_block_margins_for_stretch,
105                    has_inline_parent,
106                )
107        {
108            return cached_result;
109        };
110
111        let positioning_context_length = positioning_context.len();
112        let fragment = Arc::new(positioning_context.layout_maybe_position_relative_fragment(
113            layout_context,
114            containing_block,
115            &self.base,
116            |positioning_context| {
117                self.layout_in_flow_non_replaced_block_level(
118                    layout_context,
119                    positioning_context,
120                    containing_block,
121                    sequential_layout_state,
122                    collapsible_with_parent_start_margin,
123                    ignore_block_margins_for_stretch,
124                    has_inline_parent,
125                )
126            },
127        ));
128
129        // We currently do not allow caching `SameFormattingContextBlock` box layout results if they
130        // contain absolutely positioned children.
131        //
132        // TODO: It would be good to find a way to allow this, without having to create and store a
133        // PositioningContext for every single SameFormattingContextBlock.
134        allows_caching = allows_caching && positioning_context_length == positioning_context.len();
135
136        if !allows_caching {
137            self.base.clear_fragments_and_dirty_fragment_cache();
138        } else {
139            self.base.cache_same_formatting_context_block_layout(
140                containing_block,
141                collapsible_with_parent_start_margin,
142                ignore_block_margins_for_stretch,
143                has_inline_parent,
144                fragment.clone(),
145            );
146        }
147
148        fragment
149    }
150
151    /// Lay out a normal flow non-replaced [`SameFormattingContextBlock`].
152    ///
153    /// - <https://drafts.csswg.org/css2/visudet.html#blockwidth>
154    /// - <https://drafts.csswg.org/css2/visudet.html#normal-block>
155    #[expect(clippy::too_many_arguments)]
156    fn layout_in_flow_non_replaced_block_level(
157        &self,
158        layout_context: &LayoutContext,
159        positioning_context: &mut PositioningContext,
160        containing_block: &ContainingBlock,
161        mut sequential_layout_state: Option<&mut SequentialLayoutState>,
162        collapsible_with_parent_start_margin: Option<CollapsibleWithParentStartMargin>,
163        ignore_block_margins_for_stretch: LogicalSides1D<bool>,
164        has_inline_parent: bool,
165    ) -> BoxFragment {
166        let style = &self.base.style;
167        let layout_style = self.contents.layout_style(&self.base);
168        let containing_block_writing_mode = containing_block.style.writing_mode;
169        let get_inline_content_sizes = |constraint_space: &ConstraintSpace| {
170            self.base
171                .inline_content_sizes(layout_context, constraint_space, &self.contents)
172                .sizes
173        };
174        let ContainingBlockPaddingAndBorder {
175            containing_block: containing_block_for_children,
176            pbm,
177            block_sizes,
178            depends_on_block_constraints,
179            available_block_size,
180            justify_self,
181            ..
182        } = solve_containing_block_padding_and_border_for_in_flow_box(
183            containing_block,
184            &layout_style,
185            get_inline_content_sizes,
186            ignore_block_margins_for_stretch,
187            None,
188            has_inline_parent,
189        );
190        let ResolvedMargins {
191            margin,
192            effective_margin_inline_start,
193        } = solve_margins(
194            containing_block,
195            &pbm,
196            containing_block_for_children.size.inline,
197            justify_self,
198        );
199
200        let start_margin_can_collapse_with_children =
201            pbm.padding.block_start.is_zero() && pbm.border.block_start.is_zero();
202
203        let mut clearance = None;
204        let parent_containing_block_position_info;
205        match sequential_layout_state {
206            None => parent_containing_block_position_info = None,
207            Some(ref mut sequential_layout_state) => {
208                let clear = Clear::from_style_and_container_writing_mode(
209                    style,
210                    containing_block_writing_mode,
211                );
212                let mut block_start_margin = CollapsedMargin::new(margin.block_start);
213
214                // The block start margin may collapse with content margins,
215                // compute the resulting one in order to place floats correctly.
216                // Only need to do this if the element isn't also collapsing with its parent,
217                // otherwise we should have already included the margin in an ancestor.
218                // Note this lookahead stops when finding a descendant whose `clear` isn't `none`
219                // (since clearance prevents collapsing margins with the parent).
220                // But then we have to decide whether to actually add clearance or not,
221                // so look forward again regardless of `collapsible_with_parent_start_margin`.
222                // TODO: This isn't completely right: if we don't add actual clearance,
223                // the margin should have been included in the parent (or some ancestor).
224                // The lookahead should stop for actual clearance, not just for `clear`.
225                let collapsible_with_parent_start_margin = collapsible_with_parent_start_margin.expect(
226                    "We should know whether we are collapsing the block start margin with the parent \
227                    when laying out sequentially",
228                ).0 && clear == Clear::None;
229                if !collapsible_with_parent_start_margin && start_margin_can_collapse_with_children
230                {
231                    self.contents.find_block_margin_collapsing_with_parent(
232                        layout_context,
233                        &mut block_start_margin,
234                        &containing_block_for_children,
235                    );
236                }
237
238                // Introduce clearance if necessary.
239                clearance = sequential_layout_state.calculate_clearance(clear, &block_start_margin);
240                if clearance.is_some() {
241                    sequential_layout_state.commit_margin();
242                }
243                sequential_layout_state.adjoin_assign(&block_start_margin);
244                if !start_margin_can_collapse_with_children {
245                    sequential_layout_state.commit_margin();
246                }
247
248                // NB: This will be a no-op if we're collapsing margins with our children since that
249                // can only happen if we have no block-start padding and border.
250                sequential_layout_state.advance_block_position(
251                    pbm.padding.block_start +
252                        pbm.border.block_start +
253                        clearance.unwrap_or_else(Au::zero),
254                );
255
256                // We are about to lay out children. Update the offset between the block formatting
257                // context and the containing block that we create for them. This offset is used to
258                // ajust BFC relative coordinates to coordinates that are relative to our content box.
259                // Our content box establishes the containing block for non-abspos children, including
260                // floats.
261                let inline_start = sequential_layout_state
262                    .floats
263                    .containing_block_info
264                    .inline_start +
265                    pbm.padding.inline_start +
266                    pbm.border.inline_start +
267                    effective_margin_inline_start;
268                let new_cb_offsets = ContainingBlockPositionInfo {
269                    block_start: sequential_layout_state.bfc_relative_block_position,
270                    block_start_margins_not_collapsed: sequential_layout_state.current_margin,
271                    inline_start,
272                    inline_end: inline_start + containing_block_for_children.size.inline,
273                };
274                parent_containing_block_position_info = Some(
275                    sequential_layout_state.replace_containing_block_position_info(new_cb_offsets),
276                );
277            },
278        };
279
280        // https://drafts.csswg.org/css-sizing-4/#stretch-fit-sizing
281        // > If this is a block axis size, and the element is in a Block Layout formatting context,
282        // > and the parent element does not have a block-start border or padding and is not an
283        // > independent formatting context, treat the element’s block-start margin as zero
284        // > for the purpose of calculating this size. Do the same for the block-end margin.
285        let ignore_block_margins_for_stretch = LogicalSides1D::new(
286            pbm.border.block_start.is_zero() && pbm.padding.block_start.is_zero(),
287            pbm.border.block_end.is_zero() && pbm.padding.block_end.is_zero(),
288        );
289
290        let flow_layout = self.contents.layout(
291            layout_context,
292            positioning_context,
293            &containing_block_for_children,
294            sequential_layout_state.as_deref_mut(),
295            CollapsibleWithParentStartMargin(start_margin_can_collapse_with_children),
296            ignore_block_margins_for_stretch,
297        );
298        let mut content_block_size = flow_layout.content_block_size;
299
300        // Update margins.
301        let mut block_margins_collapsed_with_children = CollapsedBlockMargins::from_margin(&margin);
302        let mut collapsible_margins_in_children = flow_layout.collapsible_margins_in_children;
303        if start_margin_can_collapse_with_children {
304            block_margins_collapsed_with_children
305                .start
306                .adjoin_assign(&collapsible_margins_in_children.start);
307            if collapsible_margins_in_children.collapsed_through {
308                block_margins_collapsed_with_children
309                    .start
310                    .adjoin_assign(&std::mem::replace(
311                        &mut collapsible_margins_in_children.end,
312                        CollapsedMargin::zero(),
313                    ));
314            }
315        }
316
317        let is_anonymous = matches!(
318            self.base.style.pseudo(),
319            Some(PseudoElement::ServoAnonymousBox)
320        );
321        let tentative_block_size = if is_anonymous {
322            // Anonymous blocks do not establish a containing block for their children,
323            // so we can't use that. However, they always have their sizing properties
324            // set to their initial values, so it's fine to use the default.
325            &Default::default()
326        } else {
327            &containing_block_for_children.size.block
328        };
329        let collapsed_through = collapsible_margins_in_children.collapsed_through &&
330            pbm.padding_border_sums.block.is_zero() &&
331            tentative_block_size.definite_or_min().is_zero();
332        block_margins_collapsed_with_children.collapsed_through = collapsed_through;
333
334        let end_margin_can_collapse_with_children =
335            pbm.padding.block_end.is_zero() && pbm.border.block_end.is_zero();
336        if !end_margin_can_collapse_with_children {
337            content_block_size += collapsible_margins_in_children.end.solve();
338        }
339
340        let block_size = block_sizes.resolve(
341            Direction::Block,
342            Size::FitContent,
343            Au::zero,
344            available_block_size,
345            || content_block_size.into(),
346            false, /* is_table */
347        );
348
349        // If the final block size is different than the intrinsic size of the contents,
350        // then we can't actually collapse the end margins. This can happen due to min
351        // or max block sizes, or due to `calc-size()` once we implement it.
352        //
353        // We also require `block-size` to have an intrinsic value, by checking whether
354        // the containing block established for the contents has an indefinite block size.
355        // However, even if `block-size: 0px` is extrinsic (so it would normally prevent
356        // collapsing the end margin with children), it doesn't prevent the top and end
357        // margins from collapsing through. If that happens, allow collapsing end margins.
358        //
359        // This is being discussed in https://github.com/w3c/csswg-drafts/issues/12218.
360        // It would probably make more sense to check the definiteness of the containing
361        // block in the logic above (when we check if there is some block-end padding or
362        // border), or maybe drop the condition altogether. But for now, we match Blink.
363        let end_margin_can_collapse_with_children = end_margin_can_collapse_with_children &&
364            block_size == content_block_size &&
365            (collapsed_through || !tentative_block_size.is_definite());
366        if end_margin_can_collapse_with_children {
367            block_margins_collapsed_with_children
368                .end
369                .adjoin_assign(&collapsible_margins_in_children.end);
370        }
371
372        if let Some(ref mut sequential_layout_state) = sequential_layout_state {
373            // Now that we're done laying out our children, we can restore the
374            // parent's containing block position information.
375            sequential_layout_state.replace_containing_block_position_info(
376                parent_containing_block_position_info.unwrap(),
377            );
378
379            // Account for padding and border. We also might have to readjust the
380            // `bfc_relative_block_position` if it was different from the content size (i.e. was
381            // non-`auto` and/or was affected by min/max block size).
382            //
383            // If this adjustment is positive, that means that a block size was specified, but
384            // the content inside had a smaller block size. If this adjustment is negative, a
385            // block size was specified, but the content inside overflowed this container in
386            // the block direction. In that case, the ceiling for floats is effectively raised
387            // as long as no floats in the overflowing content lowered it.
388            sequential_layout_state.advance_block_position(
389                block_size - content_block_size + pbm.padding.block_end + pbm.border.block_end,
390            );
391
392            if !end_margin_can_collapse_with_children {
393                sequential_layout_state.commit_margin();
394            }
395            sequential_layout_state.adjoin_assign(&CollapsedMargin::new(margin.block_end));
396        }
397
398        let content_rect = LogicalRect {
399            start_corner: LogicalVec2 {
400                block: (pbm.padding.block_start +
401                    pbm.border.block_start +
402                    clearance.unwrap_or_else(Au::zero)),
403                inline: pbm.padding.inline_start +
404                    pbm.border.inline_start +
405                    effective_margin_inline_start,
406            },
407            size: LogicalVec2 {
408                block: block_size,
409                inline: containing_block_for_children.size.inline,
410            },
411        };
412
413        let mut base_fragment_info = self.base.base_fragment_info;
414
415        // An anonymous block doesn't establish a containing block for its contents. Therefore,
416        // if its contents depend on block constraints, its block size (which is intrinsic) also
417        // depends on block constraints.
418        if depends_on_block_constraints ||
419            (is_anonymous && flow_layout.depends_on_block_constraints)
420        {
421            base_fragment_info.flags.insert(
422                FragmentFlags::SIZE_DEPENDS_ON_BLOCK_CONSTRAINTS_AND_CAN_BE_CHILD_OF_FLEX_ITEM,
423            );
424        }
425
426        BoxFragment::new(
427            base_fragment_info,
428            style.clone(),
429            flow_layout.fragments,
430            content_rect.as_physical(Some(containing_block)),
431            pbm.padding.to_physical(containing_block_writing_mode),
432            pbm.border.to_physical(containing_block_writing_mode),
433            margin.to_physical(containing_block_writing_mode),
434            flow_layout.specific_layout_info,
435        )
436        .with_baselines(flow_layout.baselines)
437        .with_block_level_layout_info(block_margins_collapsed_with_children, clearance)
438    }
439}