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, BlurEdgeMode};
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                edge_mode: BlurEdgeMode::Duplicate,
145            }),
146        ));
147        state.rg_builder.add_dependency(blur_task_v, pattern_task_id);
148
149        let blur_task_h = state.rg_builder.add().init(RenderTask::new_dynamic(
150            task_size,
151            RenderTaskKind::HorizontalBlur(BlurTask {
152                blur_std_deviation: blur_radius,
153                target_kind: RenderTargetKind::Color,
154                blur_region: task_size,
155                edge_mode: BlurEdgeMode::Duplicate,
156            }),
157        ).with_uv_rect_kind(uv_rect_kind));
158        state.rg_builder.add_dependency(blur_task_h, blur_task_v);
159
160        Pattern::texture(
161            blur_task_h,
162            self.kind.color,
163        )
164    }
165
166    fn get_base_color(
167        &self,
168        _ctx: &PatternBuilderContext,
169    ) -> ColorF {
170        self.kind.color
171    }
172
173    fn use_shared_pattern(
174        &self,
175    ) -> bool {
176        false
177    }
178
179    fn can_use_nine_patch(&self) -> bool {
180        false
181    }
182}
183
184impl InternablePrimitive for BoxShadow {
185    fn into_key(
186        self,
187        info: &LayoutPrimitiveInfo,
188    ) -> BoxShadowKey {
189        BoxShadowKey::new(info, self)
190    }
191
192    fn make_instance_kind(
193        _key: BoxShadowKey,
194        data_handle: BoxShadowDataHandle,
195        _prim_store: &mut PrimitiveStore,
196    ) -> PrimitiveInstanceKind {
197        PrimitiveInstanceKind::BoxShadow {
198            data_handle,
199        }
200    }
201}
202
203#[cfg_attr(feature = "capture", derive(Serialize))]
204#[cfg_attr(feature = "replay", derive(Deserialize))]
205#[derive(Debug, MallocSizeOf)]
206pub struct BoxShadowData {
207    pub color: ColorF,
208    pub blur_radius: f32,
209    pub clip_mode: BoxShadowClipMode,
210    pub inner_shadow_rect: LayoutRect,
211    pub outer_shadow_rect: LayoutRect,
212    pub shadow_radius: BorderRadius,
213    pub clip: ClipDataHandle,
214}
215
216impl From<BoxShadow> for BoxShadowData {
217    fn from(shadow: BoxShadow) -> Self {
218        BoxShadowData {
219            color: shadow.color.into(),
220            blur_radius: shadow.blur_radius.to_f32_px(),
221            clip_mode: shadow.clip_mode,
222            inner_shadow_rect: shadow.inner_shadow_rect.into(),
223            outer_shadow_rect: shadow.outer_shadow_rect.into(),
224            shadow_radius: shadow.shadow_radius.into(),
225            clip: shadow.clip,
226        }
227    }
228}
229
230pub type BoxShadowTemplate = PrimTemplate<BoxShadowData>;
231
232impl Internable for BoxShadow {
233    type Key = BoxShadowKey;
234    type StoreData = BoxShadowTemplate;
235    type InternData = ();
236    const PROFILE_COUNTER: usize = crate::profiler::INTERNED_BOX_SHADOWS;
237}
238
239impl From<BoxShadowKey> for BoxShadowTemplate {
240    fn from(shadow: BoxShadowKey) -> Self {
241        BoxShadowTemplate {
242            common: PrimTemplateCommonData::with_key_common(shadow.common),
243            kind: shadow.kind.into(),
244        }
245    }
246}
247
248#[derive(Debug, Clone, MallocSizeOf)]
249#[cfg_attr(feature = "capture", derive(Serialize))]
250#[cfg_attr(feature = "replay", derive(Deserialize))]
251pub struct BoxShadowClipSource {
252    // Parameters that define the shadow and are constant.
253    pub shadow_radius: BorderRadius,
254    pub blur_radius: f32,
255    pub clip_mode: BoxShadowClipMode,
256    pub stretch_mode_x: BoxShadowStretchMode,
257    pub stretch_mode_y: BoxShadowStretchMode,
258
259    // The current cache key (in device-pixels), and handles
260    // to the cached clip region and blurred texture.
261    pub cache_key: Option<(DeviceIntSize, BoxShadowCacheKey)>,
262    pub render_task: Option<RenderTaskId>,
263
264    // Local-space size of the required render task size.
265    pub shadow_rect_alloc_size: LayoutSize,
266
267    // Local-space size of the required render task size without any downscaling
268    // applied. This is needed to stretch the shadow properly.
269    pub original_alloc_size: LayoutSize,
270
271    // The minimal shadow rect for the parameters above,
272    // used when drawing the shadow rect to be blurred.
273    pub minimal_shadow_rect: LayoutRect,
274
275    // Local space rect for the shadow to be drawn or
276    // stretched in the shadow primitive.
277    pub prim_shadow_rect: LayoutRect,
278}
279
280// The blur shader samples BLUR_SAMPLE_SCALE * blur_radius surrounding texels.
281pub const BLUR_SAMPLE_SCALE: f32 = 3.0;
282
283// Maximum blur radius for box-shadows (different than blur filters).
284// Taken from nsCSSRendering.cpp in Gecko.
285pub const MAX_BLUR_RADIUS: f32 = 300.;
286
287// A cache key that uniquely identifies a minimally sized
288// and blurred box-shadow rect that can be stored in the
289// texture cache and applied to clip-masks.
290#[derive(Debug, Clone, Eq, Hash, MallocSizeOf, PartialEq)]
291#[cfg_attr(feature = "capture", derive(Serialize))]
292#[cfg_attr(feature = "replay", derive(Deserialize))]
293pub struct BoxShadowCacheKey {
294    pub blur_radius_dp: i32,
295    pub clip_mode: BoxShadowClipMode,
296    // NOTE(emilio): Only the original allocation size needs to be in the cache
297    // key, since the actual size is derived from that.
298    pub original_alloc_size: DeviceIntSize,
299    pub br_top_left: DeviceIntSize,
300    pub br_top_right: DeviceIntSize,
301    pub br_bottom_right: DeviceIntSize,
302    pub br_bottom_left: DeviceIntSize,
303    pub device_pixel_scale: Au,
304}
305
306impl<'a> SceneBuilder<'a> {
307    pub fn add_box_shadow(
308        &mut self,
309        spatial_node_index: SpatialNodeIndex,
310        clip_node_id: ClipNodeId,
311        prim_info: &LayoutPrimitiveInfo,
312        box_offset: &LayoutVector2D,
313        color: ColorF,
314        mut blur_radius: f32,
315        spread_radius: f32,
316        border_radius: BorderRadius,
317        clip_mode: BoxShadowClipMode,
318        is_root_coord_system: bool,
319    ) {
320        if color.a == 0.0 {
321            return;
322        }
323
324        // Inset shadows get smaller as spread radius increases.
325        let (spread_amount, prim_clip_mode) = match clip_mode {
326            BoxShadowClipMode::Outset => (spread_radius, ClipMode::ClipOut),
327            BoxShadowClipMode::Inset => (-spread_radius, ClipMode::Clip),
328        };
329
330        // Ensure the blur radius is somewhat sensible.
331        blur_radius = f32::min(blur_radius, MAX_BLUR_RADIUS);
332
333        // Adjust the border radius of the box shadow per CSS-spec.
334        let mut shadow_radius = adjust_border_radius_for_box_shadow(border_radius, spread_amount);
335
336        // Apply parameters that affect where the shadow rect
337        // exists in the local space of the primitive.
338        let shadow_rect = prim_info
339            .rect
340            .translate(*box_offset)
341            .inflate(spread_amount, spread_amount);
342
343        // If blur radius is zero, we can use a fast path with
344        // no blur applied.
345        if blur_radius == 0.0 {
346            // Trivial reject of box-shadows that are not visible.
347            if box_offset.x == 0.0 && box_offset.y == 0.0 && spread_amount == 0.0 {
348                return;
349            }
350
351            let mut clips = Vec::with_capacity(2);
352            let (final_prim_rect, clip_radius) = match clip_mode {
353                BoxShadowClipMode::Outset => {
354                    if shadow_rect.is_empty() {
355                        return;
356                    }
357
358                    // TODO(gw): Add a fast path for ClipOut + zero border radius!
359                    clips.push(ClipItemKey {
360                        kind: ClipItemKeyKind::rounded_rect(
361                            prim_info.rect,
362                            border_radius,
363                            ClipMode::ClipOut,
364                        ),
365                        spatial_node_index,
366                    });
367
368                    (shadow_rect, shadow_radius)
369                }
370                BoxShadowClipMode::Inset => {
371                    if !shadow_rect.is_empty() {
372                        clips.push(ClipItemKey {
373                            kind: ClipItemKeyKind::rounded_rect(
374                                shadow_rect,
375                                shadow_radius,
376                                ClipMode::ClipOut,
377                            ),
378                            spatial_node_index,
379                        });
380                    }
381
382                    (prim_info.rect, border_radius)
383                }
384            };
385
386            clips.push(ClipItemKey {
387                kind: ClipItemKeyKind::rounded_rect(
388                    final_prim_rect,
389                    clip_radius,
390                    ClipMode::Clip,
391                ),
392                spatial_node_index,
393            });
394
395            self.add_primitive(
396                spatial_node_index,
397                clip_node_id,
398                &LayoutPrimitiveInfo::with_clip_rect(final_prim_rect, prim_info.clip_rect),
399                clips,
400                PrimitiveKeyKind::Rectangle {
401                    color: PropertyBinding::Value(color.into()),
402                },
403            );
404        } else {
405            // If we know that this is an axis-aligned (root-coord) outset box-shadow,
406            // enable the new quad based render path. Complex transformed and inset
407            // box-shadow support will be added to this path as a follow up.
408            if is_root_coord_system &&
409               clip_mode == BoxShadowClipMode::Outset &&
410               blur_radius < 32.0 &&
411               false {
412                // Make sure corners don't overlap.
413                ensure_no_corner_overlap(&mut shadow_radius, shadow_rect.size());
414
415                // Create clip that gets applied to the primitive
416                let prim_clip = ClipItemKey {
417                    kind: ClipItemKeyKind::rounded_rect(
418                        prim_info.rect,
419                        border_radius,
420                        ClipMode::ClipOut,
421                    ),
422                    spatial_node_index,
423                };
424
425                // Per https://drafts.csswg.org/css-backgrounds/#shadow-blur
426                let blur_radius = (blur_radius * 0.5).round();
427                // Range over which blue radius affects pixels (~99% within 3 * sigma)
428                let sig3 = blur_radius * 3.0;
429
430                // Clip for the pattern primitive
431                let item = ClipItemKey {
432                    kind: ClipItemKeyKind::rounded_rect(
433                        shadow_rect,
434                        shadow_radius,
435                        ClipMode::Clip,
436                    ),
437                    spatial_node_index: self.spatial_tree.root_reference_frame_index(),
438                };
439
440                let clip = self
441                    .interners
442                    .clip
443                    .intern(&item, || {
444                        ClipInternData {
445                            key: item,
446                        }
447                    });
448
449                let inner_shadow_rect = shadow_rect.inflate(-sig3, -sig3);
450                let outer_shadow_rect = shadow_rect.inflate( sig3,  sig3);
451                let inner_shadow_rect = extract_inner_rect_k(&inner_shadow_rect, &shadow_radius, 0.5).unwrap_or(LayoutRect::zero());
452
453                let prim = BoxShadow {
454                    color: color.into(),
455                    blur_radius: Au::from_f32_px(blur_radius),
456                    clip_mode,
457
458                    inner_shadow_rect: inner_shadow_rect.into(),
459                    outer_shadow_rect: outer_shadow_rect.into(),
460                    shadow_radius: shadow_radius.into(),
461                    clip,
462                };
463
464                // Out rect is the shadow rect + extent of blur
465                let prim_info = LayoutPrimitiveInfo::with_clip_rect(
466                    outer_shadow_rect,
467                    prim_info.clip_rect,
468                );
469
470                self.add_nonshadowable_primitive(
471                    spatial_node_index,
472                    clip_node_id,
473                    &prim_info,
474                    vec![prim_clip],
475                    prim,
476                );
477            } else {
478                // Normal path for box-shadows with a valid blur radius.
479                let blur_offset = (BLUR_SAMPLE_SCALE * blur_radius).ceil();
480                let mut extra_clips = vec![];
481
482                // Add a normal clip mask to clip out the contents
483                // of the surrounding primitive.
484                extra_clips.push(ClipItemKey {
485                    kind: ClipItemKeyKind::rounded_rect(
486                        prim_info.rect,
487                        border_radius,
488                        prim_clip_mode,
489                    ),
490                    spatial_node_index,
491                });
492
493                // Get the local rect of where the shadow will be drawn,
494                // expanded to include room for the blurred region.
495                let dest_rect = shadow_rect.inflate(blur_offset, blur_offset);
496
497                // Draw the box-shadow as a solid rect, using a box-shadow
498                // clip mask item.
499                let prim = PrimitiveKeyKind::Rectangle {
500                    color: PropertyBinding::Value(color.into()),
501                };
502
503                // Create the box-shadow clip item.
504                let shadow_clip_source = ClipItemKey {
505                    kind: ClipItemKeyKind::box_shadow(
506                        shadow_rect,
507                        shadow_radius,
508                        dest_rect,
509                        blur_radius,
510                        clip_mode,
511                    ),
512                    spatial_node_index,
513                };
514
515                let prim_info = match clip_mode {
516                    BoxShadowClipMode::Outset => {
517                        // Certain spread-radii make the shadow invalid.
518                        if shadow_rect.is_empty() {
519                            return;
520                        }
521
522                        // Add the box-shadow clip source.
523                        extra_clips.push(shadow_clip_source);
524
525                        // Outset shadows are expanded by the shadow
526                        // region from the original primitive.
527                        LayoutPrimitiveInfo::with_clip_rect(dest_rect, prim_info.clip_rect)
528                    }
529                    BoxShadowClipMode::Inset => {
530                        // If the inner shadow rect contains the prim
531                        // rect, no pixels will be shadowed.
532                        if border_radius.is_zero() && shadow_rect
533                            .inflate(-blur_radius, -blur_radius)
534                            .contains_box(&prim_info.rect)
535                        {
536                            return;
537                        }
538
539                        // Inset shadows are still visible, even if the
540                        // inset shadow rect becomes invalid (they will
541                        // just look like a solid rectangle).
542                        if !shadow_rect.is_empty() {
543                            extra_clips.push(shadow_clip_source);
544                        }
545
546                        // Inset shadows draw inside the original primitive.
547                        prim_info.clone()
548                    }
549                };
550
551                self.add_primitive(
552                    spatial_node_index,
553                    clip_node_id,
554                    &prim_info,
555                    extra_clips,
556                    prim,
557                );
558            }
559        }
560    }
561}
562
563fn adjust_border_radius_for_box_shadow(radius: BorderRadius, spread_amount: f32) -> BorderRadius {
564    BorderRadius {
565        top_left: adjust_corner_for_box_shadow(radius.top_left, spread_amount),
566        top_right: adjust_corner_for_box_shadow(radius.top_right, spread_amount),
567        bottom_right: adjust_corner_for_box_shadow(radius.bottom_right, spread_amount),
568        bottom_left: adjust_corner_for_box_shadow(radius.bottom_left, spread_amount),
569    }
570}
571
572fn adjust_corner_for_box_shadow(corner: LayoutSize, spread_amount: f32) -> LayoutSize {
573    LayoutSize::new(
574        adjust_radius_for_box_shadow(corner.width, spread_amount),
575        adjust_radius_for_box_shadow(corner.height, spread_amount),
576    )
577}
578
579fn adjust_radius_for_box_shadow(border_radius: f32, spread_amount: f32) -> f32 {
580    if border_radius > 0.0 {
581        (border_radius + spread_amount).max(0.0)
582    } else {
583        0.0
584    }
585}