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