webrender/
box_shadow.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 http://mozilla.org/MPL/2.0/. */
4use api::{BorderRadius, BoxShadowClipMode, ClipMode, ColorF, ColorU, PrimitiveKeyKind, PropertyBinding};
5use api::units::*;
6use crate::border::{ensure_no_corner_overlap, BorderRadiusAu};
7use crate::clip::{ClipDataHandle, ClipInternData, ClipItemKey, ClipItemKeyKind, ClipNodeId};
8use crate::command_buffer::QuadFlags;
9use crate::intern::{Handle as InternHandle, InternDebug, Internable};
10use crate::pattern::{Pattern, PatternBuilder, PatternBuilderContext, PatternBuilderState};
11use crate::picture::calculate_uv_rect_kind;
12use crate::prim_store::{InternablePrimitive, PrimKey, PrimTemplate, PrimTemplateCommonData};
13use crate::prim_store::{PrimitiveInstanceKind, PrimitiveStore, RectangleKey};
14use crate::quad;
15use crate::render_target::RenderTargetKind;
16use crate::render_task::{BlurTask, MaskSubPass, PrimTask, RenderTask, RenderTaskKind, SubPass};
17use crate::scene_building::{SceneBuilder, IsVisible};
18use crate::segment::EdgeAaSegmentMask;
19use crate::spatial_tree::SpatialNodeIndex;
20use crate::gpu_types::{BoxShadowStretchMode, TransformPaletteId, UvRectKind};
21use crate::render_task_graph::RenderTaskId;
22use crate::internal_types::LayoutPrimitiveInfo;
23use crate::util::{extract_inner_rect_k, ScaleOffset};
24
25pub type BoxShadowKey = PrimKey<BoxShadow>;
26
27impl BoxShadowKey {
28    pub fn new(
29        info: &LayoutPrimitiveInfo,
30        shadow: BoxShadow,
31    ) -> Self {
32        BoxShadowKey {
33            common: info.into(),
34            kind: shadow,
35        }
36    }
37}
38
39impl InternDebug for BoxShadowKey {}
40
41#[cfg_attr(feature = "capture", derive(Serialize))]
42#[cfg_attr(feature = "replay", derive(Deserialize))]
43#[derive(Debug, Clone, MallocSizeOf, Hash, Eq, PartialEq)]
44pub struct BoxShadow {
45    pub color: ColorU,
46    pub blur_radius: Au,
47    pub clip_mode: BoxShadowClipMode,
48    pub inner_shadow_rect: RectangleKey,
49    pub outer_shadow_rect: RectangleKey,
50    pub shadow_radius: BorderRadiusAu,
51    pub clip: ClipDataHandle,
52}
53
54impl IsVisible for BoxShadow {
55    fn is_visible(&self) -> bool {
56        true
57    }
58}
59
60pub type BoxShadowDataHandle = InternHandle<BoxShadow>;
61
62impl PatternBuilder for BoxShadowTemplate {
63    fn build(
64        &self,
65        sub_rect: Option<DeviceRect>,
66        ctx: &PatternBuilderContext,
67        state: &mut PatternBuilderState,
68    ) -> crate::pattern::Pattern {
69
70        let raster_spatial_node_index = ctx.spatial_tree.root_reference_frame_index();
71        let pattern_rect = self.kind.outer_shadow_rect;
72
73        // TODO(gw): Correctly account for scaled blur radius inflation, and device
74        //           pixel scale here.
75
76        let (task_size, content_origin, scale_factor, uv_rect_kind) = match sub_rect {
77            Some(rect) => {
78                let expanded_rect = rect.inflate(32.0, 32.0);
79                let uv_rect_kind = calculate_uv_rect_kind(expanded_rect, pattern_rect.cast_unit());
80
81                (
82                    expanded_rect.size().cast_unit().to_i32(),
83                    expanded_rect.min.cast_unit(),
84                    DevicePixelScale::new(1.0),
85                    uv_rect_kind,
86                )
87            }
88            None => {
89                (
90                    pattern_rect.size().cast_unit().to_i32(),
91                    pattern_rect.min.cast_unit(),
92                    DevicePixelScale::new(1.0),
93                    UvRectKind::Rect,
94                )
95            }
96        };
97
98        let blur_radius = self.kind.blur_radius * scale_factor.0;
99        let clips_range = state.clip_store.push_clip_instance(self.kind.clip);
100        let color_pattern = Pattern::color(self.kind.color);
101
102        let pattern_prim_address_f = quad::write_prim_blocks(
103            &mut state.frame_gpu_data.f32,
104            pattern_rect,
105            pattern_rect,
106            color_pattern.base_color,
107            color_pattern.texture_input.task_id,
108            &[],
109            ScaleOffset::identity(),
110        );
111
112        let pattern_task_id = state.rg_builder.add().init(RenderTask::new_dynamic(
113            task_size,
114            RenderTaskKind::Prim(PrimTask {
115                pattern: color_pattern.kind,
116                pattern_input: color_pattern.shader_input,
117                raster_spatial_node_index,
118                device_pixel_scale: DevicePixelScale::new(1.0),
119                content_origin,
120                prim_address_f: pattern_prim_address_f,
121                transform_id: TransformPaletteId::IDENTITY,
122                edge_flags: EdgeAaSegmentMask::empty(),
123                quad_flags: QuadFlags::APPLY_RENDER_TASK_CLIP | QuadFlags::IGNORE_DEVICE_PIXEL_SCALE,
124                prim_needs_scissor_rect: false,
125                texture_input: color_pattern.texture_input.task_id,
126            }),
127        ));
128
129        let masks = MaskSubPass {
130            clip_node_range: clips_range,
131            prim_spatial_node_index: raster_spatial_node_index,
132            prim_address_f: pattern_prim_address_f,
133        };
134
135        let task = state.rg_builder.get_task_mut(pattern_task_id);
136        task.add_sub_pass(SubPass::Masks { masks });
137
138        let blur_task_v = state.rg_builder.add().init(RenderTask::new_dynamic(
139            task_size,
140            RenderTaskKind::VerticalBlur(BlurTask {
141                blur_std_deviation: blur_radius,
142                target_kind: RenderTargetKind::Color,
143                blur_region: task_size,
144            }),
145        ));
146        state.rg_builder.add_dependency(blur_task_v, pattern_task_id);
147
148        let blur_task_h = state.rg_builder.add().init(RenderTask::new_dynamic(
149            task_size,
150            RenderTaskKind::HorizontalBlur(BlurTask {
151                blur_std_deviation: blur_radius,
152                target_kind: RenderTargetKind::Color,
153                blur_region: task_size,
154            }),
155        ).with_uv_rect_kind(uv_rect_kind));
156        state.rg_builder.add_dependency(blur_task_h, blur_task_v);
157
158        Pattern::texture(
159            blur_task_h,
160            self.kind.color,
161        )
162    }
163
164    fn get_base_color(
165        &self,
166        _ctx: &PatternBuilderContext,
167    ) -> ColorF {
168        self.kind.color
169    }
170
171    fn use_shared_pattern(
172        &self,
173    ) -> bool {
174        false
175    }
176
177    fn can_use_nine_patch(&self) -> bool {
178        false
179    }
180}
181
182impl InternablePrimitive for BoxShadow {
183    fn into_key(
184        self,
185        info: &LayoutPrimitiveInfo,
186    ) -> BoxShadowKey {
187        BoxShadowKey::new(info, self)
188    }
189
190    fn make_instance_kind(
191        _key: BoxShadowKey,
192        data_handle: BoxShadowDataHandle,
193        _prim_store: &mut PrimitiveStore,
194    ) -> PrimitiveInstanceKind {
195        PrimitiveInstanceKind::BoxShadow {
196            data_handle,
197        }
198    }
199}
200
201#[cfg_attr(feature = "capture", derive(Serialize))]
202#[cfg_attr(feature = "replay", derive(Deserialize))]
203#[derive(Debug, MallocSizeOf)]
204pub struct BoxShadowData {
205    pub color: ColorF,
206    pub blur_radius: f32,
207    pub clip_mode: BoxShadowClipMode,
208    pub inner_shadow_rect: LayoutRect,
209    pub outer_shadow_rect: LayoutRect,
210    pub shadow_radius: BorderRadius,
211    pub clip: ClipDataHandle,
212}
213
214impl From<BoxShadow> for BoxShadowData {
215    fn from(shadow: BoxShadow) -> Self {
216        BoxShadowData {
217            color: shadow.color.into(),
218            blur_radius: shadow.blur_radius.to_f32_px(),
219            clip_mode: shadow.clip_mode,
220            inner_shadow_rect: shadow.inner_shadow_rect.into(),
221            outer_shadow_rect: shadow.outer_shadow_rect.into(),
222            shadow_radius: shadow.shadow_radius.into(),
223            clip: shadow.clip,
224        }
225    }
226}
227
228pub type BoxShadowTemplate = PrimTemplate<BoxShadowData>;
229
230impl Internable for BoxShadow {
231    type Key = BoxShadowKey;
232    type StoreData = BoxShadowTemplate;
233    type InternData = ();
234    const PROFILE_COUNTER: usize = crate::profiler::INTERNED_BOX_SHADOWS;
235}
236
237impl From<BoxShadowKey> for BoxShadowTemplate {
238    fn from(shadow: BoxShadowKey) -> Self {
239        BoxShadowTemplate {
240            common: PrimTemplateCommonData::with_key_common(shadow.common),
241            kind: shadow.kind.into(),
242        }
243    }
244}
245
246#[derive(Debug, Clone, MallocSizeOf)]
247#[cfg_attr(feature = "capture", derive(Serialize))]
248#[cfg_attr(feature = "replay", derive(Deserialize))]
249pub struct BoxShadowClipSource {
250    // Parameters that define the shadow and are constant.
251    pub shadow_radius: BorderRadius,
252    pub blur_radius: f32,
253    pub clip_mode: BoxShadowClipMode,
254    pub stretch_mode_x: BoxShadowStretchMode,
255    pub stretch_mode_y: BoxShadowStretchMode,
256
257    // The current cache key (in device-pixels), and handles
258    // to the cached clip region and blurred texture.
259    pub cache_key: Option<(DeviceIntSize, BoxShadowCacheKey)>,
260    pub render_task: Option<RenderTaskId>,
261
262    // Local-space size of the required render task size.
263    pub shadow_rect_alloc_size: LayoutSize,
264
265    // Local-space size of the required render task size without any downscaling
266    // applied. This is needed to stretch the shadow properly.
267    pub original_alloc_size: LayoutSize,
268
269    // The minimal shadow rect for the parameters above,
270    // used when drawing the shadow rect to be blurred.
271    pub minimal_shadow_rect: LayoutRect,
272
273    // Local space rect for the shadow to be drawn or
274    // stretched in the shadow primitive.
275    pub prim_shadow_rect: LayoutRect,
276}
277
278// The blur shader samples BLUR_SAMPLE_SCALE * blur_radius surrounding texels.
279pub const BLUR_SAMPLE_SCALE: f32 = 3.0;
280
281// Maximum blur radius for box-shadows (different than blur filters).
282// Taken from nsCSSRendering.cpp in Gecko.
283pub const MAX_BLUR_RADIUS: f32 = 300.;
284
285// A cache key that uniquely identifies a minimally sized
286// and blurred box-shadow rect that can be stored in the
287// texture cache and applied to clip-masks.
288#[derive(Debug, Clone, Eq, Hash, MallocSizeOf, PartialEq)]
289#[cfg_attr(feature = "capture", derive(Serialize))]
290#[cfg_attr(feature = "replay", derive(Deserialize))]
291pub struct BoxShadowCacheKey {
292    pub blur_radius_dp: i32,
293    pub clip_mode: BoxShadowClipMode,
294    // NOTE(emilio): Only the original allocation size needs to be in the cache
295    // key, since the actual size is derived from that.
296    pub original_alloc_size: DeviceIntSize,
297    pub br_top_left: DeviceIntSize,
298    pub br_top_right: DeviceIntSize,
299    pub br_bottom_right: DeviceIntSize,
300    pub br_bottom_left: DeviceIntSize,
301    pub device_pixel_scale: Au,
302}
303
304impl<'a> SceneBuilder<'a> {
305    pub fn add_box_shadow(
306        &mut self,
307        spatial_node_index: SpatialNodeIndex,
308        clip_node_id: ClipNodeId,
309        prim_info: &LayoutPrimitiveInfo,
310        box_offset: &LayoutVector2D,
311        color: ColorF,
312        mut blur_radius: f32,
313        spread_radius: f32,
314        border_radius: BorderRadius,
315        clip_mode: BoxShadowClipMode,
316        is_root_coord_system: bool,
317    ) {
318        if color.a == 0.0 {
319            return;
320        }
321
322        // Inset shadows get smaller as spread radius increases.
323        let (spread_amount, prim_clip_mode) = match clip_mode {
324            BoxShadowClipMode::Outset => (spread_radius, ClipMode::ClipOut),
325            BoxShadowClipMode::Inset => (-spread_radius, ClipMode::Clip),
326        };
327
328        // Ensure the blur radius is somewhat sensible.
329        blur_radius = f32::min(blur_radius, MAX_BLUR_RADIUS);
330
331        // Adjust the border radius of the box shadow per CSS-spec.
332        let mut shadow_radius = adjust_border_radius_for_box_shadow(border_radius, spread_amount);
333
334        // Apply parameters that affect where the shadow rect
335        // exists in the local space of the primitive.
336        let shadow_rect = prim_info
337            .rect
338            .translate(*box_offset)
339            .inflate(spread_amount, spread_amount);
340
341        // If blur radius is zero, we can use a fast path with
342        // no blur applied.
343        if blur_radius == 0.0 {
344            // Trivial reject of box-shadows that are not visible.
345            if box_offset.x == 0.0 && box_offset.y == 0.0 && spread_amount == 0.0 {
346                return;
347            }
348
349            let mut clips = Vec::with_capacity(2);
350            let (final_prim_rect, clip_radius) = match clip_mode {
351                BoxShadowClipMode::Outset => {
352                    if shadow_rect.is_empty() {
353                        return;
354                    }
355
356                    // TODO(gw): Add a fast path for ClipOut + zero border radius!
357                    clips.push(ClipItemKey {
358                        kind: ClipItemKeyKind::rounded_rect(
359                            prim_info.rect,
360                            border_radius,
361                            ClipMode::ClipOut,
362                        ),
363                        spatial_node_index,
364                    });
365
366                    (shadow_rect, shadow_radius)
367                }
368                BoxShadowClipMode::Inset => {
369                    if !shadow_rect.is_empty() {
370                        clips.push(ClipItemKey {
371                            kind: ClipItemKeyKind::rounded_rect(
372                                shadow_rect,
373                                shadow_radius,
374                                ClipMode::ClipOut,
375                            ),
376                            spatial_node_index,
377                        });
378                    }
379
380                    (prim_info.rect, border_radius)
381                }
382            };
383
384            clips.push(ClipItemKey {
385                kind: ClipItemKeyKind::rounded_rect(
386                    final_prim_rect,
387                    clip_radius,
388                    ClipMode::Clip,
389                ),
390                spatial_node_index,
391            });
392
393            self.add_primitive(
394                spatial_node_index,
395                clip_node_id,
396                &LayoutPrimitiveInfo::with_clip_rect(final_prim_rect, prim_info.clip_rect),
397                clips,
398                PrimitiveKeyKind::Rectangle {
399                    color: PropertyBinding::Value(color.into()),
400                },
401            );
402        } else {
403            // If we know that this is an axis-aligned (root-coord) outset box-shadow,
404            // enable the new quad based render path. Complex transformed and inset
405            // box-shadow support will be added to this path as a follow up.
406            if is_root_coord_system &&
407               clip_mode == BoxShadowClipMode::Outset &&
408               blur_radius < 32.0 &&
409               false {
410                // Make sure corners don't overlap.
411                ensure_no_corner_overlap(&mut shadow_radius, shadow_rect.size());
412
413                // Create clip that gets applied to the primitive
414                let prim_clip = ClipItemKey {
415                    kind: ClipItemKeyKind::rounded_rect(
416                        prim_info.rect,
417                        border_radius,
418                        ClipMode::ClipOut,
419                    ),
420                    spatial_node_index,
421                };
422
423                // Per https://drafts.csswg.org/css-backgrounds/#shadow-blur
424                let blur_radius = (blur_radius * 0.5).round();
425                // Range over which blue radius affects pixels (~99% within 3 * sigma)
426                let sig3 = blur_radius * 3.0;
427
428                // Clip for the pattern primitive
429                let item = ClipItemKey {
430                    kind: ClipItemKeyKind::rounded_rect(
431                        shadow_rect,
432                        shadow_radius,
433                        ClipMode::Clip,
434                    ),
435                    spatial_node_index: self.spatial_tree.root_reference_frame_index(),
436                };
437
438                let clip = self
439                    .interners
440                    .clip
441                    .intern(&item, || {
442                        ClipInternData {
443                            key: item,
444                        }
445                    });
446
447                let inner_shadow_rect = shadow_rect.inflate(-sig3, -sig3);
448                let outer_shadow_rect = shadow_rect.inflate( sig3,  sig3);
449                let inner_shadow_rect = extract_inner_rect_k(&inner_shadow_rect, &shadow_radius, 0.5).unwrap_or(LayoutRect::zero());
450
451                let prim = BoxShadow {
452                    color: color.into(),
453                    blur_radius: Au::from_f32_px(blur_radius),
454                    clip_mode,
455
456                    inner_shadow_rect: inner_shadow_rect.into(),
457                    outer_shadow_rect: outer_shadow_rect.into(),
458                    shadow_radius: shadow_radius.into(),
459                    clip,
460                };
461
462                // Out rect is the shadow rect + extent of blur
463                let prim_info = LayoutPrimitiveInfo::with_clip_rect(
464                    outer_shadow_rect,
465                    prim_info.clip_rect,
466                );
467
468                self.add_nonshadowable_primitive(
469                    spatial_node_index,
470                    clip_node_id,
471                    &prim_info,
472                    vec![prim_clip],
473                    prim,
474                );
475            } else {
476                // Normal path for box-shadows with a valid blur radius.
477                let blur_offset = (BLUR_SAMPLE_SCALE * blur_radius).ceil();
478                let mut extra_clips = vec![];
479
480                // Add a normal clip mask to clip out the contents
481                // of the surrounding primitive.
482                extra_clips.push(ClipItemKey {
483                    kind: ClipItemKeyKind::rounded_rect(
484                        prim_info.rect,
485                        border_radius,
486                        prim_clip_mode,
487                    ),
488                    spatial_node_index,
489                });
490
491                // Get the local rect of where the shadow will be drawn,
492                // expanded to include room for the blurred region.
493                let dest_rect = shadow_rect.inflate(blur_offset, blur_offset);
494
495                // Draw the box-shadow as a solid rect, using a box-shadow
496                // clip mask item.
497                let prim = PrimitiveKeyKind::Rectangle {
498                    color: PropertyBinding::Value(color.into()),
499                };
500
501                // Create the box-shadow clip item.
502                let shadow_clip_source = ClipItemKey {
503                    kind: ClipItemKeyKind::box_shadow(
504                        shadow_rect,
505                        shadow_radius,
506                        dest_rect,
507                        blur_radius,
508                        clip_mode,
509                    ),
510                    spatial_node_index,
511                };
512
513                let prim_info = match clip_mode {
514                    BoxShadowClipMode::Outset => {
515                        // Certain spread-radii make the shadow invalid.
516                        if shadow_rect.is_empty() {
517                            return;
518                        }
519
520                        // Add the box-shadow clip source.
521                        extra_clips.push(shadow_clip_source);
522
523                        // Outset shadows are expanded by the shadow
524                        // region from the original primitive.
525                        LayoutPrimitiveInfo::with_clip_rect(dest_rect, prim_info.clip_rect)
526                    }
527                    BoxShadowClipMode::Inset => {
528                        // If the inner shadow rect contains the prim
529                        // rect, no pixels will be shadowed.
530                        if border_radius.is_zero() && shadow_rect
531                            .inflate(-blur_radius, -blur_radius)
532                            .contains_box(&prim_info.rect)
533                        {
534                            return;
535                        }
536
537                        // Inset shadows are still visible, even if the
538                        // inset shadow rect becomes invalid (they will
539                        // just look like a solid rectangle).
540                        if !shadow_rect.is_empty() {
541                            extra_clips.push(shadow_clip_source);
542                        }
543
544                        // Inset shadows draw inside the original primitive.
545                        prim_info.clone()
546                    }
547                };
548
549                self.add_primitive(
550                    spatial_node_index,
551                    clip_node_id,
552                    &prim_info,
553                    extra_clips,
554                    prim,
555                );
556            }
557        }
558    }
559}
560
561fn adjust_border_radius_for_box_shadow(radius: BorderRadius, spread_amount: f32) -> BorderRadius {
562    BorderRadius {
563        top_left: adjust_corner_for_box_shadow(radius.top_left, spread_amount),
564        top_right: adjust_corner_for_box_shadow(radius.top_right, spread_amount),
565        bottom_right: adjust_corner_for_box_shadow(radius.bottom_right, spread_amount),
566        bottom_left: adjust_corner_for_box_shadow(radius.bottom_left, spread_amount),
567    }
568}
569
570fn adjust_corner_for_box_shadow(corner: LayoutSize, spread_amount: f32) -> LayoutSize {
571    LayoutSize::new(
572        adjust_radius_for_box_shadow(corner.width, spread_amount),
573        adjust_radius_for_box_shadow(corner.height, spread_amount),
574    )
575}
576
577fn adjust_radius_for_box_shadow(border_radius: f32, spread_amount: f32) -> f32 {
578    if border_radius > 0.0 {
579        (border_radius + spread_amount).max(0.0)
580    } else {
581        0.0
582    }
583}