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            .overflow(Overflow::Clip)
128            .on_pointer_down(on_pointer_down)
129            .on_sized(move |e: Event<SizedEventData>| container_size.set(e.area.size))
130            .children(self.children.clone())
131            .children(ripples.read().iter().map(|ripple| {
132                RippleCircle {
133                    id: ripple.id,
134                    center: ripple.center,
135                    color,
136                    duration,
137                    max_size,
138                    ripples,
139                }
140                .into()
141            }))
142    }
143
144    fn render_key(&self) -> DiffKey {
145        self.key.clone().or(self.default_key())
146    }
147}
148
149#[derive(Clone, PartialEq)]
150struct RippleCircle {
151    id: u64,
152    center: Point2D,
153    color: Color,
154    duration: Duration,
155    max_size: f32,
156    ripples: State<Vec<RippleInstance>>,
157}
158
159impl Component for RippleCircle {
160    fn render(&self) -> impl IntoElement {
161        let id = self.id;
162        let mut ripples = self.ripples;
163
164        let animation = use_animation_with_dependencies(
165            &(self.max_size, self.duration),
166            move |conf, (max_size, duration)| {
167                conf.on_creation(OnCreation::Run);
168
169                (
170                    AnimNum::new(0., *max_size)
171                        .duration(*duration)
172                        .function(Function::Expo)
173                        .ease(Ease::Out),
174                    AnimNum::new(0.35, 0.)
175                        .duration(*duration)
176                        .function(Function::Linear)
177                        .ease(Ease::Out),
178                )
179            },
180        );
181
182        use_side_effect(move || {
183            if !*animation.is_running().read() && *animation.has_run_yet().read() {
184                ripples.write().retain(|r| r.id != id);
185            }
186        });
187
188        let (size, opacity) = animation.get().value();
189        let half = size / 2.0;
190
191        rect()
192            .interactive(false)
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}