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}