tessera_components/
ripple_state.rs1use std::time::Duration;
9
10use tessera_ui::{Dp, PxSize, current_frame_nanos, time::Instant};
11
12use crate::theme::MaterialAlpha;
13
14#[derive(Clone, Copy, Debug, PartialEq)]
16pub struct RippleSpec {
17 pub bounded: bool,
22 pub radius: Option<Dp>,
25}
26
27impl Default for RippleSpec {
28 fn default() -> Self {
29 Self {
30 bounded: true,
31 radius: None,
32 }
33 }
34}
35
36#[derive(Clone, Copy, Debug, PartialEq)]
38pub struct RippleAnimation {
39 pub progress: f32,
41 pub center: [f32; 2],
44 pub radius: f32,
47 pub alpha: f32,
49}
50
51#[derive(Clone, PartialEq, Copy, Debug)]
52struct RippleAnimationState {
53 start_instant: Instant,
54 start_frame_nanos: u64,
55 center: [f32; 2],
56 max_radius: f32,
57}
58
59impl RippleAnimationState {
60 fn animation_at_instant(self, now: Instant) -> Option<RippleAnimation> {
61 let elapsed_nanos = now
62 .saturating_duration_since(self.start_instant)
63 .as_nanos()
64 .min(u64::MAX as u128) as u64;
65 self.animation_from_elapsed_nanos(elapsed_nanos)
66 }
67
68 fn animation_at_frame_nanos(self, frame_nanos: u64) -> Option<RippleAnimation> {
69 let elapsed_nanos = frame_nanos.saturating_sub(self.start_frame_nanos);
70 self.animation_from_elapsed_nanos(elapsed_nanos)
71 }
72
73 fn animation_from_elapsed_nanos(self, elapsed_nanos: u64) -> Option<RippleAnimation> {
74 let duration_nanos = RippleState::ANIMATION_DURATION
75 .as_nanos()
76 .min(u64::MAX as u128) as u64;
77 let progress = (elapsed_nanos as f32 / duration_nanos as f32).clamp(0.0, 1.0);
78 if progress >= 1.0 {
79 return None;
80 }
81
82 let eased = ease_out_cubic(progress);
83 let radius = self.max_radius * eased;
84 let alpha = MaterialAlpha::PRESSED * (1.0 - progress);
85
86 Some(RippleAnimation {
87 progress,
88 center: self.center,
89 radius,
90 alpha,
91 })
92 }
93}
94
95fn ease_out_cubic(t: f32) -> f32 {
96 let t = t.clamp(0.0, 1.0);
97 1.0 - (1.0 - t).powi(3)
98}
99
100pub struct RippleState {
106 animation: Option<RippleAnimationState>,
107 is_hovered: bool,
108 is_focused: bool,
109 is_dragged: bool,
110 is_pressed: bool,
111}
112
113impl Default for RippleState {
114 fn default() -> Self {
117 Self::new()
118 }
119}
120
121impl RippleState {
122 pub const ANIMATION_DURATION: Duration = Duration::from_millis(300);
124
125 pub fn new() -> Self {
133 Self {
134 animation: None,
135 is_hovered: false,
136 is_focused: false,
137 is_dragged: false,
138 is_pressed: false,
139 }
140 }
141
142 pub fn start_animation(&mut self, click_pos: [f32; 2]) {
158 let now = Instant::now();
159 self.animation = Some(RippleAnimationState {
160 start_instant: now,
161 start_frame_nanos: current_frame_nanos(),
162 center: [click_pos[0].clamp(0.0, 1.0), click_pos[1].clamp(0.0, 1.0)],
163 max_radius: 1.0,
164 });
165 }
166
167 pub fn start_animation_with_spec(
169 &mut self,
170 click_pos: [f32; 2],
171 size: PxSize,
172 spec: RippleSpec,
173 ) {
174 let now = Instant::now();
175 let size = [size.width.to_f32(), size.height.to_f32()];
176 let center = if spec.bounded { click_pos } else { [0.5, 0.5] };
177
178 let min_dimension = size[0].min(size[1]).max(1.0);
179 let max_radius = if let Some(radius) = spec.radius {
180 radius.to_pixels_f32() / min_dimension
181 } else {
182 max_distance_to_corners(center, size) / min_dimension
183 };
184
185 self.animation = Some(RippleAnimationState {
186 start_instant: now,
187 start_frame_nanos: current_frame_nanos(),
188 center,
189 max_radius,
190 });
191 }
192
193 pub fn release(&mut self) {
195 self.set_pressed(false);
196 }
197
198 pub fn set_pressed(&mut self, pressed: bool) {
200 self.is_pressed = pressed;
201 }
202
203 pub fn animation(&mut self) -> Option<RippleAnimation> {
205 self.animation_at_frame_nanos(current_frame_nanos())
206 }
207
208 pub fn animation_snapshot(&self) -> Option<RippleAnimation> {
210 self.animation_snapshot_at_frame_nanos(current_frame_nanos())
211 }
212
213 pub fn animation_snapshot_at(&self, now: Instant) -> Option<RippleAnimation> {
215 self.animation
216 .and_then(|state| state.animation_at_instant(now))
217 }
218
219 pub fn animation_at(&mut self, now: Instant) -> Option<RippleAnimation> {
221 let state = self.animation?;
222 match state.animation_at_instant(now) {
223 Some(animation) => Some(animation),
224 None => {
225 self.animation = None;
226 None
227 }
228 }
229 }
230
231 pub fn animation_snapshot_at_frame_nanos(&self, frame_nanos: u64) -> Option<RippleAnimation> {
233 self.animation
234 .and_then(|state| state.animation_at_frame_nanos(frame_nanos))
235 }
236
237 pub fn animation_at_frame_nanos(&mut self, frame_nanos: u64) -> Option<RippleAnimation> {
239 let state = self.animation?;
240 match state.animation_at_frame_nanos(frame_nanos) {
241 Some(animation) => Some(animation),
242 None => {
243 self.animation = None;
244 None
245 }
246 }
247 }
248
249 pub fn get_animation_progress(&mut self) -> Option<(f32, [f32; 2])> {
272 self.animation()
273 .map(|animation| (animation.progress, animation.center))
274 }
275
276 pub fn set_hovered(&mut self, hovered: bool) {
278 self.is_hovered = hovered;
279 }
280
281 pub fn is_hovered(&self) -> bool {
283 self.is_hovered
284 }
285
286 pub fn set_focused(&mut self, focused: bool) {
288 self.is_focused = focused;
289 }
290
291 pub fn is_focused(&self) -> bool {
293 self.is_focused
294 }
295
296 pub fn set_dragged(&mut self, dragged: bool) {
298 self.is_dragged = dragged;
299 }
300
301 pub fn is_dragged(&self) -> bool {
303 self.is_dragged
304 }
305
306 pub fn is_pressed(&self) -> bool {
308 self.is_pressed
309 }
310
311 pub fn state_layer_alpha(&self) -> f32 {
313 if self.is_dragged {
314 MaterialAlpha::DRAGGED
315 } else if self.is_pressed {
316 MaterialAlpha::PRESSED
317 } else if self.is_focused {
318 MaterialAlpha::FOCUSED
319 } else if self.is_hovered {
320 MaterialAlpha::HOVER
321 } else {
322 0.0
323 }
324 }
325}
326
327fn max_distance_to_corners(center: [f32; 2], size: [f32; 2]) -> f32 {
328 let origin = [center[0] * size[0], center[1] * size[1]];
329 let corners = [
330 [0.0, 0.0],
331 [size[0], 0.0],
332 [size[0], size[1]],
333 [0.0, size[1]],
334 ];
335
336 corners
337 .into_iter()
338 .map(|corner| {
339 let dx = corner[0] - origin[0];
340 let dy = corner[1] - origin[1];
341 (dx * dx + dy * dy).sqrt()
342 })
343 .fold(0.0_f32, f32::max)
344}