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    Area,
8    Position,
9    Size,
10};
11
12/// A ripple effect instance
13#[derive(Clone, PartialEq)]
14struct RippleInstance {
15    id: u64,
16    x: f32,
17    y: f32,
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    /// Default is white with transparency.
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_area = use_state(Area::default);
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 location = e.element_location();
114            let id = ripple_counter();
115            *ripple_counter.write() += 1;
116
117            ripples.write().push(RippleInstance {
118                id,
119                x: location.x as f32,
120                y: location.y as f32,
121            });
122        };
123
124        let area = container_area.read();
125        let max_dimension = area.width().max(area.height());
126
127        rect()
128            .layout(self.layout.clone())
129            .interactive(false)
130            .overflow(Overflow::Clip)
131            .on_pointer_down(on_pointer_down)
132            .on_sized(move |e: Event<SizedEventData>| container_area.set(e.area))
133            .children(self.children.clone())
134            .children(ripples.read().iter().map(|ripple| {
135                RippleCircle {
136                    id: ripple.id,
137                    x: ripple.x,
138                    y: ripple.y,
139                    color,
140                    duration,
141                    max_size: max_dimension * 2.5,
142                    ripples,
143                    key: DiffKey::U64(ripple.id),
144                }
145                .into()
146            }))
147    }
148
149    fn render_key(&self) -> DiffKey {
150        self.key.clone().or(self.default_key())
151    }
152}
153
154#[derive(Clone, PartialEq)]
155struct RippleCircle {
156    id: u64,
157    x: f32,
158    y: f32,
159    color: Color,
160    duration: Duration,
161    max_size: f32,
162    ripples: State<Vec<RippleInstance>>,
163    key: DiffKey,
164}
165
166impl KeyExt for RippleCircle {
167    fn write_key(&mut self) -> &mut DiffKey {
168        &mut self.key
169    }
170}
171
172impl Component for RippleCircle {
173    fn render(&self) -> impl IntoElement {
174        let id = self.id;
175        let mut ripples = self.ripples;
176
177        let animation = use_animation_with_dependencies(
178            &(self.max_size, self.duration),
179            move |conf, (max_size, duration)| {
180                conf.on_creation(OnCreation::Run);
181
182                (
183                    // Scale animation: 0 -> max_size
184                    AnimNum::new(0., *max_size)
185                        .duration(*duration)
186                        .function(Function::Expo)
187                        .ease(Ease::Out),
188                    // Opacity animation: 0.35 -> 0
189                    AnimNum::new(0.35, 0.)
190                        .duration(*duration)
191                        .function(Function::Linear)
192                        .ease(Ease::Out),
193                )
194            },
195        );
196
197        // Remove ripple when animation finishes
198        use_side_effect(move || {
199            if !*animation.is_running().read() && *animation.has_run_yet().read() {
200                ripples.write().retain(|r| r.id != id);
201            }
202        });
203
204        let (size, opacity) = animation.get().value();
205
206        let half_size = size / 2.0;
207        let left = self.x - half_size;
208        let top = self.y - half_size;
209
210        rect()
211            .position(Position::new_absolute().left(left).top(top))
212            .width(Size::px(size))
213            .height(Size::px(size))
214            .corner_radius(CornerRadius::new_all(size / 2.0))
215            .layer(1)
216            .background(self.color.with_a((opacity * 255.0) as u8))
217    }
218
219    fn render_key(&self) -> DiffKey {
220        DiffKey::U64(self.id)
221    }
222}