layout/
construct_modern.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
5//! Layout construction code that is shared between modern layout modes (Flexbox and CSS Grid)
6
7use std::borrow::Cow;
8use std::sync::OnceLock;
9
10use rayon::iter::{IntoParallelIterator, ParallelIterator};
11use style::selector_parser::PseudoElement;
12
13use crate::PropagatedBoxTreeData;
14use crate::context::LayoutContext;
15use crate::dom::{BoxSlot, LayoutBox, NodeExt};
16use crate::dom_traversal::{Contents, NodeAndStyleInfo, TraversalHandler};
17use crate::flow::inline::SharedInlineStyles;
18use crate::flow::inline::construct::InlineFormattingContextBuilder;
19use crate::flow::{BlockContainer, BlockFormattingContext};
20use crate::formatting_contexts::{
21    IndependentFormattingContext, IndependentFormattingContextContents,
22};
23use crate::layout_box_base::LayoutBoxBase;
24use crate::style_ext::{ComputedValuesExt, DisplayGeneratingBox};
25
26/// A builder used for both flex and grid containers.
27pub(crate) struct ModernContainerBuilder<'a, 'dom> {
28    context: &'a LayoutContext<'a>,
29    info: &'a NodeAndStyleInfo<'dom>,
30    /// A [`NodeAndStyleInfo`] to use for anonymous box children. Only initialized if
31    /// there is such a child.
32    anonymous_info: OnceLock<NodeAndStyleInfo<'dom>>,
33    propagated_data: PropagatedBoxTreeData,
34    contiguous_text_runs: Vec<ModernContainerTextRun<'dom>>,
35    /// To be run in parallel with rayon in `finish`
36    jobs: Vec<ModernContainerJob<'dom>>,
37    has_text_runs: bool,
38    /// A stack of `display: contents` styles currently in scope. This matters because
39    /// `display: contents` elements do not generate boxes but still provide styling
40    /// for their children, and text runs which get different styles due to that can be
41    /// wrapped into the same anonymous flex/grid item.
42    display_contents_shared_styles: Vec<SharedInlineStyles>,
43}
44
45enum ModernContainerJob<'dom> {
46    ElementOrPseudoElement {
47        info: NodeAndStyleInfo<'dom>,
48        display: DisplayGeneratingBox,
49        contents: Contents,
50        box_slot: BoxSlot<'dom>,
51    },
52    TextRuns(Vec<ModernContainerTextRun<'dom>>, BoxSlot<'dom>),
53}
54
55impl<'dom> ModernContainerJob<'dom> {
56    fn finish(self, builder: &ModernContainerBuilder) -> Option<ModernItem<'dom>> {
57        match self {
58            ModernContainerJob::TextRuns(runs, box_slot) => {
59                let mut inline_formatting_context_builder =
60                    InlineFormattingContextBuilder::new(builder.info, builder.context);
61                let mut last_style_from_display_contents: Option<SharedInlineStyles> = None;
62                for flex_text_run in runs.into_iter() {
63                    match (
64                        last_style_from_display_contents.as_ref(),
65                        flex_text_run.style_from_display_contents.as_ref(),
66                    ) {
67                        (None, None) => {},
68                        (Some(old_style), Some(new_style)) if old_style.ptr_eq(new_style) => {},
69                        _ => {
70                            // If we have nested `display: contents`, then this logic will leave the
71                            // outer one before entering the new one. This is fine, because the inline
72                            // formatting context builder only uses the last style on the stack.
73                            if last_style_from_display_contents.is_some() {
74                                inline_formatting_context_builder.leave_display_contents();
75                            }
76                            if let Some(ref new_style) = flex_text_run.style_from_display_contents {
77                                inline_formatting_context_builder
78                                    .enter_display_contents(new_style.clone());
79                            }
80                        },
81                    }
82                    last_style_from_display_contents = flex_text_run.style_from_display_contents;
83                    inline_formatting_context_builder
84                        .push_text(flex_text_run.text, &flex_text_run.info);
85                }
86
87                let inline_formatting_context = inline_formatting_context_builder.finish(
88                    builder.context,
89                    true,  /* has_first_formatted_line */
90                    false, /* is_single_line_text_box */
91                    builder.info.style.to_bidi_level(),
92                )?;
93
94                let block_formatting_context = BlockFormattingContext::from_block_container(
95                    BlockContainer::InlineFormattingContext(inline_formatting_context),
96                );
97
98                let info = builder.anonymous_info();
99                let formatting_context = IndependentFormattingContext::new(
100                    LayoutBoxBase::new(info.into(), info.style.clone()),
101                    IndependentFormattingContextContents::Flow(block_formatting_context),
102                    // This is just a series of anonymous text runs, so we don't need to worry
103                    // about what kind of PropagatedBoxTreeData is used here.
104                    Default::default(),
105                );
106
107                Some(ModernItem {
108                    kind: ModernItemKind::InFlow(formatting_context),
109                    order: 0,
110                    box_slot,
111                })
112            },
113            ModernContainerJob::ElementOrPseudoElement {
114                info,
115                display,
116                contents,
117                box_slot,
118            } => {
119                let is_abspos = info.style.get_box().position.is_absolutely_positioned();
120                let order = if is_abspos {
121                    0
122                } else {
123                    info.style.clone_order()
124                };
125
126                if let Some(layout_box) =
127                    box_slot
128                        .take_layout_box()
129                        .and_then(|layout_box| match &layout_box {
130                            LayoutBox::FlexLevel(_) | LayoutBox::TaffyItemBox(_) => {
131                                Some(layout_box)
132                            },
133                            _ => None,
134                        })
135                {
136                    return Some(ModernItem {
137                        kind: ModernItemKind::ReusedBox(layout_box),
138                        order,
139                        box_slot,
140                    });
141                }
142
143                // Text decorations are not propagated to any out-of-flow descendants. In addition,
144                // absolutes don't affect the size of ancestors so it is fine to allow descendent
145                // tables to resolve percentage columns.
146                let propagated_data = match is_abspos {
147                    false => builder.propagated_data,
148                    true => PropagatedBoxTreeData::default(),
149                };
150
151                let formatting_context = IndependentFormattingContext::construct(
152                    builder.context,
153                    &info,
154                    display.display_inside(),
155                    contents,
156                    propagated_data,
157                );
158
159                let kind = if is_abspos {
160                    ModernItemKind::OutOfFlow(formatting_context)
161                } else {
162                    ModernItemKind::InFlow(formatting_context)
163                };
164                Some(ModernItem {
165                    kind,
166                    order,
167                    box_slot,
168                })
169            },
170        }
171    }
172}
173
174struct ModernContainerTextRun<'dom> {
175    info: NodeAndStyleInfo<'dom>,
176    text: Cow<'dom, str>,
177    style_from_display_contents: Option<SharedInlineStyles>,
178}
179
180impl ModernContainerTextRun<'_> {
181    /// <https://drafts.csswg.org/css-text/#white-space>
182    fn is_only_document_white_space(&self) -> bool {
183        // FIXME: is this the right definition? See
184        // https://github.com/w3c/csswg-drafts/issues/5146
185        // https://github.com/w3c/csswg-drafts/issues/5147
186        self.text
187            .bytes()
188            .all(|byte| matches!(byte, b' ' | b'\n' | b'\t'))
189    }
190}
191
192pub(crate) enum ModernItemKind {
193    InFlow(IndependentFormattingContext),
194    OutOfFlow(IndependentFormattingContext),
195    ReusedBox(LayoutBox),
196}
197
198pub(crate) struct ModernItem<'dom> {
199    pub kind: ModernItemKind,
200    pub order: i32,
201    pub box_slot: BoxSlot<'dom>,
202}
203
204impl<'dom> TraversalHandler<'dom> for ModernContainerBuilder<'_, 'dom> {
205    fn handle_text(&mut self, info: &NodeAndStyleInfo<'dom>, text: Cow<'dom, str>) {
206        self.contiguous_text_runs.push(ModernContainerTextRun {
207            info: info.clone(),
208            text,
209            style_from_display_contents: self.display_contents_shared_styles.last().cloned(),
210        })
211    }
212
213    fn enter_display_contents(&mut self, styles: SharedInlineStyles) {
214        self.display_contents_shared_styles.push(styles);
215    }
216
217    fn leave_display_contents(&mut self) {
218        self.display_contents_shared_styles.pop();
219    }
220
221    /// Or pseudo-element
222    fn handle_element(
223        &mut self,
224        info: &NodeAndStyleInfo<'dom>,
225        display: DisplayGeneratingBox,
226        contents: Contents,
227        box_slot: BoxSlot<'dom>,
228    ) {
229        self.wrap_any_text_in_anonymous_block_container();
230
231        self.jobs.push(ModernContainerJob::ElementOrPseudoElement {
232            info: info.clone(),
233            display,
234            contents,
235            box_slot,
236        })
237    }
238}
239
240impl<'a, 'dom> ModernContainerBuilder<'a, 'dom> {
241    pub fn new(
242        context: &'a LayoutContext<'a>,
243        info: &'a NodeAndStyleInfo<'dom>,
244        propagated_data: PropagatedBoxTreeData,
245    ) -> Self {
246        ModernContainerBuilder {
247            context,
248            info,
249            anonymous_info: Default::default(),
250            propagated_data: propagated_data.disallowing_percentage_table_columns(),
251            contiguous_text_runs: Vec::new(),
252            jobs: Vec::new(),
253            has_text_runs: false,
254            display_contents_shared_styles: Vec::new(),
255        }
256    }
257
258    fn anonymous_info(&self) -> &NodeAndStyleInfo<'dom> {
259        self.anonymous_info.get_or_init(|| {
260            self.info
261                .with_pseudo_element(self.context, PseudoElement::ServoAnonymousBox)
262                .expect("Should always be able to construct info for anonymous boxes.")
263        })
264    }
265
266    fn wrap_any_text_in_anonymous_block_container(&mut self) {
267        let runs = std::mem::take(&mut self.contiguous_text_runs);
268
269        // If there is no text run or they all only contain document white space
270        // characters, do nothing.
271        if runs
272            .iter()
273            .all(ModernContainerTextRun::is_only_document_white_space)
274        {
275            return;
276        }
277
278        let box_slot = self.anonymous_info().node.box_slot();
279        self.jobs.push(ModernContainerJob::TextRuns(runs, box_slot));
280        self.has_text_runs = true;
281    }
282
283    pub(crate) fn finish(mut self) -> Vec<ModernItem<'dom>> {
284        self.wrap_any_text_in_anonymous_block_container();
285
286        let jobs = std::mem::take(&mut self.jobs);
287        let mut children: Vec<_> = if self.context.use_rayon {
288            jobs.into_par_iter()
289                .filter_map(|job| job.finish(&self))
290                .collect()
291        } else {
292            jobs.into_iter()
293                .filter_map(|job| job.finish(&self))
294                .collect()
295        };
296
297        // https://drafts.csswg.org/css-flexbox/#order-modified-document-order
298        children.sort_by_key(|child| child.order);
299
300        children
301    }
302}