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