#[cfg(feature = "animation")]
use crate::animation::Spring;
#[derive(Debug, Clone)]
pub struct ElasticSlider {
pub value: f32,
pub min: f32,
pub max: f32,
pub step: f32,
pub max_overflow: f32,
overflow: f32,
#[cfg(feature = "animation")]
spring_state: Option<SpringState>,
#[cfg(feature = "animation")]
spring_start_time: f64,
}
#[cfg(feature = "animation")]
#[derive(Debug, Clone)]
struct SpringState {
spring: Spring,
}
impl Default for ElasticSlider {
fn default() -> Self {
Self::new(0.0, 100.0)
}
}
impl ElasticSlider {
pub fn new(min: f32, max: f32) -> Self {
Self {
value: (min + max) / 2.0,
min,
max,
step: 0.0,
max_overflow: 50.0,
overflow: 0.0,
#[cfg(feature = "animation")]
spring_state: None,
#[cfg(feature = "animation")]
spring_start_time: 0.0,
}
}
pub fn with_step(mut self, step: f32) -> Self {
self.step = step;
self
}
pub fn with_max_overflow(mut self, max_overflow: f32) -> Self {
self.max_overflow = max_overflow;
self
}
pub fn update_from_pointer(&mut self, pointer_x: f32, slider_width: f32) {
let range = self.max - self.min;
let mut new_value = self.min + (pointer_x / slider_width) * range;
if self.step > 0.0 {
new_value = (new_value / self.step).round() * self.step;
}
let overflow_distance = if pointer_x < 0.0 {
pointer_x
} else if pointer_x > slider_width {
pointer_x - slider_width
} else {
0.0
};
self.overflow = Self::decay(overflow_distance, self.max_overflow);
self.value = new_value.clamp(self.min, self.max);
}
#[cfg(feature = "animation")]
pub fn release(&mut self, current_time: f64) {
if self.overflow.abs() > 0.01 {
self.spring_state = Some(SpringState {
spring: Spring::new()
.stiffness(180.0)
.damping(12.0)
.mass(1.0),
});
self.spring_start_time = current_time;
}
}
#[cfg(not(feature = "animation"))]
pub fn release(&mut self, _current_time: f64) {
self.overflow = 0.0;
}
#[cfg(feature = "animation")]
pub fn update(&mut self, current_time: f64) {
if let Some(ref state) = self.spring_state {
let elapsed = current_time - self.spring_start_time;
let initial_overflow = self.overflow;
let (displacement, _velocity) = state.spring.evaluate(elapsed);
self.overflow = initial_overflow * displacement as f32;
if state.spring.is_at_rest(elapsed) {
self.overflow = 0.0;
self.spring_state = None;
}
}
}
#[cfg(not(feature = "animation"))]
pub fn update(&mut self, _current_time: f64) {}
pub fn overflow(&self) -> f32 {
self.overflow
}
pub fn overflow_region(&self) -> OverflowRegion {
if self.overflow < -0.01 {
OverflowRegion::Left
} else if self.overflow > 0.01 {
OverflowRegion::Right
} else {
OverflowRegion::None
}
}
pub fn fill_percentage(&self) -> f32 {
let range = self.max - self.min;
if range == 0.0 {
0.0
} else {
((self.value - self.min) / range).clamp(0.0, 1.0)
}
}
fn decay(value: f32, max: f32) -> f32 {
if max == 0.0 {
return 0.0;
}
let entry = value / max;
let sigmoid = 2.0 * (1.0 / (1.0 + (-entry).exp()) - 0.5);
sigmoid * max
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OverflowRegion {
Left,
None,
Right,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_slider() {
let mut slider = ElasticSlider::new(0.0, 100.0);
slider.update_from_pointer(50.0, 100.0);
assert!((slider.value - 50.0).abs() < 0.1);
assert_eq!(slider.overflow_region(), OverflowRegion::None);
}
#[test]
fn test_stepped_slider() {
let mut slider = ElasticSlider::new(0.0, 100.0).with_step(10.0);
slider.update_from_pointer(52.0, 100.0);
assert!((slider.value - 50.0).abs() < 0.1);
}
#[test]
fn test_overflow_left() {
let mut slider = ElasticSlider::new(0.0, 100.0);
slider.update_from_pointer(-20.0, 100.0);
assert_eq!(slider.overflow_region(), OverflowRegion::Left);
assert!(slider.overflow() < 0.0);
assert_eq!(slider.value, 0.0); }
#[test]
fn test_overflow_right() {
let mut slider = ElasticSlider::new(0.0, 100.0);
slider.update_from_pointer(120.0, 100.0);
assert_eq!(slider.overflow_region(), OverflowRegion::Right);
assert!(slider.overflow() > 0.0);
assert_eq!(slider.value, 100.0); }
#[test]
fn test_decay_function() {
assert_eq!(ElasticSlider::decay(0.0, 50.0), 0.0);
let result = ElasticSlider::decay(50.0, 50.0);
assert!(result > 20.0 && result < 30.0);
let small = ElasticSlider::decay(5.0, 50.0);
let large = ElasticSlider::decay(25.0, 50.0);
assert!(small < large);
}
#[test]
fn test_fill_percentage() {
let mut slider = ElasticSlider::new(0.0, 100.0);
slider.update_from_pointer(0.0, 100.0);
assert!((slider.fill_percentage() - 0.0).abs() < 0.01);
slider.update_from_pointer(50.0, 100.0);
assert!((slider.fill_percentage() - 0.5).abs() < 0.01);
slider.update_from_pointer(100.0, 100.0);
assert!((slider.fill_percentage() - 1.0).abs() < 0.01);
}
#[cfg(feature = "animation")]
#[test]
fn test_spring_release() {
let mut slider = ElasticSlider::new(0.0, 100.0);
slider.update_from_pointer(-20.0, 100.0);
let initial_overflow = slider.overflow();
assert!(initial_overflow < 0.0);
slider.release(0.0);
slider.update(0.1);
assert!(slider.overflow().abs() < initial_overflow.abs());
slider.update(2.0);
assert!(slider.overflow().abs() < 0.01);
}
}