Skip to main content

sequoia_view/
filter_view.rs

1#![forbid(unsafe_code)]
2
3use sequoia_core::Length;
4use sequoia_layout::LayoutStyle;
5use sequoia_style::{Backdrop, Effect, Filter};
6
7use crate::any::AnyView;
8use crate::view::{PaintCtx, View};
9
10pub struct DrawGradientMask<'a>(pub &'a sequoia_draw::Gradient);
11
12impl sequoia_style::GradientAlphas for DrawGradientMask<'_> {
13    fn endpoint_alphas(&self) -> (f32, f32) {
14        let stops = self.0.stops();
15        if stops.is_empty() {
16            return (1.0, 1.0);
17        }
18        (
19            stops.first().map(|s| s.color.a).unwrap_or(1.0),
20            stops.last().map(|s| s.color.a).unwrap_or(1.0),
21        )
22    }
23
24    fn direction(&self) -> (f32, f32) {
25        let d = self.0.direction();
26        (d.x, d.y)
27    }
28}
29
30pub fn draw_gradient_mask(gradient: &sequoia_draw::Gradient) -> sequoia_style::Mask {
31    sequoia_style::gradient_mask(DrawGradientMask(gradient))
32}
33
34#[derive(Clone)]
35pub struct Filtered {
36    child: AnyView,
37    filter: Filter,
38}
39
40impl Filtered {
41    pub fn new<V: View + 'static>(child: V, filter: Filter) -> Self {
42        Self {
43            child: AnyView::new(child),
44            filter,
45        }
46    }
47
48    pub fn style(&self) -> Filter {
49        self.filter
50    }
51}
52
53impl View for Filtered {
54    fn body(&self) -> impl View + 'static {
55        crate::views::Unit
56    }
57
58    fn children_override(&self) -> Option<Vec<AnyView>> {
59        Some(vec![self.child.clone()])
60    }
61
62    fn layout_style(&self) -> LayoutStyle {
63        LayoutStyle {
64            width: Length::Fit,
65            height: Length::Fit,
66            ..LayoutStyle::default()
67        }
68    }
69
70    fn foreground_filter(&self) -> Option<Filter> {
71        Some(self.filter)
72    }
73
74    fn paint_self(&self, _ctx: &mut PaintCtx) {}
75}
76
77#[derive(Clone)]
78pub struct BackdropFiltered {
79    child: AnyView,
80    filter: Backdrop,
81}
82
83impl BackdropFiltered {
84    pub fn new<V: View + 'static>(child: V, filter: Backdrop) -> Self {
85        Self {
86            child: AnyView::new(child),
87            filter,
88        }
89    }
90
91    pub fn style(&self) -> Backdrop {
92        self.filter
93    }
94}
95
96impl View for BackdropFiltered {
97    fn body(&self) -> impl View + 'static {
98        crate::views::Unit
99    }
100
101    fn children_override(&self) -> Option<Vec<AnyView>> {
102        Some(vec![self.child.clone()])
103    }
104
105    fn layout_style(&self) -> LayoutStyle {
106        LayoutStyle {
107            width: Length::Fit,
108            height: Length::Fit,
109            ..LayoutStyle::default()
110        }
111    }
112
113    fn paint_self(&self, ctx: &mut PaintCtx) {
114        if self.filter.is_empty() {
115            return;
116        }
117        ctx.push_backdrop(ctx.rect, self.filter);
118    }
119}
120
121/// Wraps a single view and paints a [`Effect`] for it.
122///
123/// Effects are applied after the inner view is fully built. [`EffectNode`]
124/// exposes no `.child()` method, so construct the child first:
125///
126/// ```ignore
127/// let menu = Container::new().child(Row::new() /* ... */);
128/// let menu = EffectNode::new(menu, Effect::Shadow(spec));
129/// ```
130#[derive(Clone)]
131pub struct EffectNode {
132    child: AnyView,
133    effect: Effect,
134}
135
136impl EffectNode {
137    pub fn new<V: View + 'static>(child: V, effect: Effect) -> Self {
138        Self {
139            child: AnyView::new(child),
140            effect,
141        }
142    }
143
144    pub fn effect(&self) -> &Effect {
145        &self.effect
146    }
147}
148
149impl View for EffectNode {
150    fn body(&self) -> impl View + 'static {
151        crate::views::Unit
152    }
153
154    fn children_override(&self) -> Option<Vec<AnyView>> {
155        Some(vec![self.child.clone()])
156    }
157
158    fn layout_style(&self) -> LayoutStyle {
159        LayoutStyle {
160            width: Length::Fit,
161            height: Length::Fit,
162            ..LayoutStyle::default()
163        }
164    }
165
166    fn foreground_filter(&self) -> Option<Filter> {
167        match &self.effect {
168            Effect::Blur(f) => Some(*f),
169            _ => None,
170        }
171    }
172
173    fn paint_self(&self, ctx: &mut PaintCtx) {
174        match &self.effect {
175            Effect::Backdrop(b) if !b.is_empty() => {
176                ctx.push_backdrop(ctx.rect, *b);
177            }
178            Effect::Shadow(spec) if spec.is_visible() => {
179                ctx.list.push_shadow(sequoia_core::Shadow {
180                    rect: ctx.rect,
181                    color: spec.color,
182                    radius: spec.spread.max(0.0),
183                    blur: spec.blur,
184                    spread: spec.spread,
185                    offset: spec.offset,
186                    inset: false,
187                });
188            }
189            _ => {}
190        }
191    }
192}
193
194pub trait FilterExt: View + Sized + 'static {
195    fn blur_own(self, radius: f32) -> Filtered {
196        Filtered::new(self, Filter::blur(radius))
197    }
198
199    fn blur_own_with(self, build: impl FnOnce(Filter) -> Filter) -> Filtered {
200        Filtered::new(self, build(Filter::NONE))
201    }
202
203    fn blur_behind(self, radius: f32) -> BackdropFiltered {
204        BackdropFiltered::new(self, Backdrop::blur(radius))
205    }
206
207    fn blur_behind_with(self, build: impl FnOnce(Backdrop) -> Backdrop) -> BackdropFiltered {
208        BackdropFiltered::new(self, build(Backdrop::NONE))
209    }
210
211    fn effect(self, effect: Effect) -> EffectNode {
212        EffectNode::new(self, effect)
213    }
214}
215
216impl<T: View + 'static> FilterExt for T {}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221
222    #[test]
223    fn blur_shortcuts_are_canonical() {
224        let w = crate::views::Unit.blur_behind(16.0);
225        assert_eq!(w.style().blur, 16.0);
226        assert!(!w.style().is_empty());
227        let w = crate::views::Unit.blur_own(4.0);
228        assert!(!w.style().is_empty());
229        let off = crate::views::Unit.blur_own(0.0);
230        assert!(off.style().is_empty());
231    }
232
233    #[test]
234    fn blur_with_builders() {
235        use sequoia_style::gradient;
236        let w = crate::views::Unit.blur_own_with(|b| b.blur_radius(4.0).mask(gradient(1.0, 0.0)));
237        assert!(w.style().mask.is_some());
238        let w =
239            crate::views::Unit.blur_behind_with(|b| b.blur_radius(8.0).mask(gradient(1.0, 0.0)));
240        assert!(w.style().mask.is_some());
241    }
242
243    #[test]
244    fn effect_node_routes_backdrop_and_shadow() {
245        let b = Effect::Backdrop(Backdrop::blur(8.0));
246        assert!(!b.is_empty());
247        let s = Effect::Shadow(sequoia_style::ShadowSpec::new(
248            sequoia_core::Color::BLACK,
249            8.0,
250        ));
251        assert!(!s.is_empty());
252        let _ = crate::views::Unit.effect(b);
253        let _ = crate::views::Unit.effect(s);
254    }
255}