Skip to main content

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::sync::OnceLock;
8
9use layout_api::LayoutNode;
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::{BoxTreeString, 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 = InlineFormattingContextBuilder::new(
60                    builder.info,
61                    builder.context,
62                    false, /* should_process_first_letter */
63                );
64                let mut last_style_from_display_contents: Option<SharedInlineStyles> = None;
65                for flex_text_run in runs.into_iter() {
66                    match (
67                        last_style_from_display_contents.as_ref(),
68                        flex_text_run.style_from_display_contents.as_ref(),
69                    ) {
70                        (None, None) => {},
71                        (Some(old_style), Some(new_style)) if old_style.ptr_eq(new_style) => {},
72                        _ => {
73                            // If we have nested `display: contents`, then this logic will leave the
74                            // outer one before entering the new one. This is fine, because the inline
75                            // formatting context builder only uses the last style on the stack.
76                            if last_style_from_display_contents.is_some() {
77                                inline_formatting_context_builder.leave_display_contents();
78                            }
79                            if let Some(ref new_style) = flex_text_run.style_from_display_contents {
80                                inline_formatting_context_builder
81                                    .enter_display_contents(new_style.clone());
82                            }
83                        },
84                    }
85                    last_style_from_display_contents = flex_text_run.style_from_display_contents;
86                    inline_formatting_context_builder.push_text(
87                        flex_text_run.text,
88                        &flex_text_run.info,
89                        flex_text_run.info.node.text_node_selection(),
90                    );
91                }
92
93                let inline_formatting_context = inline_formatting_context_builder
94                    .finish(
95                        builder.context,
96                        true,  /* has_first_formatted_line */
97                        false, /* is_single_line_text_box */
98                        builder.info.style.to_bidi_level(),
99                    )
100                    .expect("Did not expect document white space only text runs");
101
102                let block_formatting_context = BlockFormattingContext::from_block_container(
103                    BlockContainer::InlineFormattingContext(inline_formatting_context),
104                );
105
106                let info = builder.anonymous_info();
107                let formatting_context = IndependentFormattingContext::new(
108                    LayoutBoxBase::new(info.into(), info.style.clone()),
109                    IndependentFormattingContextContents::Flow(block_formatting_context),
110                    // This is just a series of anonymous text runs, so we don't need to worry
111                    // about what kind of PropagatedBoxTreeData is used here.
112                    Default::default(),
113                );
114
115                Some(ModernItem {
116                    kind: ModernItemKind::InFlow(formatting_context),
117                    order: 0,
118                    box_slot,
119                })
120            },
121            ModernContainerJob::ElementOrPseudoElement {
122                info,
123                display,
124                contents,
125                box_slot,
126            } => {
127                let is_abspos = info.style.get_box().position.is_absolutely_positioned();
128                let order = if is_abspos {
129                    0
130                } else {
131                    info.style.slow_clone_order()
132                };
133
134                if let Some(layout_box) =
135                    box_slot
136                        .take_layout_box()
137                        .and_then(|layout_box| match &layout_box {
138                            LayoutBox::FlexLevel(_) | LayoutBox::TaffyItemBox(_) => {
139                                Some(layout_box)
140                            },
141                            _ => None,
142                        })
143                {
144                    return Some(ModernItem {
145                        kind: ModernItemKind::ReusedBox(layout_box),
146                        order,
147                        box_slot,
148                    });
149                }
150
151                // Text decorations are not propagated to any out-of-flow descendants. In addition,
152                // absolutes don't affect the size of ancestors so it is fine to allow descendent
153                // tables to resolve percentage columns.
154                let propagated_data = match is_abspos {
155                    false => builder.propagated_data,
156                    true => PropagatedBoxTreeData::default(),
157                };
158
159                let formatting_context = IndependentFormattingContext::construct(
160                    builder.context,
161                    &info,
162                    display.display_inside(),
163                    contents,
164                    propagated_data,
165                );
166
167                let kind = if is_abspos {
168                    ModernItemKind::OutOfFlow(formatting_context)
169                } else {
170                    ModernItemKind::InFlow(formatting_context)
171                };
172                Some(ModernItem {
173                    kind,
174                    order,
175                    box_slot,
176                })
177            },
178        }
179    }
180}
181
182struct ModernContainerTextRun<'dom> {
183    info: NodeAndStyleInfo<'dom>,
184    text: BoxTreeString<'dom>,
185    style_from_display_contents: Option<SharedInlineStyles>,
186}
187
188impl ModernContainerTextRun<'_> {
189    /// <https://drafts.csswg.org/css-flexbox/#flex-items>:
190    /// > However, if the entire text sequences contains only document white space characters (i.e.
191    /// > characters that can be affected by the white-space property) it is instead not rendered
192    /// > (just as if its text nodes were display:none).
193    fn is_only_document_white_space(&self) -> bool {
194        self.text
195            .bytes()
196            .all(|byte| InlineFormattingContextBuilder::is_document_white_space(byte.into()))
197    }
198}
199
200pub(crate) enum ModernItemKind {
201    InFlow(IndependentFormattingContext),
202    OutOfFlow(IndependentFormattingContext),
203    ReusedBox(LayoutBox),
204}
205
206pub(crate) struct ModernItem<'dom> {
207    pub kind: ModernItemKind,
208    pub order: i32,
209    pub box_slot: BoxSlot<'dom>,
210}
211
212impl<'dom> TraversalHandler<'dom> for ModernContainerBuilder<'_, 'dom> {
213    fn handle_text(&mut self, info: &NodeAndStyleInfo<'dom>, text: BoxTreeString<'dom>) {
214        self.contiguous_text_runs.push(ModernContainerTextRun {
215            info: info.clone(),
216            text,
217            style_from_display_contents: self.display_contents_shared_styles.last().cloned(),
218        })
219    }
220
221    fn enter_display_contents(&mut self, styles: SharedInlineStyles) {
222        self.display_contents_shared_styles.push(styles);
223    }
224
225    fn leave_display_contents(&mut self) {
226        self.display_contents_shared_styles.pop();
227    }
228
229    /// Or pseudo-element
230    fn handle_element(
231        &mut self,
232        info: &NodeAndStyleInfo<'dom>,
233        display: DisplayGeneratingBox,
234        contents: Contents,
235        box_slot: BoxSlot<'dom>,
236    ) {
237        self.wrap_any_text_in_anonymous_block_container();
238
239        self.jobs.push(ModernContainerJob::ElementOrPseudoElement {
240            info: info.clone(),
241            display,
242            contents,
243            box_slot,
244        })
245    }
246}
247
248impl<'a, 'dom> ModernContainerBuilder<'a, 'dom> {
249    pub fn new(
250        context: &'a LayoutContext<'a>,
251        info: &'a NodeAndStyleInfo<'dom>,
252        propagated_data: PropagatedBoxTreeData,
253    ) -> Self {
254        ModernContainerBuilder {
255            context,
256            info,
257            anonymous_info: Default::default(),
258            propagated_data: propagated_data.disallowing_percentage_table_columns(),
259            contiguous_text_runs: Vec::new(),
260            jobs: Vec::new(),
261            has_text_runs: false,
262            display_contents_shared_styles: Vec::new(),
263        }
264    }
265
266    fn anonymous_info(&self) -> &NodeAndStyleInfo<'dom> {
267        self.anonymous_info.get_or_init(|| {
268            self.info
269                .with_pseudo_element(self.context, PseudoElement::ServoAnonymousBox)
270                .expect("Should always be able to construct info for anonymous boxes.")
271        })
272    }
273
274    fn wrap_any_text_in_anonymous_block_container(&mut self) {
275        let runs = std::mem::take(&mut self.contiguous_text_runs);
276
277        // If there is no text run or they all only contain document white space
278        // characters, do nothing.
279        if runs
280            .iter()
281            .all(ModernContainerTextRun::is_only_document_white_space)
282        {
283            return;
284        }
285
286        let box_slot = self.anonymous_info().node.box_slot();
287        self.jobs.push(ModernContainerJob::TextRuns(runs, box_slot));
288        self.has_text_runs = true;
289    }
290
291    pub(crate) fn finish(mut self) -> Vec<ModernItem<'dom>> {
292        self.wrap_any_text_in_anonymous_block_container();
293
294        let jobs = std::mem::take(&mut self.jobs);
295        let mut children: Vec<_> = if self.context.should_parallelize(jobs.iter().len()) {
296            jobs.into_par_iter()
297                .filter_map(|job| job.finish(&self))
298                .collect()
299        } else {
300            jobs.into_iter()
301                .filter_map(|job| job.finish(&self))
302                .collect()
303        };
304
305        // https://drafts.csswg.org/css-flexbox/#order-modified-document-order
306        children.sort_by_key(|child| child.order);
307
308        children
309    }
310}