uzor_interactive/
elastic_slider.rs1#[cfg(feature = "animation")]
7use uzor_animation::Spring;
8
9#[derive(Debug, Clone)]
13pub struct ElasticSlider {
14 pub value: f32,
16
17 pub min: f32,
19
20 pub max: f32,
22
23 pub step: f32,
25
26 pub max_overflow: f32,
28
29 overflow: f32,
31
32 #[cfg(feature = "animation")]
34 spring_state: Option<SpringState>,
35
36 #[cfg(feature = "animation")]
38 spring_start_time: f64,
39}
40
41#[cfg(feature = "animation")]
42#[derive(Debug, Clone)]
43struct SpringState {
44 spring: Spring,
45}
46
47impl Default for ElasticSlider {
48 fn default() -> Self {
49 Self::new(0.0, 100.0)
50 }
51}
52
53impl ElasticSlider {
54 pub fn new(min: f32, max: f32) -> Self {
56 Self {
57 value: (min + max) / 2.0,
58 min,
59 max,
60 step: 0.0,
61 max_overflow: 50.0,
62 overflow: 0.0,
63 #[cfg(feature = "animation")]
64 spring_state: None,
65 #[cfg(feature = "animation")]
66 spring_start_time: 0.0,
67 }
68 }
69
70 pub fn with_step(mut self, step: f32) -> Self {
72 self.step = step;
73 self
74 }
75
76 pub fn with_max_overflow(mut self, max_overflow: f32) -> Self {
78 self.max_overflow = max_overflow;
79 self
80 }
81
82 pub fn update_from_pointer(&mut self, pointer_x: f32, slider_width: f32) {
88 let range = self.max - self.min;
89
90 let mut new_value = self.min + (pointer_x / slider_width) * range;
92
93 if self.step > 0.0 {
95 new_value = (new_value / self.step).round() * self.step;
96 }
97
98 let overflow_distance = if pointer_x < 0.0 {
100 pointer_x
101 } else if pointer_x > slider_width {
102 pointer_x - slider_width
103 } else {
104 0.0
105 };
106
107 self.overflow = Self::decay(overflow_distance, self.max_overflow);
109
110 self.value = new_value.clamp(self.min, self.max);
112 }
113
114 #[cfg(feature = "animation")]
116 pub fn release(&mut self, current_time: f64) {
117 if self.overflow.abs() > 0.01 {
118 self.spring_state = Some(SpringState {
120 spring: Spring::new()
121 .stiffness(180.0)
122 .damping(12.0)
123 .mass(1.0),
124 });
125 self.spring_start_time = current_time;
126 }
127 }
128
129 #[cfg(not(feature = "animation"))]
131 pub fn release(&mut self, _current_time: f64) {
132 self.overflow = 0.0;
133 }
134
135 #[cfg(feature = "animation")]
137 pub fn update(&mut self, current_time: f64) {
138 if let Some(ref state) = self.spring_state {
139 let elapsed = current_time - self.spring_start_time;
140 let initial_overflow = self.overflow;
141
142 let (displacement, _velocity) = state.spring.evaluate(elapsed);
143
144 self.overflow = initial_overflow * displacement as f32;
146
147 if state.spring.is_at_rest(elapsed) {
149 self.overflow = 0.0;
150 self.spring_state = None;
151 }
152 }
153 }
154
155 #[cfg(not(feature = "animation"))]
157 pub fn update(&mut self, _current_time: f64) {}
158
159 pub fn overflow(&self) -> f32 {
161 self.overflow
162 }
163
164 pub fn overflow_region(&self) -> OverflowRegion {
166 if self.overflow < -0.01 {
167 OverflowRegion::Left
168 } else if self.overflow > 0.01 {
169 OverflowRegion::Right
170 } else {
171 OverflowRegion::None
172 }
173 }
174
175 pub fn fill_percentage(&self) -> f32 {
177 let range = self.max - self.min;
178 if range == 0.0 {
179 0.0
180 } else {
181 ((self.value - self.min) / range).clamp(0.0, 1.0)
182 }
183 }
184
185 fn decay(value: f32, max: f32) -> f32 {
190 if max == 0.0 {
191 return 0.0;
192 }
193
194 let entry = value / max;
195 let sigmoid = 2.0 * (1.0 / (1.0 + (-entry).exp()) - 0.5);
196 sigmoid * max
197 }
198}
199
200#[derive(Debug, Clone, Copy, PartialEq, Eq)]
202pub enum OverflowRegion {
203 Left,
204 None,
205 Right,
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211
212 #[test]
213 fn test_basic_slider() {
214 let mut slider = ElasticSlider::new(0.0, 100.0);
215
216 slider.update_from_pointer(50.0, 100.0);
218 assert!((slider.value - 50.0).abs() < 0.1);
219 assert_eq!(slider.overflow_region(), OverflowRegion::None);
220 }
221
222 #[test]
223 fn test_stepped_slider() {
224 let mut slider = ElasticSlider::new(0.0, 100.0).with_step(10.0);
225
226 slider.update_from_pointer(52.0, 100.0);
228 assert!((slider.value - 50.0).abs() < 0.1);
229 }
230
231 #[test]
232 fn test_overflow_left() {
233 let mut slider = ElasticSlider::new(0.0, 100.0);
234
235 slider.update_from_pointer(-20.0, 100.0);
237 assert_eq!(slider.overflow_region(), OverflowRegion::Left);
238 assert!(slider.overflow() < 0.0);
239 assert_eq!(slider.value, 0.0); }
241
242 #[test]
243 fn test_overflow_right() {
244 let mut slider = ElasticSlider::new(0.0, 100.0);
245
246 slider.update_from_pointer(120.0, 100.0);
248 assert_eq!(slider.overflow_region(), OverflowRegion::Right);
249 assert!(slider.overflow() > 0.0);
250 assert_eq!(slider.value, 100.0); }
252
253 #[test]
254 fn test_decay_function() {
255 assert_eq!(ElasticSlider::decay(0.0, 50.0), 0.0);
257
258 let result = ElasticSlider::decay(50.0, 50.0);
260 assert!(result > 20.0 && result < 30.0);
261
262 let small = ElasticSlider::decay(5.0, 50.0);
264 let large = ElasticSlider::decay(25.0, 50.0);
265 assert!(small < large);
266 }
267
268 #[test]
269 fn test_fill_percentage() {
270 let mut slider = ElasticSlider::new(0.0, 100.0);
271
272 slider.update_from_pointer(0.0, 100.0);
273 assert!((slider.fill_percentage() - 0.0).abs() < 0.01);
274
275 slider.update_from_pointer(50.0, 100.0);
276 assert!((slider.fill_percentage() - 0.5).abs() < 0.01);
277
278 slider.update_from_pointer(100.0, 100.0);
279 assert!((slider.fill_percentage() - 1.0).abs() < 0.01);
280 }
281
282 #[cfg(feature = "animation")]
283 #[test]
284 fn test_spring_release() {
285 let mut slider = ElasticSlider::new(0.0, 100.0);
286
287 slider.update_from_pointer(-20.0, 100.0);
289 let initial_overflow = slider.overflow();
290 assert!(initial_overflow < 0.0);
291
292 slider.release(0.0);
294
295 slider.update(0.1);
297 assert!(slider.overflow().abs() < initial_overflow.abs());
298
299 slider.update(2.0);
301 assert!(slider.overflow().abs() < 0.01);
302 }
303}