tessera_ui_basic_components/ripple_state.rs
1//! This module defines the [`RippleState`] struct, which manages the state for ripple animations in interactive UI components.
2//!
3//! Currently, two foundational components use it to display ripple animations: [`crate::surface::surface`] and [`crate::fluid_glass::fluid_glass`].
4//!
5//! Other components composed from those, such as [`crate::button::button`], also leverage it to provide ripple effects.
6
7use std::sync::atomic;
8
9/// `RippleState` manages the animation and hover state for ripple effects in interactive UI components.
10/// It is designed to be shared across components using `Arc<RippleState>`, enabling coordinated animation and hover feedback.
11///
12/// # Example
13///
14/// ```
15/// use std::sync::Arc;
16/// use tessera_ui_basic_components::ripple_state::RippleState;
17///
18/// // Create a new ripple state and share it with a button or surface
19/// let ripple_state = Arc::new(RippleState::new());
20///
21/// // Start a ripple animation at a given position (e.g., on mouse click)
22/// ripple_state.start_animation([0.5, 0.5]);
23///
24/// // In your component's render or animation loop:
25/// if let Some((progress, center)) = ripple_state.get_animation_progress() {
26/// // Use progress (0.0..1.0) and center ([f32; 2]) to drive the ripple effect
27/// }
28///
29/// // Set hover state on pointer enter/leave
30/// ripple_state.set_hovered(true);
31/// ```
32pub struct RippleState {
33 /// Whether the ripple animation is currently active.
34 pub is_animating: atomic::AtomicBool,
35 /// The animation start time, stored as milliseconds since the Unix epoch.
36 pub start_time: atomic::AtomicU64,
37 /// The X coordinate of the click position, stored as fixed-point (multiplied by 1000).
38 pub click_pos_x: atomic::AtomicI32,
39 /// The Y coordinate of the click position, stored as fixed-point (multiplied by 1000).
40 pub click_pos_y: atomic::AtomicI32,
41 /// Whether the pointer is currently hovering over the component.
42 pub is_hovered: atomic::AtomicBool,
43}
44
45impl Default for RippleState {
46 /// Creates a new `RippleState` with all fields initialized to their default values.
47 fn default() -> Self {
48 Self::new()
49 }
50}
51
52impl RippleState {
53 /// Creates a new `RippleState` with default values.
54 ///
55 /// # Example
56 /// ```
57 /// use tessera_ui_basic_components::ripple_state::RippleState;
58 /// let state = RippleState::new();
59 /// ```
60 pub fn new() -> Self {
61 Self {
62 is_animating: atomic::AtomicBool::new(false),
63 start_time: atomic::AtomicU64::new(0),
64 click_pos_x: atomic::AtomicI32::new(0),
65 click_pos_y: atomic::AtomicI32::new(0),
66 is_hovered: atomic::AtomicBool::new(false),
67 }
68 }
69
70 /// Starts a new ripple animation from the given click position.
71 ///
72 /// # Arguments
73 ///
74 /// * `click_pos` - The normalized `[x, y]` position (typically in the range [0.0, 1.0]) where the ripple originates.
75 ///
76 /// # Example
77 /// ```
78 /// use tessera_ui_basic_components::ripple_state::RippleState;
79 /// let state = RippleState::new();
80 /// state.start_animation([0.5, 0.5]);
81 /// ```
82 pub fn start_animation(&self, click_pos: [f32; 2]) {
83 let now = std::time::SystemTime::now()
84 .duration_since(std::time::UNIX_EPOCH)
85 .unwrap()
86 .as_millis() as u64;
87
88 self.start_time.store(now, atomic::Ordering::SeqCst);
89 self.click_pos_x
90 .store((click_pos[0] * 1000.0) as i32, atomic::Ordering::SeqCst);
91 self.click_pos_y
92 .store((click_pos[1] * 1000.0) as i32, atomic::Ordering::SeqCst);
93 self.is_animating.store(true, atomic::Ordering::SeqCst);
94 }
95
96 /// Returns the current progress of the ripple animation and the origin position.
97 ///
98 /// Returns `Some((progress, [x, y]))` if the animation is active, where:
99 /// - `progress` is a value in `[0.0, 1.0)` representing the animation progress.
100 /// - `[x, y]` is the normalized origin of the ripple.
101 ///
102 /// Returns `None` if the animation is not active or has completed.
103 ///
104 /// # Example
105 /// ```
106 /// use tessera_ui_basic_components::ripple_state::RippleState;
107 /// let state = RippleState::new();
108 /// state.start_animation([0.5, 0.5]);
109 /// if let Some((progress, center)) = state.get_animation_progress() {
110 /// // Use progress and center for rendering
111 /// }
112 /// ```
113 pub fn get_animation_progress(&self) -> Option<(f32, [f32; 2])> {
114 let is_animating = self.is_animating.load(atomic::Ordering::SeqCst);
115
116 if !is_animating {
117 return None;
118 }
119
120 let now = std::time::SystemTime::now()
121 .duration_since(std::time::UNIX_EPOCH)
122 .unwrap()
123 .as_millis() as u64;
124 let start = self.start_time.load(atomic::Ordering::SeqCst);
125 let elapsed_ms = now.saturating_sub(start);
126 let progress = (elapsed_ms as f32) / 600.0; // 600ms animation
127
128 if progress >= 1.0 {
129 self.is_animating.store(false, atomic::Ordering::SeqCst);
130 return None;
131 }
132
133 let click_pos = [
134 self.click_pos_x.load(atomic::Ordering::SeqCst) as f32 / 1000.0,
135 self.click_pos_y.load(atomic::Ordering::SeqCst) as f32 / 1000.0,
136 ];
137
138 Some((progress, click_pos))
139 }
140
141 /// Sets the hover state for the ripple.
142 ///
143 /// # Arguments
144 ///
145 /// * `hovered` - `true` if the pointer is over the component, `false` otherwise.
146 ///
147 /// # Example
148 /// ```
149 /// use tessera_ui_basic_components::ripple_state::RippleState;
150 /// let state = RippleState::new();
151 /// state.set_hovered(true);
152 /// ```
153 pub fn set_hovered(&self, hovered: bool) {
154 self.is_hovered.store(hovered, atomic::Ordering::SeqCst);
155 }
156
157 /// Returns whether the pointer is currently hovering over the component.
158 ///
159 /// # Example
160 /// ```
161 /// use tessera_ui_basic_components::ripple_state::RippleState;
162 /// let state = RippleState::new();
163 /// let hovered = state.is_hovered();
164 /// ```
165 pub fn is_hovered(&self) -> bool {
166 self.is_hovered.load(atomic::Ordering::SeqCst)
167 }
168}