Skip to main content

freya_material_design/
ripple.rs

1use std::time::Duration;
2
3use freya_animation::prelude::*;
4use freya_components::theming::hooks::get_theme_or_default;
5use freya_core::prelude::*;
6use torin::prelude::{
7    Point2D,
8    Position,
9    Size,
10    Size2D,
11};
12
13/// A ripple effect instance
14#[derive(Clone, PartialEq)]
15struct RippleInstance {
16    id: u64,
17    center: Point2D,
18}
19
20/// A container that shows a Material Design-style ripple effect when clicked.
21///
22/// The ripple expands from the click position and fades out.
23///
24/// ```rust
25/// # use freya::{material_design::*, prelude::*};
26/// fn app() -> impl IntoElement {
27///     Ripple::new().child(
28///         rect()
29///             .width(Size::px(200.))
30///             .height(Size::px(100.))
31///             .background((100, 100, 200))
32///             .center()
33///             .child("Click me!"),
34///     )
35/// }
36/// ```
37#[derive(Clone, PartialEq)]
38pub struct Ripple {
39    children: Vec<Element>,
40    layout: LayoutData,
41    key: DiffKey,
42    color: Option<Color>,
43    duration: Duration,
44}
45
46impl Default for Ripple {
47    fn default() -> Self {
48        Self::new()
49    }
50}
51
52impl ChildrenExt for Ripple {
53    fn get_children(&mut self) -> &mut Vec<Element> {
54        &mut self.children
55    }
56}
57
58impl KeyExt for Ripple {
59    fn write_key(&mut self) -> &mut DiffKey {
60        &mut self.key
61    }
62}
63
64impl LayoutExt for Ripple {
65    fn get_layout(&mut self) -> &mut LayoutData {
66        &mut self.layout
67    }
68}
69
70impl ContainerExt for Ripple {}
71
72impl ContainerWithContentExt for Ripple {}
73
74impl Ripple {
75    pub fn new() -> Self {
76        Self {
77            children: Vec::new(),
78            layout: LayoutData::default(),
79            key: DiffKey::None,
80            color: None,
81            duration: Duration::from_millis(800),
82        }
83    }
84
85    /// Set the color of the ripple effect.
86    /// Defaults to the theme's primary color.
87    pub fn color(mut self, color: impl Into<Color>) -> Self {
88        self.color = Some(color.into());
89        self
90    }
91
92    /// Set the duration of the ripple animation.
93    /// Default is 800ms.
94    pub fn duration(mut self, duration: Duration) -> Self {
95        self.duration = duration;
96        self
97    }
98}
99
100impl Component for Ripple {
101    fn render(&self) -> impl IntoElement {
102        let mut container_size = use_state(Size2D::zero);
103        let mut ripples = use_state::<Vec<RippleInstance>>(Vec::new);
104        let mut ripple_counter = use_state(|| 0u64);
105
106        let color = self.color.unwrap_or_else(|| {
107            let theme = get_theme_or_default();
108            theme.read().colors.primary
109        });
110        let duration = self.duration;
111
112        let on_pointer_down = move |e: Event<PointerEventData>| {
113            let id = ripple_counter();
114            *ripple_counter.write() += 1;
115
116            ripples.write().push(RippleInstance {
117                id,
118                center: e.element_location().cast(),
119            });
120        };
121
122        let size = container_size();
123        let max_size = size.width.max(size.height) * 2.5;
124
125        rect()
126            .layout(self.layout.clone())
127            .interactive(false)
128            .overflow(Overflow::Clip)
129            .on_pointer_down(on_pointer_down)
130            .on_sized(move |e: Event<SizedEventData>| container_size.set(e.area.size))
131            .children(self.children.clone())
132            .children(ripples.read().iter().map(|ripple| {
133                RippleCircle {
134                    id: ripple.id,
135                    center: ripple.center,
136                    color,
137                    duration,
138                    max_size,
139                    ripples,
140                }
141                .into()
142            }))
143    }
144
145    fn render_key(&self) -> DiffKey {
146        self.key.clone().or(self.default_key())
147    }
148}
149
150#[derive(Clone, PartialEq)]
151struct RippleCircle {
152    id: u64,
153    center: Point2D,
154    color: Color,
155    duration: Duration,
156    max_size: f32,
157    ripples: State<Vec<RippleInstance>>,
158}
159
160impl Component for RippleCircle {
161    fn render(&self) -> impl IntoElement {
162        let id = self.id;
163        let mut ripples = self.ripples;
164
165        let animation = use_animation_with_dependencies(
166            &(self.max_size, self.duration),
167            move |conf, (max_size, duration)| {
168                conf.on_creation(OnCreation::Run);
169
170                (
171                    AnimNum::new(0., *max_size)
172                        .duration(*duration)
173                        .function(Function::Expo)
174                        .ease(Ease::Out),
175                    AnimNum::new(0.35, 0.)
176                        .duration(*duration)
177                        .function(Function::Linear)
178                        .ease(Ease::Out),
179                )
180            },
181        );
182
183        use_side_effect(move || {
184            if !*animation.is_running().read() && *animation.has_run_yet().read() {
185                ripples.write().retain(|r| r.id != id);
186            }
187        });
188
189        let (size, opacity) = animation.get().value();
190        let half = size / 2.0;
191
192        rect()
193            .position(
194                Position::new_absolute()
195                    .left(self.center.x - half)
196                    .top(self.center.y - half),
197            )
198            .width(Size::px(size))
199            .height(Size::px(size))
200            .corner_radius(CornerRadius::new_all(half))
201            .layer(1)
202            .background(self.color.with_a((opacity * 255.0) as u8))
203    }
204
205    fn render_key(&self) -> DiffKey {
206        DiffKey::U64(self.id)
207    }
208}