Skip to main content

layout/display_list/
paint_traversal.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 std::rc::Rc;
6use std::sync::Arc;
7
8use app_units::Au;
9use servo_base::id::ScrollTreeNodeId;
10use style::values::computed::TextDecorationLine;
11
12use crate::display_list::{
13    ClipId, FragmentTextDecoration, StackingContext, StackingContextFragments,
14};
15use crate::fragment_tree::{
16    BoxFragment, BoxFragmentWithStyle, Fragment, FragmentFlags, IFrameFragment, ImageFragment,
17    PositioningFragment, Tag, TextFragment,
18};
19use crate::geom::{PhysicalPoint, PhysicalRect};
20
21pub(crate) struct PaintTraversal<'a, Handler: PaintTraversalHandler> {
22    handler: &'a mut Handler,
23    outlines: Vec<(TraversalState, Arc<BoxFragment>)>,
24    floats: Vec<(TraversalState, Arc<BoxFragment>)>,
25}
26
27impl<'a, Handler: PaintTraversalHandler> PaintTraversal<'a, Handler> {
28    pub(crate) fn traverse(root_stacking_context: &StackingContext, handler: &'a mut Handler) {
29        Self {
30            handler,
31            outlines: Vec::new(),
32            floats: Vec::new(),
33        }
34        .traverse_stacking_context(&TraversalState::default(), root_stacking_context);
35    }
36
37    /// <https://drafts.csswg.org/css-position-4/#paint-a-stacking-context>
38    fn traverse_stacking_context(
39        &mut self,
40        state: &TraversalState,
41        stacking_context: &StackingContext,
42    ) {
43        let old_outlines_length = self.outlines.len();
44        let state = state.push_stacking_context(stacking_context);
45        let stacking_context_state = self.handler.visit_stacking_context(stacking_context);
46
47        // > Step 1: If root is an element, paint a stacking context given root’s principal
48        // > box and canvas, then return.
49        // > Step 2: Assert: root is a box, and generates a stacking context.
50        // > Ensured by the fact that `self` is a stacking context.
51
52        // > Step 3: If root is a root element’s principal box, paint root’s background over
53        // > the entire canvas, with the origin of the background positioning area being the
54        // > position on canvas that would be used if root’s background was being painted
55        // > normally.
56        if let StackingContextFragments::Root = &stacking_context.fragment {
57            self.handler.visit_box_for_root_background(&state);
58        }
59
60        // > Step 4: If root is a block-level box, paint a block’s decorations given root
61        // > and canvas.
62        let root_fragment = stacking_context.fragment();
63        let root_fragment = root_fragment
64            .as_ref()
65            .map(|root_fragment| root_fragment.with_style());
66        if let Some(root_fragment) = &root_fragment &&
67            !root_fragment.is_inline_box()
68        {
69            self.handle_box(&state, root_fragment);
70        }
71
72        // > Step 5: For each of root’s positioned descendants with negative (non-zero) z-index
73        // > values, sort those descendants by z-index order (most negative first) then tree
74        // > order, and paint a stacking context given each descendant and canvas.
75        let mut children = stacking_context.children.iter().peekable();
76        while children.peek().is_some_and(|child| child.z_index < 0) {
77            self.traverse_stacking_context(
78                &state,
79                children.next().expect("Should have a value due to peek."),
80            );
81        }
82
83        if let Some(root_fragment) = &root_fragment {
84            self.traverse_stacking_context_inner(&state, root_fragment);
85        }
86
87        // > Step 9: For each of root’s positioned descendants with z-index: auto or
88        // > z-index: 0, in tree order:
89        // >   ↪ descendant has z-index: auto
90        // >     Paint a stacking container given the descendant and canvas.
91        // >   ↪ descendant has z-index: 0
92        // >     Paint a stacking context given the descendant and canvas.
93        //
94        // > Step 10: For each of root’s positioned descendants with positive (non-zero)
95        // > z-index values, sort those descendants by z-index order (smallest first) then
96        // > tree order, and paint a stacking context given each descendant and canvas.
97        for child in children {
98            assert!(child.z_index >= 0);
99            self.traverse_stacking_context(&state, child);
100        }
101
102        // > Step 11: If the UA uses out-of-band outlines, draw all of root’s outlines
103        // > (those that it skipped drawing due to not using in-band outlines during the
104        // > current invocation of this algorithm) into canvas.
105        if old_outlines_length < self.outlines.len() {
106            for (state, outline_fragment) in &self.outlines.split_off(old_outlines_length) {
107                self.handler.visit_box_for_outline(state, outline_fragment);
108            }
109        }
110
111        self.handler
112            .leave_stacking_context(&state, stacking_context_state);
113    }
114
115    fn traverse_stacking_context_inner(
116        &mut self,
117        state: &TraversalState,
118        root: &BoxFragmentWithStyle<'_>,
119    ) {
120        let old_float_length = self.floats.len();
121        let mut saw_inline_level_or_replaced = root.is_replaced();
122
123        // > Step 6: For each of root’s in-flow, non-positioned, block-level descendants, in
124        // > tree order, paint a block’s decorations given the descendant and canvas.
125        let inner_state = state.push_box_fragment(root);
126        for child in root.children.iter() {
127            saw_inline_level_or_replaced |=
128                self.traverse_block_level_descendants_decorations(&inner_state, child);
129        }
130
131        // Collapsed table borders are painted after block-level descendants. This isn't
132        // well specified, but is being discussed in <https://github.com/w3c/csswg-drafts/issues/11570>.
133        if root.is_table_grid_with_collapsed_borders() {
134            self.handler
135                .visit_box_for_collapsed_table_borders(state, root);
136        }
137
138        // > Step 7: For each of root’s non-positioned floating descendants, in tree order,
139        // > paint a stacking container given the descendant and canvas.
140        if old_float_length < self.floats.len() {
141            for (state, float_fragment) in &self.floats.split_off(old_float_length) {
142                let float_fragment = &float_fragment.with_style();
143                self.handle_box(state, float_fragment);
144                self.traverse_stacking_context_inner(state, float_fragment);
145            }
146        }
147
148        // Step 8:
149        if root.is_inline_box() {
150            // >  ↪ If root is an inline-level box
151            // >     For each line box root is in, paint a box in a line box given root, the
152            // >     line box, and canvas.
153
154            // TODO(mrobinson): This is not actually the real line box, but the content rectangle
155            // of the inline box. The real line box is hard to access when an inline box starts
156            // a stacking container, so we fall back gracefully to this (perhaps smaller) rectangle.
157            // To fix this, we'd need to put the containing line box rectangle on the `StackingContext`
158            // data structure or onto the `TextFragment` itself somehow.
159            let line_box_rect = PhysicalRect::new(inner_state.origin, root.content_rect().size);
160            self.traverse_box_in_a_line_box(
161                state,
162                root,
163                line_box_rect,
164                true, /* at_stacking_context_root */
165            );
166        } else if saw_inline_level_or_replaced {
167            // >  ↪ Otherwise
168            // >    First for root, then for all its in-flow, non-positioned, block-level
169            // >    descendant boxes, in tree order:
170            // >    1. If the box is a replaced element, paint the replaced content into canvas,
171            // >       atomically.
172            // >    2. Otherwise, for each line box of the box, paint a box in a line box given the
173            // >       box, the line box, and canvas.
174            // >    3. If the UA uses in-band outlines, paint the outlines of the box into canvas.
175            self.traverse_line_boxes_and_replaced_for_box(
176                state, root, true, /* at_root_of_stacking_context */
177            );
178        }
179    }
180
181    /// An implementation of
182    /// <https://drafts.csswg.org/css-position-4/#paint-a-stacking-context> that only
183    /// implements the parts relevant to stacking containers. This is an optimization to
184    /// avoid work when descending into positioned container and stacking context
185    /// contents.
186    fn traverse_stacking_container(
187        &mut self,
188        state: &TraversalState,
189        root: &BoxFragmentWithStyle<'_>,
190        is_block_level: bool,
191    ) {
192        let old_outlines_length = self.outlines.len();
193
194        // > Step 4: If root is a block-level box, paint a block’s decorations given root
195        // > and canvas.
196        if is_block_level {
197            self.handle_box(state, root);
198        }
199
200        // This is steps 6 through 8.
201        self.traverse_stacking_context_inner(state, root);
202
203        // > Step 11: If the UA uses out-of-band outlines, draw all of root’s outlines
204        // > (those that it skipped drawing due to not using in-band outlines during the
205        // > current invocation of this algorithm) into canvas.
206        if old_outlines_length < self.outlines.len() {
207            for (state, outline_fragment) in &self.outlines.split_off(old_outlines_length) {
208                self.handler.visit_box_for_outline(state, outline_fragment);
209            }
210        }
211    }
212
213    fn traverse_block_level_descendants_decorations(
214        &mut self,
215        state: &TraversalState,
216        fragment: &Fragment,
217    ) -> bool {
218        let mut saw_inline_level_or_replaced = false;
219
220        match fragment {
221            Fragment::LayoutRoot(layout_root_fragment) => {
222                saw_inline_level_or_replaced = self.traverse_block_level_descendants_decorations(
223                    state,
224                    &layout_root_fragment.inner(),
225                );
226            },
227            Fragment::Box(box_fragment) => {
228                let box_fragment = &box_fragment.with_style();
229                // If this box establishes a stacking context or stacking container, do not paint
230                // it during this phase. Instead it is painted when the stacking context or container
231                // is processed.
232                if box_fragment.stacking_context_type().is_some() {
233                    return false;
234                }
235
236                // We will process inline atomics during the inline level and replaced traversal.
237                if box_fragment.is_atomic_inline_level() || box_fragment.is_flex_or_grid_item() {
238                    return true;
239                }
240
241                // Don't paint any inline boxes, but do descend into them, in case they contain floats.
242                if box_fragment.is_inline_box() {
243                    saw_inline_level_or_replaced = true;
244                } else {
245                    self.handle_box(state, box_fragment);
246                }
247
248                if box_fragment.is_replaced() {
249                    return true;
250                }
251
252                let state_for_children = state.push_box_fragment(box_fragment);
253                for child in box_fragment.children.iter() {
254                    saw_inline_level_or_replaced |= self
255                        .traverse_block_level_descendants_decorations(&state_for_children, child);
256                }
257
258                // Collapsed table borders are painted after block-level descendants. This isn't
259                // well specified, but is being discussed in <https://github.com/w3c/csswg-drafts/issues/11570>.
260                if box_fragment.is_table_grid_with_collapsed_borders() {
261                    self.handler
262                        .visit_box_for_collapsed_table_borders(state, box_fragment);
263                }
264            },
265            Fragment::Float(float_box_fragment) => {
266                if float_box_fragment.stacking_context_type().is_none() {
267                    self.floats
268                        .push((state.without_text_decorations(), float_box_fragment.clone()));
269                }
270            },
271            Fragment::Positioning(positioning_fragment) => {
272                self.handler.visit_positioning(state, positioning_fragment);
273
274                if positioning_fragment.is_line_box() {
275                    saw_inline_level_or_replaced = true;
276                }
277
278                if !positioning_fragment.children.is_empty() {
279                    let state = state.push_positioning_fragment(positioning_fragment);
280                    for child in positioning_fragment.children.iter() {
281                        saw_inline_level_or_replaced |=
282                            self.traverse_block_level_descendants_decorations(&state, child);
283                    }
284                }
285            },
286            Fragment::AbsoluteOrFixedPositionedPlaceholder(..) |
287            Fragment::Text(..) |
288            Fragment::Image(..) |
289            Fragment::IFrame(..) => {},
290        }
291
292        saw_inline_level_or_replaced
293    }
294
295    fn traverse_line_boxes_and_replaced_for_box(
296        &mut self,
297        state: &TraversalState,
298        fragment: &BoxFragmentWithStyle<'_>,
299        at_root_of_stacking_context: bool,
300    ) {
301        let is_flex_or_grid = fragment.is_flex_or_grid_item();
302        if fragment.is_replaced() {
303            if is_flex_or_grid {
304                self.handle_box(state, fragment);
305            }
306
307            let inner_state = state.push_box_fragment(fragment);
308            self.traverse_replaced_content(&inner_state, fragment);
309            return;
310        }
311
312        if !at_root_of_stacking_context && is_flex_or_grid {
313            self.traverse_stacking_container(state, fragment, true /* is_block_level */);
314            return;
315        }
316
317        let inner_state = state.push_box_fragment(fragment);
318        for child in fragment.children.iter() {
319            self.traverse_line_boxes_and_replaced(
320                &inner_state,
321                child,
322                false, /* at_root_of_stacking_context */
323            );
324        }
325    }
326
327    fn traverse_line_boxes_and_replaced(
328        &mut self,
329        state: &TraversalState,
330        fragment: &Fragment,
331        at_root_of_stacking_context: bool,
332    ) {
333        match fragment {
334            Fragment::LayoutRoot(layout_root_fragment) => self.traverse_line_boxes_and_replaced(
335                state,
336                &layout_root_fragment.inner(),
337                at_root_of_stacking_context,
338            ),
339            Fragment::Box(box_fragment) => {
340                // If this box establishes a stacking context or stacking container, do not paint
341                // it during this phase. Instead it is painted when the stacking context or container
342                // is processed.
343                if box_fragment.stacking_context_type().is_some() {
344                    return;
345                }
346                self.traverse_line_boxes_and_replaced_for_box(
347                    state,
348                    &box_fragment.with_style(),
349                    at_root_of_stacking_context,
350                );
351            },
352            Fragment::Positioning(positioning_fragment) if positioning_fragment.is_line_box() => {
353                let state = state.push_positioning_fragment(positioning_fragment);
354                let line_box_rect =
355                    PhysicalRect::new(state.origin, positioning_fragment.base.rect().size);
356                for child in &positioning_fragment.children {
357                    self.traverse_fragment_in_a_line_box(&state, child, line_box_rect);
358                }
359            },
360            Fragment::Positioning(positioning_fragment) => {
361                if !positioning_fragment.children.is_empty() {
362                    let state = state.push_positioning_fragment(positioning_fragment);
363                    for child in &positioning_fragment.children {
364                        self.traverse_line_boxes_and_replaced(
365                            &state, child, false, /* at at_root_of_stacking_context */
366                        );
367                    }
368                }
369            },
370            Fragment::AbsoluteOrFixedPositionedPlaceholder(_) |
371            Fragment::Float(..) |
372            Fragment::IFrame(_) |
373            Fragment::Image(_) |
374            Fragment::Text(..) => {},
375        }
376    }
377
378    fn traverse_fragment_in_a_line_box(
379        &mut self,
380        state: &TraversalState,
381        fragment: &Fragment,
382        line_box_rect: PhysicalRect<Au>,
383    ) {
384        match fragment {
385            Fragment::LayoutRoot(layout_root_fragment) => self.traverse_fragment_in_a_line_box(
386                state,
387                &layout_root_fragment.inner(),
388                line_box_rect,
389            ),
390            Fragment::Box(box_fragment) => self.traverse_box_in_a_line_box(
391                state,
392                &box_fragment.with_style(),
393                line_box_rect,
394                false, /* at_stacking_context_root */
395            ),
396            Fragment::Text(text_fragment) => {
397                self.handler.visit_text(state, line_box_rect, text_fragment);
398            },
399            Fragment::AbsoluteOrFixedPositionedPlaceholder(..) | Fragment::Float(..) => {},
400            Fragment::Positioning(..) => {
401                unreachable!("Unexpected direct descendant PositioningContext of inline.")
402            },
403            Fragment::Image(..) | Fragment::IFrame(..) => {
404                unreachable!("Unexpected replaced content direct descendant of inline.")
405            },
406        }
407    }
408
409    /// <https://www.w3.org/TR/css-position-4/#paint-a-box-in-a-line-box>
410    fn traverse_box_in_a_line_box(
411        &mut self,
412        state: &TraversalState,
413        box_fragment: &BoxFragmentWithStyle<'_>,
414        line_box_rect: PhysicalRect<Au>,
415        at_stacking_context_root: bool,
416    ) {
417        // If this box establishes a stacking context or stacking container, do not paint
418        // it during this phase. Instead it is painted when the stacking context or container
419        // is processed.
420        if !at_stacking_context_root && box_fragment.stacking_context_type().is_some() {
421            return;
422        }
423
424        // Block-in-inline split, return to block mode.
425        let is_atomic_inline_level = box_fragment.is_atomic_inline_level();
426        if !is_atomic_inline_level && !box_fragment.is_inline_box() {
427            self.traverse_line_boxes_and_replaced_for_box(
428                state,
429                box_fragment,
430                at_stacking_context_root,
431            );
432            return;
433        }
434
435        // > Step 1: Paint the backgrounds of root’s fragments that are in line box into canvas.
436        // > Step 2: Paint the borders of root’s fragments that are in line box into canvas.
437        self.handle_box(state, box_fragment);
438
439        // > Step 3:
440        //
441        // Note: The following steps are in a different order than the specification
442        // due to the way we classify fragments.
443        //
444        // > ↪ If root is an inline-level replaced element
445        // >   Paint the replaced content into canvas, atomically.
446        if box_fragment.is_replaced() {
447            let state = state.push_box_fragment(box_fragment);
448            self.traverse_replaced_content(&state, box_fragment);
449
450        // > ↪ If root is an inline-level block or table wrapper box
451        // >   Paint a stacking container given root and canvas.
452        } else if is_atomic_inline_level || box_fragment.is_flex_or_grid_item() {
453            self.traverse_stacking_container(state, box_fragment, false /* is_block_level */);
454
455        // > ↪ If root is an inline box
456        // Note: This is handled via recursion into `paint_a_fragment_in_a_line_box`.
457        } else {
458            let state = state.push_box_fragment(box_fragment);
459            for child in &box_fragment.children {
460                self.traverse_fragment_in_a_line_box(&state, child, line_box_rect);
461            }
462        }
463    }
464
465    fn traverse_replaced_content(
466        &mut self,
467        state: &TraversalState,
468        box_fragment: &Arc<BoxFragment>,
469    ) {
470        for child in &box_fragment.children {
471            match child {
472                Fragment::LayoutRoot(layout_root_fragment) => {
473                    self.traverse_stacking_container(
474                        &state.without_text_decorations(),
475                        &layout_root_fragment.inner_box_fragment().with_style(),
476                        true, /* is_block_level */
477                    );
478                },
479                Fragment::Image(image_fragment) => {
480                    let containing_block =
481                        PhysicalRect::new(state.origin, box_fragment.content_rect().size);
482                    self.handler
483                        .visit_image(state, containing_block, image_fragment);
484                },
485                Fragment::IFrame(iframe_fragment) => {
486                    self.handler.visit_iframe(state, iframe_fragment);
487                },
488                Fragment::Box(box_fragment) => {
489                    self.traverse_stacking_container(
490                        &state.without_text_decorations(),
491                        &box_fragment.with_style(),
492                        true, /* is_block_level */
493                    );
494                },
495                _ => {},
496            }
497        }
498    }
499
500    fn handle_box(&mut self, state: &TraversalState, fragment: &BoxFragmentWithStyle<'_>) {
501        if fragment.has_outline() {
502            self.outlines
503                .push((state.clone(), fragment.box_fragment.clone()));
504        }
505        self.handler.visit_box(state, fragment);
506    }
507}
508
509pub(crate) trait PaintTraversalHandler {
510    type StackingContextState;
511
512    fn visit_stacking_context(
513        &mut self,
514        stacking_context: &StackingContext,
515    ) -> Self::StackingContextState;
516    fn leave_stacking_context(
517        &mut self,
518        state: &TraversalState,
519        stacking_context_state: Self::StackingContextState,
520    );
521
522    fn visit_box(&mut self, state: &TraversalState, fragment: &BoxFragmentWithStyle<'_>);
523    fn visit_iframe(&mut self, _state: &TraversalState, _fragment: &Arc<IFrameFragment>) {}
524    fn visit_image(
525        &mut self,
526        _state: &TraversalState,
527        _containing_block: PhysicalRect<Au>,
528        _fragment: &Arc<ImageFragment>,
529    ) {
530    }
531    fn visit_text(
532        &mut self,
533        state: &TraversalState,
534        line_box_rect: PhysicalRect<Au>,
535        fragment: &Arc<TextFragment>,
536    );
537    fn visit_positioning(&mut self, _state: &TraversalState, _fragment: &Arc<PositioningFragment>) {
538    }
539
540    fn visit_box_for_root_background(&mut self, _state: &TraversalState) {}
541    fn visit_box_for_outline(&mut self, _state: &TraversalState, _fragment: &Arc<BoxFragment>) {}
542    fn visit_box_for_collapsed_table_borders(
543        &mut self,
544        _state: &TraversalState,
545        _fragment: &BoxFragmentWithStyle<'_>,
546    ) {
547    }
548}
549
550#[derive(Clone, Debug, Default)]
551pub(crate) struct TraversalState {
552    pub spatial_id: ScrollTreeNodeId,
553    pub clip_id: ClipId,
554    pub origin: PhysicalPoint<Au>,
555    pub text_decorations: Rc<Vec<FragmentTextDecoration>>,
556    /// The tag of the nearest ancestor box fragment that has a tag.
557    /// Used for text LCP candidate grouping — all text fragments within
558    /// a single element are unioned before computing effective visual size.
559    pub containing_element_tag: Option<Tag>,
560}
561
562impl TraversalState {
563    pub(crate) fn push_box_fragment(&self, box_fragment: &BoxFragmentWithStyle<'_>) -> Self {
564        let style = box_fragment.style();
565
566        // Text decorations are not propagated to atomic inline-level descendants.
567        // From https://drafts.csswg.org/css2/#lining-striking-props:
568        //
569        // > Note that text decorations are not propagated to floating and absolutely
570        // > positioned descendants, nor to the contents of atomic inline-level descendants
571        // > such as inline blocks and inline tables.
572        //
573        // Also do not propagate text decorations to floats or replaced content.
574        let mut propagated_text_decorations = self.text_decorations.clone();
575        if box_fragment.is_atomic_inline_level() ||
576            box_fragment.base.flags.contains(
577                FragmentFlags::IS_OUTSIDE_LIST_ITEM_MARKER | FragmentFlags::IS_REPLACED,
578            )
579        {
580            propagated_text_decorations = Default::default();
581        }
582
583        let text_decorations = match style.get_text_decoration_line() {
584            &TextDecorationLine::NONE => propagated_text_decorations,
585            line => {
586                let mut new_vector = (*propagated_text_decorations).clone();
587                let color = &style.get_inherited_text().color;
588                new_vector.push(FragmentTextDecoration {
589                    line: *line,
590                    color: style.get_text_decoration_color().resolve_to_absolute(color),
591                    style: style.slow_clone_text_decoration_style(),
592                    thickness: style.slow_clone_text_decoration_thickness(),
593                });
594                Rc::new(new_vector)
595            },
596        };
597
598        Self {
599            origin: self.origin + box_fragment.content_rect().origin.to_vector(),
600            spatial_id: box_fragment
601                .generated_scroll_tree_node_id()
602                .unwrap_or(self.spatial_id),
603            clip_id: box_fragment.generated_clip_id().unwrap_or(self.clip_id),
604            text_decorations,
605            containing_element_tag: box_fragment.base.tag.or(self.containing_element_tag),
606        }
607    }
608
609    pub(crate) fn without_text_decorations(&self) -> Self {
610        Self {
611            text_decorations: Default::default(),
612            ..*self
613        }
614    }
615
616    pub(crate) fn push_positioning_fragment(
617        &self,
618        positioning_fragment: &PositioningFragment,
619    ) -> Self {
620        Self {
621            origin: self.origin + positioning_fragment.base.rect().origin.to_vector(),
622            spatial_id: self.spatial_id,
623            clip_id: self.clip_id,
624            text_decorations: self.text_decorations.clone(),
625            containing_element_tag: self.containing_element_tag,
626        }
627    }
628
629    pub(crate) fn push_stacking_context(&self, stacking_context: &StackingContext) -> Self {
630        Self {
631            origin: stacking_context.containing_block_origin,
632            spatial_id: stacking_context.scroll_tree_node_id,
633            clip_id: stacking_context.clip_id,
634            text_decorations: stacking_context.text_decorations.clone(),
635            containing_element_tag: self.containing_element_tag,
636        }
637    }
638}