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