Skip to main content

webrender/prim_store/gradient/
linear.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/. */
4
5//! Linear gradients
6//!
7//! Specification: https://drafts.csswg.org/css-images-4/#linear-gradients
8//!
9//! Linear gradients are rendered via cached render tasks and composited with the image brush.
10
11use euclid::approxeq::ApproxEq;
12use euclid::point2;
13use api::{ExtendMode, GradientStop};
14use api::units::*;
15use crate::pattern::gradient::linear_gradient_pattern;
16use crate::pattern::{Pattern, PatternBuilder, PatternBuilderContext, PatternBuilderState};
17use crate::scene_building::IsVisible;
18use crate::intern::{Internable, InternDebug, Handle as InternHandle};
19use crate::internal_types::LayoutPrimitiveInfo;
20use crate::image_tiling::simplify_repeated_primitive;
21use crate::prim_store::{PrimitiveKind, PrimitiveOpacity};
22use crate::prim_store::{PrimKeyCommonData, PrimTemplateCommonData, PrimitiveStore};
23use crate::prim_store::{NinePatchDescriptor, PointKey, SizeKey, InternablePrimitive};
24use crate::segment::EdgeMask;
25use super::{stops_and_min_alpha, GradientStopKey, apply_gradient_local_clip};
26use std::ops::{Deref, DerefMut};
27use std::mem::swap;
28
29/// Identifying key for a linear gradient.
30#[cfg_attr(feature = "capture", derive(Serialize))]
31#[cfg_attr(feature = "replay", derive(Deserialize))]
32#[derive(Debug, Clone, Eq, PartialEq, Hash, MallocSizeOf)]
33pub struct LinearGradientKey {
34    pub common: PrimKeyCommonData,
35    pub extend_mode: ExtendMode,
36    pub start_point: PointKey,
37    pub end_point: PointKey,
38    /// Per-axis tile size encoded as a fraction of `common.prim_size`. The
39    /// runtime `stretch_size` is `stretch_ratio * common.prim_size`.
40    pub stretch_ratio: SizeKey,
41    pub tile_spacing: SizeKey,
42    pub stops: Vec<GradientStopKey>,
43    pub reverse_stops: bool,
44    pub nine_patch: Option<Box<NinePatchDescriptor>>,
45    pub enable_dithering: bool,
46}
47
48impl LinearGradientKey {
49    pub fn new(
50        info: &LayoutPrimitiveInfo,
51        linear_grad: LinearGradient,
52    ) -> Self {
53        LinearGradientKey {
54            common: info.into(),
55            extend_mode: linear_grad.extend_mode,
56            start_point: linear_grad.start_point,
57            end_point: linear_grad.end_point,
58            stretch_ratio: linear_grad.stretch_ratio,
59            tile_spacing: linear_grad.tile_spacing,
60            stops: linear_grad.stops,
61            reverse_stops: linear_grad.reverse_stops,
62            nine_patch: linear_grad.nine_patch,
63            enable_dithering: linear_grad.enable_dithering,
64        }
65    }
66}
67
68impl InternDebug for LinearGradientKey {}
69
70#[cfg_attr(feature = "capture", derive(Serialize))]
71#[cfg_attr(feature = "replay", derive(Deserialize))]
72#[derive(Debug, MallocSizeOf)]
73pub struct LinearGradientTemplate {
74    pub common: PrimTemplateCommonData,
75    pub extend_mode: ExtendMode,
76    pub start_point: LayoutPoint,
77    pub end_point: LayoutPoint,
78    /// Per-axis fraction of `common.prim_size` covered by one tile of the
79    /// gradient pattern. Multiply by `common.prim_size` at use to recover the
80    /// absolute stretch_size.
81    pub stretch_ratio: LayoutSize,
82    pub tile_spacing: LayoutSize,
83    pub stops_opacity: PrimitiveOpacity,
84    pub stops: Vec<GradientStop>,
85    pub border_nine_patch: Option<Box<NinePatchDescriptor>>,
86    pub reverse_stops: bool,
87}
88
89impl PatternBuilder for LinearGradientTemplate {
90    fn build(
91        &self,
92        _sub_rect: Option<DeviceRect>,
93        offset: LayoutVector2D,
94        ctx: &PatternBuilderContext,
95        state: &mut PatternBuilderState,
96    ) -> Pattern {
97        let (start, end) = if self.reverse_stops {
98            (self.end_point, self.start_point)
99        } else {
100            (self.start_point, self.end_point)
101        };
102        // LinearGradientTemplate stores the start and end points relative to the
103        // primitive origin, but the shader works with start/end points in "proper"
104        // layout coordinates (relative to the primitive's spatial node).
105        let offset = offset + ctx.prim_origin.to_vector();
106        linear_gradient_pattern(
107            start + offset,
108            end + offset,
109            self.extend_mode,
110            &self.stops,
111            ctx.fb_config.is_software,
112            state.frame_gpu_data,
113        )
114    }
115}
116
117impl Deref for LinearGradientTemplate {
118    type Target = PrimTemplateCommonData;
119    fn deref(&self) -> &Self::Target {
120        &self.common
121    }
122}
123
124impl DerefMut for LinearGradientTemplate {
125    fn deref_mut(&mut self) -> &mut Self::Target {
126        &mut self.common
127    }
128}
129
130/// Perform a few optimizations to the gradient that are relevant to scene building.
131///
132/// Mutates `prim_rect`, `tile_size`, `start`, `end` to bake in the simplifications
133/// (repeated-tile collapse, equivalent-to-stretching on either axis, clip-induced
134/// offsets). Decomposition into per-segment quads is no longer done here -- the
135/// caller emits a single `LinearGradient` prim and prepare-time runs
136/// [`decompose_axis_aligned_gradient`] against the snapped prim_rect when the
137/// gradient is eligible. Doing the decomposition at frame-build keeps adjacent
138/// segments phase-aligned with the snapped outer prim, even when the frame-time
139/// snap pass nudges the outer rect.
140pub fn optimize_linear_gradient(
141    prim_rect: &mut LayoutRect,
142    tile_size: &mut LayoutSize,
143    mut tile_spacing: LayoutSize,
144    clip_rect: &LayoutRect,
145    start: &mut LayoutPoint,
146    end: &mut LayoutPoint,
147) {
148    simplify_repeated_primitive(&tile_size, &mut tile_spacing, prim_rect);
149
150    let vertical = start.x.approx_eq(&end.x);
151    let horizontal = start.y.approx_eq(&end.y);
152
153    let horizontally_tiled = prim_rect.width() > tile_size.width;
154    let vertically_tiled = prim_rect.height() > tile_size.height;
155
156    // Check whether the tiling is equivalent to stretching on either axis.
157    // Stretching the gradient is more efficient than repeating it.
158    if vertically_tiled && horizontal && tile_spacing.height == 0.0 {
159        tile_size.height = prim_rect.height();
160    }
161
162    if horizontally_tiled && vertical && tile_spacing.width == 0.0 {
163        tile_size.width = prim_rect.width();
164    }
165
166    let offset = apply_gradient_local_clip(
167        prim_rect,
168        &tile_size,
169        &tile_spacing,
170        &clip_rect
171    );
172
173    // The size of gradient render tasks depends on the tile_size. No need to generate
174    // large stretch sizes that will be clipped to the bounds of the primitive.
175    tile_size.width = tile_size.width.min(prim_rect.width());
176    tile_size.height = tile_size.height.min(prim_rect.height());
177
178    *start += offset;
179    *end += offset;
180}
181
182/// Whether a linear gradient is eligible for the fast-path two-stop-per-segment
183/// decomposition at prepare time. Inputs are the values produced by
184/// `optimize_linear_gradient` (i.e. already simplified and clip-adjusted).
185pub fn linear_gradient_decomposes(
186    prim_rect: &LayoutRect,
187    tile_size: LayoutSize,
188    tile_spacing: LayoutSize,
189    start: LayoutPoint,
190    end: LayoutPoint,
191    extend_mode: ExtendMode,
192    stops: &[GradientStop],
193    enable_dithering: bool,
194) -> bool {
195    if extend_mode != ExtendMode::Clamp || stops.is_empty() {
196        return false;
197    }
198
199    let vertical = start.x.approx_eq(&end.x);
200    let horizontal = start.y.approx_eq(&end.y);
201
202    if !vertical && !horizontal {
203        return false;
204    }
205
206    if vertical && horizontal {
207        return false;
208    }
209
210    if !tile_spacing.is_empty() {
211        return false;
212    }
213
214    let horizontally_tiled = prim_rect.width() > tile_size.width;
215    let vertically_tiled = prim_rect.height() > tile_size.height;
216    if vertically_tiled || horizontally_tiled {
217        return false;
218    }
219
220    if !enable_dithering &&
221        ((horizontal && tile_size.width < 256.0)
222        || (vertical && tile_size.height < 256.0)) {
223        return false;
224    }
225
226    true
227}
228
229/// Decompose an axis-aligned linear gradient into a sequence of two-stop
230/// segments that tile end-to-end across `prim_rect`. Each callback invocation
231/// is one segment, ready to be rendered as its own quad via the fast-path
232/// gradient shader. Run at frame-build (against the snapped prim_rect) so
233/// adjacent segments share a snapped boundary and tile without phase drift.
234///
235/// Caller must have verified eligibility via [`linear_gradient_decomposes`].
236pub fn decompose_axis_aligned_gradient(
237    prim_rect: &LayoutRect,
238    tile_size: LayoutSize,
239    start: LayoutPoint,
240    end: LayoutPoint,
241    stops: &[GradientStop],
242    clip_rect: &LayoutRect,
243    mut callback: impl FnMut(&LayoutRect, LayoutPoint, LayoutPoint, [GradientStop; 2], EdgeMask),
244) {
245    debug_assert!(!stops.is_empty());
246
247    let vertical = start.x.approx_eq(&end.x);
248
249    // Flip x/y when the gradient is vertical so the remaining math treats it
250    // as horizontal; un-flip per-segment outputs at the end.
251    let adjust_rect = &mut |rect: &mut LayoutRect| {
252        if vertical {
253            swap(&mut rect.min.x, &mut rect.min.y);
254            swap(&mut rect.max.x, &mut rect.max.y);
255        }
256    };
257    let adjust_size = &mut |size: &mut LayoutSize| {
258        if vertical { swap(&mut size.width, &mut size.height); }
259    };
260    let adjust_point = &mut |p: &mut LayoutPoint| {
261        if vertical { swap(&mut p.x, &mut p.y); }
262    };
263
264    let clip_rect = match clip_rect.intersection(prim_rect) {
265        Some(clip) => clip,
266        None => return,
267    };
268
269    let mut prim_rect = *prim_rect;
270    let mut start = start;
271    let mut end = end;
272    let mut tile_size = tile_size;
273
274    adjust_rect(&mut prim_rect);
275    adjust_point(&mut start);
276    adjust_point(&mut end);
277    adjust_size(&mut tile_size);
278
279    // `clip_rect` stays in the original (un-swapped) space — segment_rect
280    // gets `adjust_rect` applied twice (once implicitly via the prim_rect
281    // copy, once explicitly after computing per-segment extent) and lands
282    // back in original space before this intersection.
283
284    let length = (end.x - start.x).abs();
285
286    // Match the pre-refactor optimiser: when the gradient line points in
287    // decreasing-x (post-axis-swap), swap start/end and walk the stop list
288    // in reverse, so the loop always processes stops in increasing-x
289    // order. The pre-refactor code did this via `stops.reverse()` in
290    // place; we can't mutate the template's stops here, so use a reversed
291    // iterator and swap which end of the slice supplies the fake-stop
292    // colour accordingly.
293    let reverse_stops = start.x > end.x;
294    if reverse_stops {
295        swap(&mut start, &mut end);
296    }
297
298    let (first_stop, last_stop) = if reverse_stops {
299        (*stops.last().unwrap(), *stops.first().unwrap())
300    } else {
301        (*stops.first().unwrap(), *stops.last().unwrap())
302    };
303
304    let mut prev = first_stop;
305    let mut last = last_stop;
306    prev.offset = -start.x / length;
307    last.offset = (tile_size.width - start.x) / length;
308    if reverse_stops {
309        prev.offset = 1.0 - prev.offset;
310        last.offset = 1.0 - last.offset;
311    }
312
313    let (side_edges, first_edge, last_edge) = if vertical {
314        (
315            EdgeMask::LEFT | EdgeMask::RIGHT,
316            EdgeMask::TOP,
317            EdgeMask::BOTTOM,
318        )
319    } else {
320        (
321            EdgeMask::TOP | EdgeMask::BOTTOM,
322            EdgeMask::LEFT,
323            EdgeMask::RIGHT,
324        )
325    };
326
327    let mut is_first = true;
328    let last_offset = last.offset;
329
330    // Iterate stops in increasing-x order. When reverse_stops is set, walk the
331    // backing slice in reverse instead of mutating it.
332    let stops_iter: Box<dyn Iterator<Item = &GradientStop>> = if reverse_stops {
333        Box::new(stops.iter().rev())
334    } else {
335        Box::new(stops.iter())
336    };
337
338    for stop in stops_iter.chain(std::iter::once(&last)) {
339        let prev_stop = prev;
340        prev = *stop;
341
342        if prev_stop.color.a == 0.0 && stop.color.a == 0.0 {
343            continue;
344        }
345
346        let prev_offset = if reverse_stops { 1.0 - prev_stop.offset } else { prev_stop.offset };
347        let offset = if reverse_stops { 1.0 - stop.offset } else { stop.offset };
348
349        // Segment_start and segment_end are in the gradient's pre-flip space
350        // (relative to the prim's origin); the adjust_* helpers below restore
351        // axis orientation when emitting.
352        let segment_start = start.x + prev_offset * length;
353        let segment_end = start.x + offset * length;
354        let segment_length = segment_end - segment_start;
355
356        if segment_length <= 0.0 {
357            continue;
358        }
359
360        let mut segment_rect = prim_rect;
361        segment_rect.min.x += segment_start;
362        segment_rect.max.x = segment_rect.min.x + segment_length;
363
364        let mut seg_start = point2(0.0, 0.0);
365        let mut seg_end = point2(segment_length, 0.0);
366
367        adjust_point(&mut seg_start);
368        adjust_point(&mut seg_end);
369        adjust_rect(&mut segment_rect);
370
371        let origin_before_clip = segment_rect.min;
372        segment_rect = match segment_rect.intersection(&clip_rect) {
373            Some(rect) => rect,
374            None => continue,
375        };
376        let clip_offset = segment_rect.min - origin_before_clip;
377        seg_start -= clip_offset;
378        seg_end -= clip_offset;
379
380        let mut edge_flags = side_edges;
381        if is_first {
382            edge_flags |= first_edge;
383            is_first = false;
384        }
385        if stop.offset == last_offset {
386            edge_flags |= last_edge;
387        }
388
389        callback(
390            &segment_rect,
391            seg_start,
392            seg_end,
393            [
394                GradientStop { offset: 0.0, color: prev_stop.color },
395                GradientStop { offset: 1.0, color: stop.color },
396            ],
397            edge_flags,
398        );
399    }
400}
401
402impl From<LinearGradientKey> for LinearGradientTemplate {
403    fn from(item: LinearGradientKey) -> Self {
404
405        let common = PrimTemplateCommonData::with_key_common(item.common);
406
407        let (stops, min_alpha) = stops_and_min_alpha(&item.stops);
408
409        // Save opacity of the stops for use in
410        // selecting which pass this gradient
411        // should be drawn in.
412        let stops_opacity = PrimitiveOpacity::from_alpha(min_alpha);
413
414        let start_point = LayoutPoint::new(item.start_point.x, item.start_point.y);
415        let end_point = LayoutPoint::new(item.end_point.x, item.end_point.y);
416        let tile_spacing: LayoutSize = item.tile_spacing.into();
417        let stretch_ratio: LayoutSize = item.stretch_ratio.into();
418
419        LinearGradientTemplate {
420            common,
421            extend_mode: item.extend_mode,
422            start_point,
423            end_point,
424            stretch_ratio,
425            tile_spacing,
426            stops_opacity,
427            stops,
428            border_nine_patch: item.nine_patch,
429            reverse_stops: item.reverse_stops,
430        }
431    }
432}
433
434pub type LinearGradientDataHandle = InternHandle<LinearGradient>;
435
436#[derive(Debug, MallocSizeOf)]
437#[cfg_attr(feature = "capture", derive(Serialize))]
438#[cfg_attr(feature = "replay", derive(Deserialize))]
439pub struct LinearGradient {
440    pub extend_mode: ExtendMode,
441    pub start_point: PointKey,
442    pub end_point: PointKey,
443    /// Per-axis tile size encoded as a fraction of the prim's size. See
444    /// [`LinearGradientKey::stretch_ratio`].
445    pub stretch_ratio: SizeKey,
446    pub tile_spacing: SizeKey,
447    pub stops: Vec<GradientStopKey>,
448    pub reverse_stops: bool,
449    pub nine_patch: Option<Box<NinePatchDescriptor>>,
450    pub edge_aa_mask: EdgeMask,
451    pub enable_dithering: bool,
452}
453
454impl Internable for LinearGradient {
455    type Key = LinearGradientKey;
456    type StoreData = LinearGradientTemplate;
457    type InternData = ();
458    const PROFILE_COUNTER: usize = crate::profiler::INTERNED_LINEAR_GRADIENTS;
459}
460
461impl InternablePrimitive for LinearGradient {
462    fn into_key(
463        self,
464        info: &LayoutPrimitiveInfo,
465    ) -> LinearGradientKey {
466        LinearGradientKey::new(info, self)
467    }
468
469    fn make_instance_kind(
470        _key: LinearGradientKey,
471        data_handle: LinearGradientDataHandle,
472        _prim_store: &mut PrimitiveStore,
473    ) -> PrimitiveKind {
474        PrimitiveKind::LinearGradient {
475            data_handle,
476        }
477    }
478}
479
480impl IsVisible for LinearGradient {
481    fn is_visible(&self) -> bool {
482        true
483    }
484}
485