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, 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            self.traverse_box_in_a_line_box(state, root, true /* at_stacking_context_root */);
154        } else if saw_inline_level_or_replaced {
155            // >  ↪ Otherwise
156            // >    First for root, then for all its in-flow, non-positioned, block-level
157            // >    descendant boxes, in tree order:
158            // >    1. If the box is a replaced element, paint the replaced content into canvas,
159            // >       atomically.
160            // >    2. Otherwise, for each line box of the box, paint a box in a line box given the
161            // >       box, the line box, and canvas.
162            // >    3. If the UA uses in-band outlines, paint the outlines of the box into canvas.
163            self.traverse_line_boxes_and_replaced_for_box(
164                state, root, true, /* at_root_of_stacking_context */
165            );
166        }
167    }
168
169    /// An implementation of
170    /// <https://drafts.csswg.org/css-position-4/#paint-a-stacking-context> that only
171    /// implements the parts relevant to stacking containers. This is an optimization to
172    /// avoid work when descending into positioned container and stacking context
173    /// contents.
174    fn traverse_stacking_container(
175        &mut self,
176        state: &TraversalState,
177        root: &BoxFragmentWithStyle<'_>,
178        is_block_level: bool,
179    ) {
180        let old_outlines_length = self.outlines.len();
181
182        // > Step 4: If root is a block-level box, paint a block’s decorations given root
183        // > and canvas.
184        if is_block_level {
185            self.handle_box(state, root);
186        }
187
188        // This is steps 6 through 8.
189        self.traverse_stacking_context_inner(state, root);
190
191        // > Step 11: If the UA uses out-of-band outlines, draw all of root’s outlines
192        // > (those that it skipped drawing due to not using in-band outlines during the
193        // > current invocation of this algorithm) into canvas.
194        if old_outlines_length < self.outlines.len() {
195            for (state, outline_fragment) in &self.outlines.split_off(old_outlines_length) {
196                self.handler.visit_box_for_outline(state, outline_fragment);
197            }
198        }
199    }
200
201    fn traverse_block_level_descendants_decorations(
202        &mut self,
203        state: &TraversalState,
204        fragment: &Fragment,
205    ) -> bool {
206        let mut saw_inline_level_or_replaced = false;
207
208        match fragment {
209            Fragment::LayoutRoot(layout_root_fragment) => {
210                saw_inline_level_or_replaced = self.traverse_block_level_descendants_decorations(
211                    state,
212                    &layout_root_fragment.inner(),
213                );
214            },
215            Fragment::Box(box_fragment) => {
216                let box_fragment = &box_fragment.with_style();
217                // If this box establishes a stacking context or stacking container, do not paint
218                // it during this phase. Instead it is painted when the stacking context or container
219                // is processed.
220                if box_fragment.stacking_context_type().is_some() {
221                    return false;
222                }
223
224                // We will process inline atomics during the inline level and replaced traversal.
225                if box_fragment.is_atomic_inline_level() || box_fragment.is_flex_or_grid_item() {
226                    return true;
227                }
228
229                // Don't paint any inline boxes, but do descend into them, in case they contain floats.
230                if box_fragment.is_inline_box() {
231                    saw_inline_level_or_replaced = true;
232                } else {
233                    self.handle_box(state, box_fragment);
234                }
235
236                if box_fragment.is_replaced() {
237                    return true;
238                }
239
240                let state_for_children = state.push_box_fragment(box_fragment);
241                for child in box_fragment.children.iter() {
242                    saw_inline_level_or_replaced |= self
243                        .traverse_block_level_descendants_decorations(&state_for_children, child);
244                }
245
246                // Collapsed table borders are painted after block-level descendants. This isn't
247                // well specified, but is being discussed in <https://github.com/w3c/csswg-drafts/issues/11570>.
248                if box_fragment.is_table_grid_with_collapsed_borders() {
249                    self.handler
250                        .visit_box_for_collapsed_table_borders(state, box_fragment);
251                }
252            },
253            Fragment::Float(float_box_fragment) => {
254                if float_box_fragment.stacking_context_type().is_none() {
255                    self.floats
256                        .push((state.without_text_decorations(), float_box_fragment.clone()));
257                }
258            },
259            Fragment::Positioning(positioning_fragment) => {
260                self.handler.visit_positioning(state, positioning_fragment);
261
262                if positioning_fragment.is_line_box() {
263                    saw_inline_level_or_replaced = true;
264                }
265
266                if !positioning_fragment.children.is_empty() {
267                    let state = state.push_positioning_fragment(positioning_fragment);
268                    for child in positioning_fragment.children.iter() {
269                        saw_inline_level_or_replaced |=
270                            self.traverse_block_level_descendants_decorations(&state, child);
271                    }
272                }
273            },
274            Fragment::AbsoluteOrFixedPositionedPlaceholder(..) |
275            Fragment::Text(..) |
276            Fragment::Image(..) |
277            Fragment::IFrame(..) => {},
278        }
279
280        saw_inline_level_or_replaced
281    }
282
283    fn traverse_line_boxes_and_replaced_for_box(
284        &mut self,
285        state: &TraversalState,
286        fragment: &BoxFragmentWithStyle<'_>,
287        at_root_of_stacking_context: bool,
288    ) {
289        let is_flex_or_grid = fragment.is_flex_or_grid_item();
290        if fragment.is_replaced() {
291            if is_flex_or_grid {
292                self.handle_box(state, fragment);
293            }
294
295            let inner_state = state.push_box_fragment(fragment);
296            self.traverse_replaced_content(&inner_state, fragment);
297            return;
298        }
299
300        if !at_root_of_stacking_context && is_flex_or_grid {
301            self.traverse_stacking_container(state, fragment, true /* is_block_level */);
302            return;
303        }
304
305        let inner_state = state.push_box_fragment(fragment);
306        for child in fragment.children.iter() {
307            self.traverse_line_boxes_and_replaced(
308                &inner_state,
309                child,
310                false, /* at_root_of_stacking_context */
311            );
312        }
313    }
314
315    fn traverse_line_boxes_and_replaced(
316        &mut self,
317        state: &TraversalState,
318        fragment: &Fragment,
319        at_root_of_stacking_context: bool,
320    ) {
321        match fragment {
322            Fragment::LayoutRoot(layout_root_fragment) => self.traverse_line_boxes_and_replaced(
323                state,
324                &layout_root_fragment.inner(),
325                at_root_of_stacking_context,
326            ),
327            Fragment::Box(box_fragment) => {
328                // If this box establishes a stacking context or stacking container, do not paint
329                // it during this phase. Instead it is painted when the stacking context or container
330                // is processed.
331                if box_fragment.stacking_context_type().is_some() {
332                    return;
333                }
334                self.traverse_line_boxes_and_replaced_for_box(
335                    state,
336                    &box_fragment.with_style(),
337                    at_root_of_stacking_context,
338                );
339            },
340            Fragment::Positioning(positioning_fragment) if positioning_fragment.is_line_box() => {
341                let state = state.push_positioning_fragment(positioning_fragment);
342                for child in &positioning_fragment.children {
343                    self.traverse_fragment_in_a_line_box(&state, child);
344                }
345            },
346            Fragment::Positioning(positioning_fragment) => {
347                if !positioning_fragment.children.is_empty() {
348                    let state = state.push_positioning_fragment(positioning_fragment);
349                    for child in &positioning_fragment.children {
350                        self.traverse_line_boxes_and_replaced(
351                            &state, child, false, /* at at_root_of_stacking_context */
352                        );
353                    }
354                }
355            },
356            Fragment::AbsoluteOrFixedPositionedPlaceholder(_) |
357            Fragment::Float(..) |
358            Fragment::IFrame(_) |
359            Fragment::Image(_) |
360            Fragment::Text(..) => {},
361        }
362    }
363
364    fn traverse_fragment_in_a_line_box(&mut self, state: &TraversalState, fragment: &Fragment) {
365        match fragment {
366            Fragment::LayoutRoot(layout_root_fragment) => {
367                self.traverse_fragment_in_a_line_box(state, &layout_root_fragment.inner())
368            },
369            Fragment::Box(box_fragment) => self.traverse_box_in_a_line_box(
370                state,
371                &box_fragment.with_style(),
372                false, /* at_stacking_context_root */
373            ),
374            Fragment::Text(text_fragment) => {
375                // This containing block is wrong and should use the size from the parent
376                // positioning context.
377                let containing_block =
378                    PhysicalRect::new(state.origin, text_fragment.base.rect().size);
379                self.handler
380                    .visit_text(state, containing_block, text_fragment);
381            },
382            Fragment::AbsoluteOrFixedPositionedPlaceholder(..) | Fragment::Float(..) => {},
383            Fragment::Positioning(..) => {
384                unreachable!("Unexpected direct descendant PositioningContext of inline.")
385            },
386            Fragment::Image(..) | Fragment::IFrame(..) => {
387                unreachable!("Unexpected replaced content direct descendant of inline.")
388            },
389        }
390    }
391
392    /// <https://www.w3.org/TR/css-position-4/#paint-a-box-in-a-line-box>
393    fn traverse_box_in_a_line_box(
394        &mut self,
395        state: &TraversalState,
396        box_fragment: &BoxFragmentWithStyle<'_>,
397        at_stacking_context_root: bool,
398    ) {
399        // If this box establishes a stacking context or stacking container, do not paint
400        // it during this phase. Instead it is painted when the stacking context or container
401        // is processed.
402        if !at_stacking_context_root && box_fragment.stacking_context_type().is_some() {
403            return;
404        }
405
406        // Block-in-inline split, return to block mode.
407        let is_atomic_inline_level = box_fragment.is_atomic_inline_level();
408        if !is_atomic_inline_level && !box_fragment.is_inline_box() {
409            self.traverse_line_boxes_and_replaced_for_box(
410                state,
411                box_fragment,
412                at_stacking_context_root,
413            );
414            return;
415        }
416
417        // > Step 1: Paint the backgrounds of root’s fragments that are in line box into canvas.
418        // > Step 2: Paint the borders of root’s fragments that are in line box into canvas.
419        self.handle_box(state, box_fragment);
420
421        // > Step 3:
422        //
423        // Note: The following steps are in a different order than the specification
424        // due to the way we classify fragments.
425        //
426        // > ↪ If root is an inline-level replaced element
427        // >   Paint the replaced content into canvas, atomically.
428        if box_fragment.is_replaced() {
429            let state = state.push_box_fragment(box_fragment);
430            self.traverse_replaced_content(&state, box_fragment);
431
432        // > ↪ If root is an inline-level block or table wrapper box
433        // >   Paint a stacking container given root and canvas.
434        } else if is_atomic_inline_level || box_fragment.is_flex_or_grid_item() {
435            self.traverse_stacking_container(state, box_fragment, false /* is_block_level */);
436
437        // > ↪ If root is an inline box
438        // Note: This is handled via recursion into `paint_a_fragment_in_a_line_box`.
439        } else {
440            let state = state.push_box_fragment(box_fragment);
441            for child in &box_fragment.children {
442                self.traverse_fragment_in_a_line_box(&state, child);
443            }
444        }
445    }
446
447    fn traverse_replaced_content(
448        &mut self,
449        state: &TraversalState,
450        box_fragment: &Arc<BoxFragment>,
451    ) {
452        for child in &box_fragment.children {
453            match child {
454                Fragment::LayoutRoot(layout_root_fragment) => {
455                    self.traverse_stacking_container(
456                        &state.without_text_decorations(),
457                        &layout_root_fragment.inner_box_fragment().with_style(),
458                        true, /* is_block_level */
459                    );
460                },
461                Fragment::Image(image_fragment) => {
462                    let containing_block =
463                        PhysicalRect::new(state.origin, box_fragment.content_rect().size);
464                    self.handler
465                        .visit_image(state, containing_block, image_fragment);
466                },
467                Fragment::IFrame(iframe_fragment) => {
468                    self.handler.visit_iframe(state, iframe_fragment);
469                },
470                Fragment::Box(box_fragment) => {
471                    self.traverse_stacking_container(
472                        &state.without_text_decorations(),
473                        &box_fragment.with_style(),
474                        true, /* is_block_level */
475                    );
476                },
477                _ => {},
478            }
479        }
480    }
481
482    fn handle_box(&mut self, state: &TraversalState, fragment: &BoxFragmentWithStyle<'_>) {
483        if fragment.has_outline() {
484            self.outlines
485                .push((state.clone(), fragment.box_fragment.clone()));
486        }
487        self.handler.visit_box(state, fragment);
488    }
489}
490
491pub(crate) trait PaintTraversalHandler {
492    type StackingContextState;
493
494    fn visit_stacking_context(
495        &mut self,
496        stacking_context: &StackingContext,
497    ) -> Self::StackingContextState;
498    fn leave_stacking_context(
499        &mut self,
500        state: &TraversalState,
501        stacking_context_state: Self::StackingContextState,
502    );
503
504    fn visit_box(&mut self, state: &TraversalState, fragment: &BoxFragmentWithStyle<'_>);
505    fn visit_iframe(&mut self, _state: &TraversalState, _fragment: &Arc<IFrameFragment>) {}
506    fn visit_image(
507        &mut self,
508        _state: &TraversalState,
509        _containing_block: PhysicalRect<Au>,
510        _fragment: &Arc<ImageFragment>,
511    ) {
512    }
513    fn visit_text(
514        &mut self,
515        state: &TraversalState,
516        containing_block: PhysicalRect<Au>,
517        fragment: &Arc<TextFragment>,
518    );
519    fn visit_positioning(&mut self, _state: &TraversalState, _fragment: &Arc<PositioningFragment>) {
520    }
521
522    fn visit_box_for_root_background(&mut self, _state: &TraversalState) {}
523    fn visit_box_for_outline(&mut self, _state: &TraversalState, _fragment: &Arc<BoxFragment>) {}
524    fn visit_box_for_collapsed_table_borders(
525        &mut self,
526        _state: &TraversalState,
527        _fragment: &BoxFragmentWithStyle<'_>,
528    ) {
529    }
530}
531
532#[derive(Clone, Debug, Default)]
533pub(crate) struct TraversalState {
534    pub spatial_id: ScrollTreeNodeId,
535    pub clip_id: ClipId,
536    pub origin: PhysicalPoint<Au>,
537    pub text_decorations: Rc<Vec<FragmentTextDecoration>>,
538}
539
540impl TraversalState {
541    pub(crate) fn push_box_fragment(&self, box_fragment: &BoxFragmentWithStyle<'_>) -> Self {
542        let style = box_fragment.style();
543
544        // Text decorations are not propagated to atomic inline-level descendants.
545        // From https://drafts.csswg.org/css2/#lining-striking-props:
546        //
547        // > Note that text decorations are not propagated to floating and absolutely
548        // > positioned descendants, nor to the contents of atomic inline-level descendants
549        // > such as inline blocks and inline tables.
550        //
551        // Also do not propagate text decorations to floats or replaced content.
552        let mut propagated_text_decorations = self.text_decorations.clone();
553        if box_fragment.is_atomic_inline_level() ||
554            box_fragment.base.flags.contains(
555                FragmentFlags::IS_OUTSIDE_LIST_ITEM_MARKER | FragmentFlags::IS_REPLACED,
556            )
557        {
558            propagated_text_decorations = Default::default();
559        }
560
561        let text_decorations = match &style.get_text().text_decoration_line {
562            &TextDecorationLine::NONE => propagated_text_decorations,
563            line => {
564                let mut new_vector = (*propagated_text_decorations).clone();
565                let color = &style.get_inherited_text().color;
566                new_vector.push(FragmentTextDecoration {
567                    line: *line,
568                    color: style
569                        .clone_text_decoration_color()
570                        .resolve_to_absolute(color),
571                    style: style.clone_text_decoration_style(),
572                });
573                Rc::new(new_vector)
574            },
575        };
576
577        Self {
578            origin: self.origin + box_fragment.content_rect().origin.to_vector(),
579            spatial_id: box_fragment
580                .generated_scroll_tree_node_id()
581                .unwrap_or(self.spatial_id),
582            clip_id: box_fragment.generated_clip_id().unwrap_or(self.clip_id),
583            text_decorations,
584        }
585    }
586
587    pub(crate) fn without_text_decorations(&self) -> Self {
588        Self {
589            text_decorations: Default::default(),
590            ..*self
591        }
592    }
593
594    pub(crate) fn push_positioning_fragment(
595        &self,
596        positioning_fragment: &PositioningFragment,
597    ) -> Self {
598        Self {
599            origin: self.origin + positioning_fragment.base.rect().origin.to_vector(),
600            spatial_id: self.spatial_id,
601            clip_id: self.clip_id,
602            text_decorations: self.text_decorations.clone(),
603        }
604    }
605
606    pub(crate) fn push_stacking_context(&self, stacking_context: &StackingContext) -> Self {
607        Self {
608            origin: stacking_context.containing_block_origin,
609            spatial_id: stacking_context.scroll_tree_node_id,
610            clip_id: stacking_context.clip_id,
611            text_decorations: stacking_context.text_decorations.clone(),
612        }
613    }
614}