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}