Skip to main content

layout/display_list/
clip.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::cell::LazyCell;
6
7use app_units::Au;
8use malloc_size_of_derive::MallocSizeOf;
9use servo_base::id::ScrollTreeNodeId;
10use style::values::computed::basic_shape::{BasicShape, ClipPath};
11use style::values::computed::position::Position;
12use style::values::computed::{Length, LengthPercentage};
13use style::values::generics::basic_shape::{GenericShapeRadius, ShapeBox, ShapeGeometryBox};
14use style::values::generics::position::GenericPositionOrAuto;
15use webrender_api::BorderRadius;
16use webrender_api::units::{LayoutPoint, LayoutRect, LayoutSideOffsets, LayoutSize};
17
18use super::{BuilderForBoxFragment, compute_margin_box_radius};
19use crate::fragment_tree::BoxFragmentWithStyle;
20use crate::geom::PhysicalPoint;
21
22/// An identifier for a clip used during StackingContextTree construction. This is a simple index in
23/// a [`ClipStore`]s vector of clips.
24#[derive(Clone, Copy, Debug, Eq, Hash, MallocSizeOf, PartialEq)]
25pub(crate) struct ClipId(pub usize);
26
27impl ClipId {
28    /// Equivalent to [`ClipChainId::INVALID`]. This means "no clip."
29    pub(crate) const INVALID: ClipId = ClipId(usize::MAX);
30}
31
32impl Default for ClipId {
33    fn default() -> Self {
34        Self::INVALID
35    }
36}
37
38/// All the information needed to create a clip on a WebRender display list. These are created at
39/// two times: during `StackingContextTree` creation and during WebRender display list construction.
40/// Only the former are stored in a [`ClipStore`].
41#[derive(Clone, MallocSizeOf)]
42pub(crate) struct Clip {
43    pub id: ClipId,
44    pub radii: BorderRadius,
45    pub rect: LayoutRect,
46    pub parent_scroll_node_id: ScrollTreeNodeId,
47    pub parent_clip_id: ClipId,
48}
49
50/// A simple vector of [`Clip`] that is built during `StackingContextTree` construction.
51/// These are later turned into WebRender clips and clip chains during WebRender display
52/// list construction.
53#[derive(Clone, Default, MallocSizeOf)]
54pub(crate) struct StackingContextTreeClipStore(pub Vec<Clip>);
55
56impl StackingContextTreeClipStore {
57    pub(super) fn get(&self, clip_id: ClipId) -> &Clip {
58        &self.0[clip_id.0]
59    }
60
61    pub(crate) fn add(
62        &mut self,
63        radii: webrender_api::BorderRadius,
64        rect: LayoutRect,
65        parent_scroll_node_id: ScrollTreeNodeId,
66        parent_clip_id: ClipId,
67    ) -> ClipId {
68        let id = ClipId(self.0.len());
69        self.0.push(Clip {
70            id,
71            radii,
72            rect,
73            parent_scroll_node_id,
74            parent_clip_id,
75        });
76        id
77    }
78
79    pub(super) fn add_for_clip_path(
80        &mut self,
81        clip_path: &ClipPath,
82        parent_scroll_node_id: ScrollTreeNodeId,
83        parent_clip_chain_id: ClipId,
84        box_fragment: &BoxFragmentWithStyle<'_>,
85        containing_block_origin: PhysicalPoint<Au>,
86    ) -> Option<ClipId> {
87        let geometry_box = match clip_path {
88            ClipPath::Shape(_, ShapeGeometryBox::ShapeBox(shape_box)) => *shape_box,
89            ClipPath::Shape(_, ShapeGeometryBox::ElementDependent) => ShapeBox::BorderBox,
90            ClipPath::Box(ShapeGeometryBox::ShapeBox(shape_box)) => *shape_box,
91            ClipPath::Box(ShapeGeometryBox::ElementDependent) => ShapeBox::BorderBox,
92            _ => return None,
93        };
94        let fragment_builder = BuilderForBoxFragment::new(box_fragment, containing_block_origin);
95        let layout_rect = match geometry_box {
96            ShapeBox::BorderBox => fragment_builder.border_rect,
97            ShapeBox::ContentBox => *fragment_builder.content_rect(),
98            ShapeBox::PaddingBox => *fragment_builder.padding_rect(),
99            ShapeBox::MarginBox => *fragment_builder.margin_rect(),
100        };
101        if let ClipPath::Shape(shape, _) = clip_path {
102            match **shape {
103                BasicShape::Circle(_) | BasicShape::Ellipse(_) | BasicShape::Rect(_) => self
104                    .add_for_basic_shape(
105                        shape,
106                        layout_rect,
107                        parent_scroll_node_id,
108                        parent_clip_chain_id,
109                    ),
110                BasicShape::Polygon(_) | BasicShape::PathOrShape(_) => None,
111            }
112        } else {
113            Some(self.add(
114                match geometry_box {
115                    ShapeBox::MarginBox => compute_margin_box_radius(
116                        fragment_builder.border_radius(),
117                        layout_rect.size(),
118                        fragment_builder.fragment,
119                    ),
120                    _ => fragment_builder.border_radius(),
121                },
122                layout_rect,
123                parent_scroll_node_id,
124                parent_clip_chain_id,
125            ))
126        }
127    }
128
129    #[servo_tracing::instrument(name = "StackingContextClipStore::add_for_basic_shape", skip_all)]
130    fn add_for_basic_shape(
131        &mut self,
132        shape: &BasicShape,
133        layout_box: LayoutRect,
134        parent_scroll_node_id: ScrollTreeNodeId,
135        parent_clip_chain_id: ClipId,
136    ) -> Option<ClipId> {
137        match shape {
138            BasicShape::Rect(rect) => {
139                let box_height = Length::new(layout_box.height());
140                let box_width = Length::new(layout_box.width());
141                let insets = LayoutSideOffsets::new(
142                    rect.rect.0.resolve(box_height).px(),
143                    rect.rect.1.resolve(box_width).px(),
144                    rect.rect.2.resolve(box_height).px(),
145                    rect.rect.3.resolve(box_width).px(),
146                );
147
148                // `inner_rect()` will cause an assertion failure if the insets are larger than the
149                // rectangle dimension.
150                let shape_rect = if insets.left + insets.right >= layout_box.width() ||
151                    insets.top + insets.bottom > layout_box.height()
152                {
153                    LayoutRect::from_origin_and_size(layout_box.min, LayoutSize::zero())
154                } else {
155                    layout_box.to_rect().inner_rect(insets).to_box2d()
156                };
157
158                let corner = |corner: &style::values::computed::BorderCornerRadius| {
159                    LayoutSize::new(
160                        corner.0.width.0.resolve(box_width).px(),
161                        corner.0.height.0.resolve(box_height).px(),
162                    )
163                };
164                let radii = webrender_api::BorderRadius {
165                    top_left: corner(&rect.round.top_left),
166                    top_right: corner(&rect.round.top_right),
167                    bottom_left: corner(&rect.round.bottom_left),
168                    bottom_right: corner(&rect.round.bottom_right),
169                };
170                Some(self.add(
171                    radii,
172                    shape_rect,
173                    parent_scroll_node_id,
174                    parent_clip_chain_id,
175                ))
176            },
177            BasicShape::Circle(circle) => {
178                let center = match &circle.position {
179                    GenericPositionOrAuto::Position(position) => position.clone(),
180                    GenericPositionOrAuto::Auto => Position::center(),
181                };
182                let anchor_x = center.horizontal.resolve(Length::new(layout_box.width()));
183                let anchor_y = center.vertical.resolve(Length::new(layout_box.height()));
184                let center = layout_box
185                    .min
186                    .add_size(&LayoutSize::new(anchor_x.px(), anchor_y.px()));
187
188                let radius = compute_shape_radius_for_circle(
189                    center,
190                    &circle.radius,
191                    layout_box.min,
192                    layout_box.max,
193                );
194                let radius = LayoutSize::new(radius, radius);
195                let radii = webrender_api::BorderRadius {
196                    top_left: radius,
197                    top_right: radius,
198                    bottom_left: radius,
199                    bottom_right: radius,
200                };
201                let start = center.add_size(&-radius);
202                let rect = LayoutRect::from_origin_and_size(start, radius * 2.);
203                Some(self.add(radii, rect, parent_scroll_node_id, parent_clip_chain_id))
204            },
205            BasicShape::Ellipse(ellipse) => {
206                let center = match &ellipse.position {
207                    GenericPositionOrAuto::Position(position) => position.clone(),
208                    GenericPositionOrAuto::Auto => Position::center(),
209                };
210                let anchor_x = center.horizontal.resolve(Length::new(layout_box.width()));
211                let anchor_y = center.vertical.resolve(Length::new(layout_box.height()));
212                let center = layout_box
213                    .min
214                    .add_size(&LayoutSize::new(anchor_x.px(), anchor_y.px()));
215
216                let radius = compute_shape_radius_for_ellipse(
217                    center,
218                    (&ellipse.semiaxis_x, &ellipse.semiaxis_y),
219                    layout_box.min,
220                    layout_box.max,
221                );
222
223                let radii = webrender_api::BorderRadius {
224                    top_left: radius,
225                    top_right: radius,
226                    bottom_left: radius,
227                    bottom_right: radius,
228                };
229                let start = center.add_size(&-radius);
230                let rect = LayoutRect::from_origin_and_size(start, radius * 2.);
231                Some(self.add(radii, rect, parent_scroll_node_id, parent_clip_chain_id))
232            },
233            _ => None,
234        }
235    }
236}
237
238fn compute_shape_radius_for_circle(
239    center: LayoutPoint,
240    radius: &GenericShapeRadius<LengthPercentage>,
241    min_edge: LayoutPoint,
242    max_edge: LayoutPoint,
243) -> f32 {
244    let distance_from_min_edge = (min_edge - center).abs();
245    let distance_from_max_edge = (max_edge - center).abs();
246    match radius {
247        GenericShapeRadius::FarthestSide => {
248            let x = distance_from_min_edge.x.max(distance_from_max_edge.x);
249            let y = distance_from_min_edge.y.max(distance_from_max_edge.y);
250            x.max(y)
251        },
252        GenericShapeRadius::ClosestSide => {
253            let x = distance_from_min_edge.x.min(distance_from_max_edge.x);
254            let y = distance_from_min_edge.y.min(distance_from_max_edge.y);
255            x.min(y)
256        },
257        GenericShapeRadius::FarthestCorner => {
258            let x = distance_from_min_edge.x.max(distance_from_max_edge.x);
259            let y = distance_from_min_edge.y.max(distance_from_max_edge.y);
260            x.hypot(y)
261        },
262        GenericShapeRadius::ClosestCorner => {
263            let x = distance_from_min_edge.x.min(distance_from_max_edge.x);
264            let y = distance_from_min_edge.y.min(distance_from_max_edge.y);
265            x.hypot(y)
266        },
267        GenericShapeRadius::Length(length) => {
268            // https://www.w3.org/TR/css-shapes/#direction-agnostic-size
269            let size = max_edge - min_edge;
270            let basis = size.x.hypot(size.y) / std::f32::consts::SQRT_2;
271            length.0.resolve(Length::new(basis)).px()
272        },
273    }
274}
275
276fn compute_shape_radius_for_ellipse(
277    center: LayoutPoint,
278    radius: (
279        &GenericShapeRadius<LengthPercentage>,
280        &GenericShapeRadius<LengthPercentage>,
281    ),
282    min_edge: LayoutPoint,
283    max_edge: LayoutPoint,
284) -> LayoutSize {
285    let distance_from_min_edge = (min_edge - center).abs();
286    let distance_from_max_edge = (max_edge - center).abs();
287
288    // FIXME: The logic for FarthestCorner and ClosestCorner matches other browsers,
289    // but it incorrectly assumes that the ellipse's aspect ratio should be 1:1.
290    // It's also not clear what to do when only one axis is set to a corner value,
291    // see <https://github.com/w3c/csswg-drafts/issues/14010>.
292    let farthest_corner = LazyCell::new(|| {
293        let x = distance_from_min_edge.x.max(distance_from_max_edge.x);
294        let y = distance_from_min_edge.y.max(distance_from_max_edge.y);
295        x.hypot(y)
296    });
297    let closest_corner = LazyCell::new(|| {
298        let x = distance_from_min_edge.x.min(distance_from_max_edge.x);
299        let y = distance_from_min_edge.y.min(distance_from_max_edge.y);
300        x.hypot(y)
301    });
302
303    let radius_x = match radius.0 {
304        GenericShapeRadius::FarthestSide => distance_from_min_edge.x.max(distance_from_max_edge.x),
305        GenericShapeRadius::ClosestSide => distance_from_min_edge.x.min(distance_from_max_edge.x),
306        GenericShapeRadius::FarthestCorner => *farthest_corner,
307        GenericShapeRadius::ClosestCorner => *closest_corner,
308        GenericShapeRadius::Length(length) => {
309            length.0.resolve(Length::new(max_edge.x - min_edge.x)).px()
310        },
311    };
312    let radius_y = match radius.1 {
313        GenericShapeRadius::FarthestSide => distance_from_min_edge.y.max(distance_from_max_edge.y),
314        GenericShapeRadius::ClosestSide => distance_from_min_edge.y.min(distance_from_max_edge.y),
315        GenericShapeRadius::FarthestCorner => *farthest_corner,
316        GenericShapeRadius::ClosestCorner => *closest_corner,
317        GenericShapeRadius::Length(length) => {
318            length.0.resolve(Length::new(max_edge.y - min_edge.y)).px()
319        },
320    };
321    LayoutSize::new(radius_x, radius_y)
322}