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 http://mozilla.org/MPL/2.0/. */
4 
5 use api::{BorderRadius, BoxShadowClipMode, ClipMode, ColorF, PrimitiveKeyKind};
6 use api::PropertyBinding;
7 use api::units::*;
8 use crate::clip::{ClipItemKey, ClipItemKeyKind, ClipChainId};
9 use crate::scene_building::SceneBuilder;
10 use crate::spatial_tree::SpatialNodeIndex;
11 use crate::gpu_types::BoxShadowStretchMode;
12 use crate::render_task_graph::RenderTaskId;
13 use crate::internal_types::LayoutPrimitiveInfo;
14 
15 #[derive(Debug, Clone, MallocSizeOf)]
16 #[cfg_attr(feature = "capture", derive(Serialize))]
17 #[cfg_attr(feature = "replay", derive(Deserialize))]
18 pub struct BoxShadowClipSource {
19     // Parameters that define the shadow and are constant.
20     pub shadow_radius: BorderRadius,
21     pub blur_radius: f32,
22     pub clip_mode: BoxShadowClipMode,
23     pub stretch_mode_x: BoxShadowStretchMode,
24     pub stretch_mode_y: BoxShadowStretchMode,
25 
26     // The current cache key (in device-pixels), and handles
27     // to the cached clip region and blurred texture.
28     pub cache_key: Option<(DeviceIntSize, BoxShadowCacheKey)>,
29     pub render_task: Option<RenderTaskId>,
30 
31     // Local-space size of the required render task size.
32     pub shadow_rect_alloc_size: LayoutSize,
33 
34     // Local-space size of the required render task size without any downscaling
35     // applied. This is needed to stretch the shadow properly.
36     pub original_alloc_size: LayoutSize,
37 
38     // The minimal shadow rect for the parameters above,
39     // used when drawing the shadow rect to be blurred.
40     pub minimal_shadow_rect: LayoutRect,
41 
42     // Local space rect for the shadow to be drawn or
43     // stretched in the shadow primitive.
44     pub prim_shadow_rect: LayoutRect,
45 }
46 
47 // The blur shader samples BLUR_SAMPLE_SCALE * blur_radius surrounding texels.
48 pub const BLUR_SAMPLE_SCALE: f32 = 3.0;
49 
50 // Maximum blur radius for box-shadows (different than blur filters).
51 // Taken from nsCSSRendering.cpp in Gecko.
52 pub const MAX_BLUR_RADIUS: f32 = 300.;
53 
54 // A cache key that uniquely identifies a minimally sized
55 // and blurred box-shadow rect that can be stored in the
56 // texture cache and applied to clip-masks.
57 #[derive(Debug, Clone, Eq, Hash, MallocSizeOf, PartialEq)]
58 #[cfg_attr(feature = "capture", derive(Serialize))]
59 #[cfg_attr(feature = "replay", derive(Deserialize))]
60 pub struct BoxShadowCacheKey {
61     pub blur_radius_dp: i32,
62     pub clip_mode: BoxShadowClipMode,
63     // NOTE(emilio): Only the original allocation size needs to be in the cache
64     // key, since the actual size is derived from that.
65     pub original_alloc_size: DeviceIntSize,
66     pub br_top_left: DeviceIntSize,
67     pub br_top_right: DeviceIntSize,
68     pub br_bottom_right: DeviceIntSize,
69     pub br_bottom_left: DeviceIntSize,
70     pub device_pixel_scale: Au,
71 }
72 
73 impl<'a> SceneBuilder<'a> {
add_box_shadow( &mut self, spatial_node_index: SpatialNodeIndex, clip_chain_id: ClipChainId, prim_info: &LayoutPrimitiveInfo, box_offset: &LayoutVector2D, color: ColorF, mut blur_radius: f32, spread_radius: f32, border_radius: BorderRadius, clip_mode: BoxShadowClipMode, )74     pub fn add_box_shadow(
75         &mut self,
76         spatial_node_index: SpatialNodeIndex,
77         clip_chain_id: ClipChainId,
78         prim_info: &LayoutPrimitiveInfo,
79         box_offset: &LayoutVector2D,
80         color: ColorF,
81         mut blur_radius: f32,
82         spread_radius: f32,
83         border_radius: BorderRadius,
84         clip_mode: BoxShadowClipMode,
85     ) {
86         if color.a == 0.0 {
87             return;
88         }
89 
90         // Inset shadows get smaller as spread radius increases.
91         let (spread_amount, prim_clip_mode) = match clip_mode {
92             BoxShadowClipMode::Outset => (spread_radius, ClipMode::ClipOut),
93             BoxShadowClipMode::Inset => (-spread_radius, ClipMode::Clip),
94         };
95 
96         // Ensure the blur radius is somewhat sensible.
97         blur_radius = f32::min(blur_radius, MAX_BLUR_RADIUS);
98 
99         // Adjust the border radius of the box shadow per CSS-spec.
100         let shadow_radius = adjust_border_radius_for_box_shadow(border_radius, spread_amount);
101 
102         // Apply parameters that affect where the shadow rect
103         // exists in the local space of the primitive.
104         let shadow_rect = self.snap_rect(
105             &prim_info
106                 .rect
107                 .translate(*box_offset)
108                 .inflate(spread_amount, spread_amount),
109             spatial_node_index,
110         );
111 
112         // If blur radius is zero, we can use a fast path with
113         // no blur applied.
114         if blur_radius == 0.0 {
115             // Trivial reject of box-shadows that are not visible.
116             if box_offset.x == 0.0 && box_offset.y == 0.0 && spread_amount == 0.0 {
117                 return;
118             }
119 
120             let mut clips = Vec::with_capacity(2);
121             let (final_prim_rect, clip_radius) = match clip_mode {
122                 BoxShadowClipMode::Outset => {
123                     if shadow_rect.is_empty() {
124                         return;
125                     }
126 
127                     // TODO(gw): Add a fast path for ClipOut + zero border radius!
128                     clips.push(ClipItemKey {
129                         kind: ClipItemKeyKind::rounded_rect(
130                             prim_info.rect,
131                             border_radius,
132                             ClipMode::ClipOut,
133                         ),
134                         spatial_node_index,
135                     });
136 
137                     (shadow_rect, shadow_radius)
138                 }
139                 BoxShadowClipMode::Inset => {
140                     if !shadow_rect.is_empty() {
141                         clips.push(ClipItemKey {
142                             kind: ClipItemKeyKind::rounded_rect(
143                                 shadow_rect,
144                                 shadow_radius,
145                                 ClipMode::ClipOut,
146                             ),
147                             spatial_node_index,
148                         });
149                     }
150 
151                     (prim_info.rect, border_radius)
152                 }
153             };
154 
155             clips.push(ClipItemKey {
156                 kind: ClipItemKeyKind::rounded_rect(
157                     final_prim_rect,
158                     clip_radius,
159                     ClipMode::Clip,
160                 ),
161                 spatial_node_index,
162             });
163 
164             self.add_primitive(
165                 spatial_node_index,
166                 clip_chain_id,
167                 &LayoutPrimitiveInfo::with_clip_rect(final_prim_rect, prim_info.clip_rect),
168                 clips,
169                 PrimitiveKeyKind::Rectangle {
170                     color: PropertyBinding::Value(color.into()),
171                 },
172             );
173         } else {
174             // Normal path for box-shadows with a valid blur radius.
175             let blur_offset = (BLUR_SAMPLE_SCALE * blur_radius).ceil();
176             let mut extra_clips = vec![];
177 
178             // Add a normal clip mask to clip out the contents
179             // of the surrounding primitive.
180             extra_clips.push(ClipItemKey {
181                 kind: ClipItemKeyKind::rounded_rect(
182                     prim_info.rect,
183                     border_radius,
184                     prim_clip_mode,
185                 ),
186                 spatial_node_index,
187             });
188 
189             // Get the local rect of where the shadow will be drawn,
190             // expanded to include room for the blurred region.
191             let dest_rect = shadow_rect.inflate(blur_offset, blur_offset);
192 
193             // Draw the box-shadow as a solid rect, using a box-shadow
194             // clip mask item.
195             let prim = PrimitiveKeyKind::Rectangle {
196                 color: PropertyBinding::Value(color.into()),
197             };
198 
199             // Create the box-shadow clip item.
200             let shadow_clip_source = ClipItemKey {
201                 kind: ClipItemKeyKind::box_shadow(
202                     shadow_rect,
203                     shadow_radius,
204                     dest_rect,
205                     blur_radius,
206                     clip_mode,
207                 ),
208                 spatial_node_index,
209             };
210 
211             let prim_info = match clip_mode {
212                 BoxShadowClipMode::Outset => {
213                     // Certain spread-radii make the shadow invalid.
214                     if shadow_rect.is_empty() {
215                         return;
216                     }
217 
218                     // Add the box-shadow clip source.
219                     extra_clips.push(shadow_clip_source);
220 
221                     // Outset shadows are expanded by the shadow
222                     // region from the original primitive.
223                     LayoutPrimitiveInfo::with_clip_rect(dest_rect, prim_info.clip_rect)
224                 }
225                 BoxShadowClipMode::Inset => {
226                     // If the inner shadow rect contains the prim
227                     // rect, no pixels will be shadowed.
228                     if border_radius.is_zero() && shadow_rect
229                         .inflate(-blur_radius, -blur_radius)
230                         .contains_box(&prim_info.rect)
231                     {
232                         return;
233                     }
234 
235                     // Inset shadows are still visible, even if the
236                     // inset shadow rect becomes invalid (they will
237                     // just look like a solid rectangle).
238                     if !shadow_rect.is_empty() {
239                         extra_clips.push(shadow_clip_source);
240                     }
241 
242                     // Inset shadows draw inside the original primitive.
243                     prim_info.clone()
244                 }
245             };
246 
247             self.add_primitive(
248                 spatial_node_index,
249                 clip_chain_id,
250                 &prim_info,
251                 extra_clips,
252                 prim,
253             );
254         }
255     }
256 }
257 
adjust_border_radius_for_box_shadow(radius: BorderRadius, spread_amount: f32) -> BorderRadius258 fn adjust_border_radius_for_box_shadow(radius: BorderRadius, spread_amount: f32) -> BorderRadius {
259     BorderRadius {
260         top_left: adjust_corner_for_box_shadow(radius.top_left, spread_amount),
261         top_right: adjust_corner_for_box_shadow(radius.top_right, spread_amount),
262         bottom_right: adjust_corner_for_box_shadow(radius.bottom_right, spread_amount),
263         bottom_left: adjust_corner_for_box_shadow(radius.bottom_left, spread_amount),
264     }
265 }
266 
adjust_corner_for_box_shadow(corner: LayoutSize, spread_amount: f32) -> LayoutSize267 fn adjust_corner_for_box_shadow(corner: LayoutSize, spread_amount: f32) -> LayoutSize {
268     LayoutSize::new(
269         adjust_radius_for_box_shadow(corner.width, spread_amount),
270         adjust_radius_for_box_shadow(corner.height, spread_amount),
271     )
272 }
273 
adjust_radius_for_box_shadow(border_radius: f32, spread_amount: f32) -> f32274 fn adjust_radius_for_box_shadow(border_radius: f32, spread_amount: f32) -> f32 {
275     if border_radius > 0.0 {
276         (border_radius + spread_amount).max(0.0)
277     } else {
278         0.0
279     }
280 }
281