tessera_ui_basic_components/
ripple_state.rs

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