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}