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 Ripple {
73    pub fn new() -> Self {
74        Self {
75            children: Vec::new(),
76            layout: LayoutData::default(),
77            key: DiffKey::None,
78            color: None,
79            duration: Duration::from_millis(800),
80        }
81    }
82
83    /// Set the color of the ripple effect.
84    /// Default is white with transparency.
85    pub fn color(mut self, color: impl Into<Color>) -> Self {
86        self.color = Some(color.into());
87        self
88    }
89
90    /// Set the duration of the ripple animation.
91    /// Default is 800ms.
92    pub fn duration(mut self, duration: Duration) -> Self {
93        self.duration = duration;
94        self
95    }
96}
97
98impl Render for Ripple {
99    fn render(&self) -> impl IntoElement {
100        let mut container_area = use_state(Area::default);
101        let mut ripples = use_state::<Vec<RippleInstance>>(Vec::new);
102        let mut ripple_counter = use_state(|| 0u64);
103
104        let color = self.color.unwrap_or_else(|| {
105            let theme = get_theme_or_default();
106            theme.read().colors.primary
107        });
108        let duration = self.duration;
109
110        let on_mouse_down = move |e: Event<MouseEventData>| {
111            let location = e.element_location;
112            let id = ripple_counter();
113            *ripple_counter.write() += 1;
114
115            ripples.write().push(RippleInstance {
116                id,
117                x: location.x as f32,
118                y: location.y as f32,
119            });
120        };
121
122        let area = container_area.read();
123        let max_dimension = area.width().max(area.height());
124
125        rect()
126            .layout(self.layout.clone())
127            .interactive(false)
128            .overflow(Overflow::Clip)
129            .on_mouse_down(on_mouse_down)
130            .on_sized(move |e: Event<SizedEventData>| container_area.set(e.area))
131            .children(self.children.clone())
132            .children_iter(ripples.read().iter().map(|ripple| {
133                RippleCircle {
134                    id: ripple.id,
135                    x: ripple.x,
136                    y: ripple.y,
137                    color,
138                    duration,
139                    max_size: max_dimension * 2.5,
140                    ripples,
141                    key: DiffKey::U64(ripple.id),
142                }
143                .into()
144            }))
145    }
146
147    fn render_key(&self) -> DiffKey {
148        self.key.clone().or(self.default_key())
149    }
150}
151
152#[derive(Clone, PartialEq)]
153struct RippleCircle {
154    id: u64,
155    x: f32,
156    y: f32,
157    color: Color,
158    duration: Duration,
159    max_size: f32,
160    ripples: State<Vec<RippleInstance>>,
161    key: DiffKey,
162}
163
164impl KeyExt for RippleCircle {
165    fn write_key(&mut self) -> &mut DiffKey {
166        &mut self.key
167    }
168}
169
170impl Render for RippleCircle {
171    fn render(&self) -> impl IntoElement {
172        let id = self.id;
173        let mut ripples = self.ripples;
174
175        let animation = use_animation_with_dependencies(
176            &(self.max_size, self.duration),
177            move |conf, (max_size, duration)| {
178                conf.on_creation(OnCreation::Run);
179
180                (
181                    // Scale animation: 0 -> max_size
182                    AnimNum::new(0., *max_size)
183                        .duration(*duration)
184                        .function(Function::Expo)
185                        .ease(Ease::Out),
186                    // Opacity animation: 0.35 -> 0
187                    AnimNum::new(0.35, 0.)
188                        .duration(*duration)
189                        .function(Function::Linear)
190                        .ease(Ease::Out),
191                )
192            },
193        );
194
195        // Remove ripple when animation finishes
196        use_side_effect(move || {
197            if !*animation.is_running().read() && *animation.has_run_yet().read() {
198                ripples.write().retain(|r| r.id != id);
199            }
200        });
201
202        let (size, opacity) = animation.get().value();
203
204        let half_size = size / 2.0;
205        let left = self.x - half_size;
206        let top = self.y - half_size;
207
208        rect()
209            .position(Position::new_absolute().left(left).top(top))
210            .width(Size::px(size))
211            .height(Size::px(size))
212            .corner_radius(CornerRadius::new_all(size / 2.0))
213            .layer(1)
214            .background(self.color.with_a((opacity * 255.0) as u8))
215    }
216
217    fn render_key(&self) -> DiffKey {
218        DiffKey::U64(self.id)
219    }
220}