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        let is_anonymous = matches!(
281            self.base.style.pseudo(),
282            Some(PseudoElement::ServoAnonymousBox)
283        );
284
285        // https://drafts.csswg.org/css-sizing-4/#stretch-fit-sizing
286        // > If this is a block axis size, and the element is in a Block Layout formatting context,
287        // > and the parent element does not have a block-start border or padding and is not an
288        // > independent formatting context, treat the element’s block-start margin as zero
289        // > for the purpose of calculating this size. Do the same for the block-end margin.
290        //
291        // However, as resolved in https://github.com/w3c/csswg-drafts/issues/13260, we should
292        // check the containing block instead of the parent. Note that anonymous blocks don't
293        // establish a containing block.
294        let ignore_block_margins_for_stretch = if is_anonymous {
295            ignore_block_margins_for_stretch
296        } else {
297            LogicalSides1D::new(
298                pbm.border.block_start.is_zero() && pbm.padding.block_start.is_zero(),
299                pbm.border.block_end.is_zero() && pbm.padding.block_end.is_zero(),
300            )
301        };
302
303        let flow_layout = self.contents.layout(
304            layout_context,
305            positioning_context,
306            &containing_block_for_children,
307            sequential_layout_state.as_deref_mut(),
308            CollapsibleWithParentStartMargin(start_margin_can_collapse_with_children),
309            ignore_block_margins_for_stretch,
310        );
311        let mut content_block_size = flow_layout.content_block_size;
312
313        // Update margins.
314        let mut block_margins_collapsed_with_children = CollapsedBlockMargins::from_margin(&margin);
315        let mut collapsible_margins_in_children = flow_layout.collapsible_margins_in_children;
316        if start_margin_can_collapse_with_children {
317            block_margins_collapsed_with_children
318                .start
319                .adjoin_assign(&collapsible_margins_in_children.start);
320            if collapsible_margins_in_children.collapsed_through {
321                block_margins_collapsed_with_children
322                    .start
323                    .adjoin_assign(&std::mem::replace(
324                        &mut collapsible_margins_in_children.end,
325                        CollapsedMargin::zero(),
326                    ));
327            }
328        }
329
330        let tentative_block_size = if is_anonymous {
331            // Anonymous blocks do not establish a containing block for their children,
332            // so we can't use that. However, they always have their sizing properties
333            // set to their initial values, so it's fine to use the default.
334            &Default::default()
335        } else {
336            &containing_block_for_children.size.block
337        };
338        let collapsed_through = collapsible_margins_in_children.collapsed_through &&
339            pbm.padding_border_sums.block.is_zero() &&
340            tentative_block_size.definite_or_min().is_zero();
341        block_margins_collapsed_with_children.collapsed_through = collapsed_through;
342
343        let end_margin_can_collapse_with_children =
344            pbm.padding.block_end.is_zero() && pbm.border.block_end.is_zero();
345        if !end_margin_can_collapse_with_children {
346            content_block_size += collapsible_margins_in_children.end.solve();
347        }
348
349        let block_size = block_sizes.resolve(
350            Direction::Block,
351            Size::FitContent,
352            Au::zero,
353            available_block_size,
354            || content_block_size.into(),
355            false, /* is_table */
356        );
357
358        // If the final block size is different than the intrinsic size of the contents,
359        // then we can't actually collapse the end margins. This can happen due to min
360        // or max block sizes, or due to `calc-size()` once we implement it.
361        //
362        // We also require `block-size` to have an intrinsic value, by checking whether
363        // the containing block established for the contents has an indefinite block size.
364        // However, even if `block-size: 0px` is extrinsic (so it would normally prevent
365        // collapsing the end margin with children), it doesn't prevent the top and end
366        // margins from collapsing through. If that happens, allow collapsing end margins.
367        //
368        // This is being discussed in https://github.com/w3c/csswg-drafts/issues/12218.
369        // It would probably make more sense to check the definiteness of the containing
370        // block in the logic above (when we check if there is some block-end padding or
371        // border), or maybe drop the condition altogether. But for now, we match Blink.
372        let end_margin_can_collapse_with_children = end_margin_can_collapse_with_children &&
373            block_size == content_block_size &&
374            (collapsed_through || !tentative_block_size.is_definite());
375        if end_margin_can_collapse_with_children {
376            block_margins_collapsed_with_children
377                .end
378                .adjoin_assign(&collapsible_margins_in_children.end);
379        }
380
381        if let Some(ref mut sequential_layout_state) = sequential_layout_state {
382            // Now that we're done laying out our children, we can restore the
383            // parent's containing block position information.
384            sequential_layout_state.replace_containing_block_position_info(
385                parent_containing_block_position_info.unwrap(),
386            );
387
388            // Account for padding and border. We also might have to readjust the
389            // `bfc_relative_block_position` if it was different from the content size (i.e. was
390            // non-`auto` and/or was affected by min/max block size).
391            //
392            // If this adjustment is positive, that means that a block size was specified, but
393            // the content inside had a smaller block size. If this adjustment is negative, a
394            // block size was specified, but the content inside overflowed this container in
395            // the block direction. In that case, the ceiling for floats is effectively raised
396            // as long as no floats in the overflowing content lowered it.
397            sequential_layout_state.advance_block_position(
398                block_size - content_block_size + pbm.padding.block_end + pbm.border.block_end,
399            );
400
401            if !end_margin_can_collapse_with_children {
402                sequential_layout_state.commit_margin();
403            }
404            sequential_layout_state.adjoin_assign(&CollapsedMargin::new(margin.block_end));
405        }
406
407        let content_rect = LogicalRect {
408            start_corner: LogicalVec2 {
409                block: (pbm.padding.block_start +
410                    pbm.border.block_start +
411                    clearance.unwrap_or_else(Au::zero)),
412                inline: pbm.padding.inline_start +
413                    pbm.border.inline_start +
414                    effective_margin_inline_start,
415            },
416            size: LogicalVec2 {
417                block: block_size,
418                inline: containing_block_for_children.size.inline,
419            },
420        };
421
422        let mut base_fragment_info = self.base.base_fragment_info;
423
424        // An anonymous block doesn't establish a containing block for its contents. Therefore,
425        // if its contents depend on block constraints, its block size (which is intrinsic) also
426        // depends on block constraints.
427        if depends_on_block_constraints ||
428            (is_anonymous && flow_layout.depends_on_block_constraints)
429        {
430            base_fragment_info.flags.insert(
431                FragmentFlags::SIZE_DEPENDS_ON_BLOCK_CONSTRAINTS_AND_CAN_BE_CHILD_OF_FLEX_ITEM,
432            );
433        }
434
435        BoxFragment::new(
436            base_fragment_info,
437            style.clone(),
438            flow_layout.fragments,
439            content_rect.as_physical(Some(containing_block)),
440            pbm.padding.to_physical(containing_block_writing_mode),
441            pbm.border.to_physical(containing_block_writing_mode),
442            margin.to_physical(containing_block_writing_mode),
443            flow_layout.specific_layout_info,
444        )
445        .with_baselines(flow_layout.baselines)
446        .with_block_level_layout_info(block_margins_collapsed_with_children, clearance)
447    }
448}