tessera_ui_basic_components/ripple_state.rs
1//! Ripple state — manage ripple animation and hover state for interactive components.
2//!
3//! ## Usage
4//! Provide visual ripple feedback for interactive controls in your app (buttons, surfaces, glass buttons) to indicate clicks and hover interactions.
5
6use std::sync::{Arc, atomic};
7
8/// # RippleState
9///
10/// Manage ripple animations and hover state for interactive UI components.
11/// Recommended use: create a single `RippleState` handle and clone it to share.
12///
13/// ## Parameters
14///
15/// - This type has no constructor parameters; create it with [`RippleState::new()`].
16///
17/// ## Examples
18///
19/// ```
20/// use tessera_ui_basic_components::ripple_state::RippleState;
21/// let s = RippleState::new();
22/// assert!(!s.is_hovered());
23/// s.set_hovered(true);
24/// assert!(s.is_hovered());
25/// ```
26#[derive(Clone)]
27pub struct RippleState {
28 inner: Arc<RippleStateInner>,
29}
30
31impl Default for RippleState {
32 /// Creates a new `RippleState` with all fields initialized to their default values.
33 fn default() -> Self {
34 Self::new()
35 }
36}
37
38impl RippleState {
39 /// Creates a new `RippleState` with default values.
40 ///
41 /// # Example
42 /// ```
43 /// use tessera_ui_basic_components::ripple_state::RippleState;
44 /// let state = RippleState::new();
45 /// ```
46 pub fn new() -> Self {
47 Self {
48 inner: Arc::new(RippleStateInner::new()),
49 }
50 }
51
52 /// Starts a new ripple animation from the given click position.
53 ///
54 /// # Arguments
55 ///
56 /// * `click_pos` - The normalized `[x, y]` position (typically in the range [0.0, 1.0]) where the ripple originates.
57 ///
58 /// # Example
59 /// ```
60 /// use tessera_ui_basic_components::ripple_state::RippleState;
61 /// let state = RippleState::new();
62 /// state.start_animation([0.5, 0.5]);
63 /// ```
64 pub fn start_animation(&self, click_pos: [f32; 2]) {
65 self.inner.start_animation(click_pos);
66 }
67
68 /// Returns the current progress of the ripple animation and the origin position.
69 ///
70 /// Returns `Some((progress, [x, y]))` if the animation is active, where:
71 /// - `progress` is a value in `[0.0, 1.0)` representing the animation progress.
72 /// - `[x, y]` is the normalized origin of the ripple.
73 ///
74 /// Returns `None` if the animation is not active or has completed.
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 /// if let Some((progress, center)) = state.get_animation_progress() {
82 /// // Use progress and center for rendering
83 /// }
84 /// ```
85 pub fn get_animation_progress(&self) -> Option<(f32, [f32; 2])> {
86 self.inner.get_animation_progress()
87 }
88
89 /// Sets the hover state for the ripple.
90 ///
91 /// # Arguments
92 ///
93 /// * `hovered` - `true` if the pointer is over the component, `false` otherwise.
94 ///
95 /// # Example
96 /// ```
97 /// use tessera_ui_basic_components::ripple_state::RippleState;
98 /// let state = RippleState::new();
99 /// state.set_hovered(true);
100 /// ```
101 pub fn set_hovered(&self, hovered: bool) {
102 self.inner.set_hovered(hovered);
103 }
104
105 /// Returns whether the pointer is currently hovering over the component.
106 ///
107 /// # Example
108 /// ```
109 /// use tessera_ui_basic_components::ripple_state::RippleState;
110 /// let state = RippleState::new();
111 /// let hovered = state.is_hovered();
112 /// ```
113 pub fn is_hovered(&self) -> bool {
114 self.inner.is_hovered()
115 }
116}
117
118struct RippleStateInner {
119 /// Whether the ripple animation is currently active.
120 is_animating: atomic::AtomicBool,
121 /// The animation start time, stored as milliseconds since the Unix epoch.
122 start_time: atomic::AtomicU64,
123 /// The X coordinate of the click position, stored as fixed-point (multiplied by 1000).
124 click_pos_x: atomic::AtomicI32,
125 /// The Y coordinate of the click position, stored as fixed-point (multiplied by 1000).
126 click_pos_y: atomic::AtomicI32,
127 /// Whether the pointer is currently hovering over the component.
128 is_hovered: atomic::AtomicBool,
129}
130
131impl RippleStateInner {
132 fn new() -> Self {
133 Self {
134 is_animating: atomic::AtomicBool::new(false),
135 start_time: atomic::AtomicU64::new(0),
136 click_pos_x: atomic::AtomicI32::new(0),
137 click_pos_y: atomic::AtomicI32::new(0),
138 is_hovered: atomic::AtomicBool::new(false),
139 }
140 }
141
142 fn start_animation(&self, click_pos: [f32; 2]) {
143 let now = std::time::SystemTime::now()
144 .duration_since(std::time::UNIX_EPOCH)
145 .expect("System time earlier than UNIX_EPOCH")
146 .as_millis() as u64;
147
148 self.start_time.store(now, atomic::Ordering::SeqCst);
149 self.click_pos_x
150 .store((click_pos[0] * 1000.0) as i32, atomic::Ordering::SeqCst);
151 self.click_pos_y
152 .store((click_pos[1] * 1000.0) as i32, atomic::Ordering::SeqCst);
153 self.is_animating.store(true, atomic::Ordering::SeqCst);
154 }
155
156 fn get_animation_progress(&self) -> Option<(f32, [f32; 2])> {
157 let is_animating = self.is_animating.load(atomic::Ordering::SeqCst);
158
159 if !is_animating {
160 return None;
161 }
162
163 let now = std::time::SystemTime::now()
164 .duration_since(std::time::UNIX_EPOCH)
165 .expect("System time earlier than UNIX_EPOCH")
166 .as_millis() as u64;
167 let start = self.start_time.load(atomic::Ordering::SeqCst);
168 let elapsed_ms = now.saturating_sub(start);
169 let progress = (elapsed_ms as f32) / 600.0; // 600ms animation
170
171 if progress >= 1.0 {
172 self.is_animating.store(false, atomic::Ordering::SeqCst);
173 return None;
174 }
175
176 let click_pos = [
177 self.click_pos_x.load(atomic::Ordering::SeqCst) as f32 / 1000.0,
178 self.click_pos_y.load(atomic::Ordering::SeqCst) as f32 / 1000.0,
179 ];
180
181 Some((progress, click_pos))
182 }
183
184 fn set_hovered(&self, hovered: bool) {
185 self.is_hovered.store(hovered, atomic::Ordering::SeqCst);
186 }
187
188 fn is_hovered(&self) -> bool {
189 self.is_hovered.load(atomic::Ordering::SeqCst)
190 }
191}