Skip to main content

sequoia_style/
filter.rs

1#![forbid(unsafe_code)]
2
3use sequoia_core::SpanMask;
4
5use crate::effect::Backdrop;
6
7#[derive(Clone, Copy, Debug, PartialEq)]
8#[non_exhaustive]
9pub enum Mask {
10    LinearFade {
11        from_alpha: f32,
12        to_alpha: f32,
13        angle_rad: f32,
14    },
15
16    Uniform(f32),
17}
18
19impl Mask {
20    pub fn strength_at(&self, t: f32) -> f32 {
21        let t = t.clamp(0.0, 1.0);
22        match *self {
23            Mask::LinearFade {
24                from_alpha,
25                to_alpha,
26                ..
27            } => from_alpha + (to_alpha - from_alpha) * t,
28            Mask::Uniform(v) => v.clamp(0.0, 1.0),
29        }
30    }
31
32    pub fn span_mask(&self) -> SpanMask {
33        match *self {
34            Mask::LinearFade {
35                from_alpha,
36                to_alpha,
37                angle_rad,
38            } => SpanMask::Linear {
39                from: from_alpha,
40                to: to_alpha,
41                angle_rad,
42            },
43            Mask::Uniform(v) => SpanMask::Uniform(v),
44        }
45    }
46}
47
48pub const VERTICAL: f32 = std::f32::consts::FRAC_PI_2;
49
50pub fn gradient_mask<G>(gradient: G) -> Mask
51where
52    G: GradientAlphas,
53{
54    let (a, b) = gradient.endpoint_alphas();
55    let (dx, dy) = gradient.direction();
56    let angle = if dx == 0.0 && dy == 0.0 {
57        VERTICAL
58    } else {
59        dy.atan2(dx)
60    };
61    Mask::LinearFade {
62        from_alpha: a.clamp(0.0, 1.0),
63        to_alpha: b.clamp(0.0, 1.0),
64        angle_rad: angle,
65    }
66}
67
68pub trait GradientAlphas {
69    fn endpoint_alphas(&self) -> (f32, f32);
70
71    fn direction(&self) -> (f32, f32) {
72        (0.0, 1.0)
73    }
74}
75
76#[derive(Clone, Copy, Debug, Default, PartialEq)]
77pub struct Filter {
78    pub blur: f32,
79    pub mask: Option<Mask>,
80}
81
82impl Filter {
83    pub const NONE: Self = Self {
84        blur: 0.0,
85        mask: None,
86    };
87
88    pub const fn blur(radius: f32) -> Self {
89        Self {
90            blur: if radius > 0.0 { radius } else { 0.0 },
91            mask: None,
92        }
93    }
94
95    pub fn blur_radius(mut self, radius: f32) -> Self {
96        self.blur = radius.max(0.0);
97        self
98    }
99
100    pub fn mask(mut self, mask: Mask) -> Self {
101        self.mask = Some(mask);
102        self
103    }
104
105    pub fn is_empty(&self) -> bool {
106        !(self.blur > 0.0 && self.blur.is_finite())
107    }
108
109    pub fn blur_px(&self) -> f32 {
110        if self.blur.is_finite() {
111            self.blur.max(0.0)
112        } else {
113            0.0
114        }
115    }
116
117    pub fn span_mask(&self) -> SpanMask {
118        self.mask.map(|m| m.span_mask()).unwrap_or(SpanMask::None)
119    }
120}
121
122#[derive(Clone, Copy, Debug, PartialEq)]
123#[non_exhaustive]
124pub enum Effect {
125    Blur(Filter),
126    Shadow(crate::ShadowSpec),
127    Backdrop(Backdrop),
128}
129
130impl Effect {
131    pub fn is_empty(&self) -> bool {
132        match self {
133            Effect::Blur(b) => b.is_empty(),
134            Effect::Shadow(s) => !s.is_visible(),
135            Effect::Backdrop(b) => b.is_empty(),
136        }
137    }
138}
139
140pub fn blur(radius: f32) -> Filter {
141    Filter::blur(radius.max(0.0))
142}
143
144pub fn gradient(from_alpha: f32, to_alpha: f32) -> Mask {
145    gradient_angled(from_alpha, to_alpha, VERTICAL)
146}
147
148pub fn gradient_angled(from_alpha: f32, to_alpha: f32, angle_rad: f32) -> Mask {
149    Mask::LinearFade {
150        from_alpha: from_alpha.clamp(0.0, 1.0),
151        to_alpha: to_alpha.clamp(0.0, 1.0),
152        angle_rad,
153    }
154}
155
156pub fn backdrop_blur(radius: f32) -> Backdrop {
157    Backdrop::blur(radius)
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163    use sequoia_core::Color;
164
165    #[test]
166    fn filter_vs_backdrop_semantics() {
167        assert!(Filter::NONE.is_empty());
168        assert!(!Filter::blur(12.0).is_empty());
169        assert!(Backdrop::NONE.is_empty());
170        assert!(!Backdrop::blur(4.0).is_empty());
171
172        assert!(Backdrop::NONE.radius(12.0).is_empty());
173    }
174
175    #[test]
176    fn blur_mask_builder() {
177        let b = blur(30.0).mask(gradient(1.0, 0.0));
178        assert_eq!(b.blur, 30.0);
179        assert!(b.mask.is_some());
180        let m = b.mask.as_ref().unwrap();
181        assert_eq!(m.strength_at(0.0), 1.0, "the mask holds the ramp");
182        assert_eq!(m.strength_at(1.0), 0.0);
183        assert_eq!(
184            m.span_mask(),
185            SpanMask::Linear {
186                from: 1.0,
187                to: 0.0,
188                angle_rad: VERTICAL,
189            },
190            "pionowa rampa do GPU"
191        );
192    }
193
194    #[test]
195    fn angled_mask_encodes_direction() {
196        let m = gradient_angled(0.0, 1.0, 0.0);
197        assert_eq!(m.span_mask().mask_vec(), [1.0, 0.0, 1.0, 0.0]);
198        assert_eq!(Filter::NONE.span_mask(), SpanMask::None);
199        assert_eq!(
200            Backdrop::blur(4.0).span_mask(),
201            SpanMask::None,
202            "no mask, full strength"
203        );
204    }
205
206    #[test]
207    fn backdrop_builder_with_mask() {
208        let f = Backdrop::blur(16.0)
209            .dim(Color::BLACK)
210            .noise(0.06)
211            .radius(12.0)
212            .mask(gradient(1.0, 0.0));
213        assert_eq!(f.blur, 16.0);
214        assert_eq!(f.radius, 12.0);
215        assert!(!f.is_empty());
216        assert!(f.mask.is_some());
217        assert!(f.span_mask() != SpanMask::None);
218    }
219}