x11-overlay 0.1.0

A library for creating overlay interfaces on X11 systems using Cairo for rendering
Documentation
#![allow(dead_code)]

use std::f32::consts::PI;

/// Easing function type
pub type EasingFn = fn(f32) -> f32;

/// Linear interpolation - no easing
pub fn linear(t: f32) -> f32 {
    t
}

/// Quadratic easing functions
pub mod quad {
    pub fn ease_in(t: f32) -> f32 {
        t * t
    }

    pub fn ease_out(t: f32) -> f32 {
        1.0 - (1.0 - t) * (1.0 - t)
    }

    pub fn ease_in_out(t: f32) -> f32 {
        if t < 0.5 {
            2.0 * t * t
        } else {
            1.0 - (-2.0 * t + 2.0).powi(2) / 2.0
        }
    }
}

/// Cubic easing functions
pub mod cubic {
    pub fn ease_in(t: f32) -> f32 {
        t * t * t
    }

    pub fn ease_out(t: f32) -> f32 {
        1.0 - (1.0 - t).powi(3)
    }

    pub fn ease_in_out(t: f32) -> f32 {
        if t < 0.5 {
            4.0 * t * t * t
        } else {
            1.0 - (-2.0 * t + 2.0).powi(3) / 2.0
        }
    }
}

/// Quartic easing functions
pub mod quart {
    pub fn ease_in(t: f32) -> f32 {
        t * t * t * t
    }

    pub fn ease_out(t: f32) -> f32 {
        1.0 - (1.0 - t).powi(4)
    }

    pub fn ease_in_out(t: f32) -> f32 {
        if t < 0.5 {
            8.0 * t * t * t * t
        } else {
            1.0 - (-2.0 * t + 2.0).powi(4) / 2.0
        }
    }
}

/// Sine wave easing functions
pub mod sine {
    use super::PI;

    pub fn ease_in(t: f32) -> f32 {
        1.0 - (t * PI / 2.0).cos()
    }

    pub fn ease_out(t: f32) -> f32 {
        (t * PI / 2.0).sin()
    }

    pub fn ease_in_out(t: f32) -> f32 {
        -(((t * PI).cos() - 1.0) / 2.0)
    }
}

/// Exponential easing functions
pub mod expo {
    pub fn ease_in(t: f32) -> f32 {
        if t == 0.0 {
            0.0
        } else {
            2.0_f32.powf(10.0 * (t - 1.0))
        }
    }

    pub fn ease_out(t: f32) -> f32 {
        if t == 1.0 {
            1.0
        } else {
            1.0 - 2.0_f32.powf(-10.0 * t)
        }
    }

    pub fn ease_in_out(t: f32) -> f32 {
        if t == 0.0 {
            0.0
        } else if t == 1.0 {
            1.0
        } else if t < 0.5 {
            2.0_f32.powf(20.0 * t - 10.0) / 2.0
        } else {
            (2.0 - 2.0_f32.powf(-20.0 * t + 10.0)) / 2.0
        }
    }
}

/// Circular easing functions
pub mod circ {
    pub fn ease_in(t: f32) -> f32 {
        1.0 - (1.0 - t * t).sqrt()
    }

    pub fn ease_out(t: f32) -> f32 {
        (1.0 - (t - 1.0) * (t - 1.0)).sqrt()
    }

    pub fn ease_in_out(t: f32) -> f32 {
        if t < 0.5 {
            (1.0 - (1.0 - (2.0 * t).powi(2)).sqrt()) / 2.0
        } else {
            ((1.0 - (-2.0 * t + 2.0).powi(2)).sqrt() + 1.0) / 2.0
        }
    }
}

/// Bounce easing functions
pub mod bounce {
    pub fn ease_out(t: f32) -> f32 {
        const N1: f32 = 7.5625;
        const D1: f32 = 2.75;

        if t < 1.0 / D1 {
            N1 * t * t
        } else if t < 2.0 / D1 {
            N1 * (t - 1.5 / D1) * (t - 1.5 / D1) + 0.75
        } else if t < 2.5 / D1 {
            N1 * (t - 2.25 / D1) * (t - 2.25 / D1) + 0.9375
        } else {
            N1 * (t - 2.625 / D1) * (t - 2.625 / D1) + 0.984375
        }
    }

    pub fn ease_in(t: f32) -> f32 {
        1.0 - ease_out(1.0 - t)
    }

    pub fn ease_in_out(t: f32) -> f32 {
        if t < 0.5 {
            (1.0 - ease_out(1.0 - 2.0 * t)) / 2.0
        } else {
            (1.0 + ease_out(2.0 * t - 1.0)) / 2.0
        }
    }
}

/// Elastic easing functions
pub mod elastic {
    use super::PI;

    const C4: f32 = (2.0 * PI) / 3.0;
    const C5: f32 = (2.0 * PI) / 4.5;

    pub fn ease_in(t: f32) -> f32 {
        if t == 0.0 {
            0.0
        } else if t == 1.0 {
            1.0
        } else {
            -2.0_f32.powf(10.0 * t - 10.0) * ((t * 10.0 - 10.75) * C4).sin()
        }
    }

    pub fn ease_out(t: f32) -> f32 {
        if t == 0.0 {
            0.0
        } else if t == 1.0 {
            1.0
        } else {
            2.0_f32.powf(-10.0 * t) * ((t * 10.0 - 0.75) * C4).sin() + 1.0
        }
    }

    pub fn ease_in_out(t: f32) -> f32 {
        if t == 0.0 {
            0.0
        } else if t == 1.0 {
            1.0
        } else if t < 0.5 {
            -(2.0_f32.powf(20.0 * t - 10.0) * ((20.0 * t - 11.125) * C5).sin()) / 2.0
        } else {
            (2.0_f32.powf(-20.0 * t + 10.0) * ((20.0 * t - 11.125) * C5).sin()) / 2.0 + 1.0
        }
    }
}

/// Apply easing to interpolate between two values
pub fn interpolate<T>(start: T, end: T, t: f32, easing: EasingFn) -> T
where
    T: std::ops::Add<Output = T>
        + std::ops::Sub<Output = T>
        + std::ops::Mul<f32, Output = T>
        + Copy,
{
    let eased_t = easing(t.clamp(0.0, 1.0));
    start + (end - start) * eased_t
}

/// Commonly used easing presets
pub mod presets {
    use super::*;

    pub const EASE: EasingFn = cubic::ease_in_out;
    pub const EASE_IN: EasingFn = cubic::ease_in;
    pub const EASE_OUT: EasingFn = cubic::ease_out;
    pub const EASE_IN_OUT: EasingFn = cubic::ease_in_out;
}

#[cfg(test)]
mod tests {
    use super::*;
    use approx::assert_abs_diff_eq;

    #[test]
    fn test_linear_easing() {
        assert_abs_diff_eq!(linear(0.0), 0.0, epsilon = 1e-6);
        assert_abs_diff_eq!(linear(0.5), 0.5, epsilon = 1e-6);
        assert_abs_diff_eq!(linear(1.0), 1.0, epsilon = 1e-6);
    }

    #[test]
    fn test_quad_easing_boundaries() {
        // Test that all easing functions start at 0 and end at 1
        assert_abs_diff_eq!(quad::ease_in(0.0), 0.0, epsilon = 1e-6);
        assert_abs_diff_eq!(quad::ease_in(1.0), 1.0, epsilon = 1e-6);

        assert_abs_diff_eq!(quad::ease_out(0.0), 0.0, epsilon = 1e-6);
        assert_abs_diff_eq!(quad::ease_out(1.0), 1.0, epsilon = 1e-6);

        assert_abs_diff_eq!(quad::ease_in_out(0.0), 0.0, epsilon = 1e-6);
        assert_abs_diff_eq!(quad::ease_in_out(1.0), 1.0, epsilon = 1e-6);
    }

    #[test]
    fn test_cubic_easing_boundaries() {
        assert_abs_diff_eq!(cubic::ease_in(0.0), 0.0, epsilon = 1e-6);
        assert_abs_diff_eq!(cubic::ease_in(1.0), 1.0, epsilon = 1e-6);

        assert_abs_diff_eq!(cubic::ease_out(0.0), 0.0, epsilon = 1e-6);
        assert_abs_diff_eq!(cubic::ease_out(1.0), 1.0, epsilon = 1e-6);

        assert_abs_diff_eq!(cubic::ease_in_out(0.0), 0.0, epsilon = 1e-6);
        assert_abs_diff_eq!(cubic::ease_in_out(1.0), 1.0, epsilon = 1e-6);
    }

    #[test]
    fn test_sine_easing_boundaries() {
        assert_abs_diff_eq!(sine::ease_in(0.0), 0.0, epsilon = 1e-6);
        assert_abs_diff_eq!(sine::ease_in(1.0), 1.0, epsilon = 1e-6);

        assert_abs_diff_eq!(sine::ease_out(0.0), 0.0, epsilon = 1e-6);
        assert_abs_diff_eq!(sine::ease_out(1.0), 1.0, epsilon = 1e-6);

        assert_abs_diff_eq!(sine::ease_in_out(0.0), 0.0, epsilon = 1e-6);
        assert_abs_diff_eq!(sine::ease_in_out(1.0), 1.0, epsilon = 1e-6);
    }

    #[test]
    fn test_bounce_easing_boundaries() {
        assert_abs_diff_eq!(bounce::ease_out(0.0), 0.0, epsilon = 1e-6);
        assert_abs_diff_eq!(bounce::ease_out(1.0), 1.0, epsilon = 1e-6);
    }

    #[test]
    fn test_interpolate_function() {
        // Test interpolate with different types
        assert_abs_diff_eq!(interpolate(0.0, 10.0, 0.5, linear), 5.0, epsilon = 1e-6);
        assert_abs_diff_eq!(interpolate(0.0, 10.0, 0.0, linear), 0.0, epsilon = 1e-6);
        assert_abs_diff_eq!(interpolate(0.0, 10.0, 1.0, linear), 10.0, epsilon = 1e-6);

        // Test with cubic easing
        let result = interpolate(0.0, 10.0, 0.5, cubic::ease_in_out);
        assert!((0.0..=10.0).contains(&result));
    }

    #[test]
    fn test_easing_monotonicity() {
        // Test that ease_in functions are monotonically increasing
        let values = [0.0, 0.25, 0.5, 0.75, 1.0];

        for i in 0..values.len() - 1 {
            assert!(quad::ease_in(values[i]) <= quad::ease_in(values[i + 1]));
            assert!(cubic::ease_in(values[i]) <= cubic::ease_in(values[i + 1]));
            assert!(sine::ease_in(values[i]) <= sine::ease_in(values[i + 1]));
        }
    }

    #[test]
    fn test_ease_in_out_symmetry() {
        // Test that ease_in_out functions are symmetric around t=0.5
        let test_values = [0.1, 0.2, 0.3, 0.4];

        for &t in &test_values {
            let quad_left = quad::ease_in_out(t);
            let quad_right = 1.0 - quad::ease_in_out(1.0 - t);
            assert_abs_diff_eq!(quad_left, quad_right, epsilon = 1e-5);

            let cubic_left = cubic::ease_in_out(t);
            let cubic_right = 1.0 - cubic::ease_in_out(1.0 - t);
            assert_abs_diff_eq!(cubic_left, cubic_right, epsilon = 1e-5);
        }
    }

    #[test]
    fn test_bounce_characteristics() {
        // Bounce should have multiple peaks and valleys
        let bounce_values: Vec<f32> = (0..=20)
            .map(|i| bounce::ease_out(i as f32 / 20.0))
            .collect();

        // Should have some local maxima (bounces)
        let mut local_maxima = 0;
        for i in 1..bounce_values.len() - 1 {
            if bounce_values[i] > bounce_values[i - 1] && bounce_values[i] > bounce_values[i + 1] {
                local_maxima += 1;
            }
        }

        assert!(local_maxima >= 2, "Bounce should have multiple peaks");
    }

    #[test]
    fn test_elastic_characteristics() {
        // Elastic should oscillate and potentially go outside [0,1] range temporarily
        let elastic_values: Vec<f32> = (1..20)
            .map(|i| elastic::ease_out(i as f32 / 20.0))
            .collect();

        // Should have some oscillation - values that go above or below the trend
        let has_overshoot = elastic_values.iter().any(|&v| !(-0.01..=1.01).contains(&v));
        assert!(has_overshoot, "Elastic should overshoot the target range");
    }

    mod property_tests {
        use super::*;
        use proptest::prelude::*;

        proptest! {
            #[test]
            fn test_easing_functions_stay_finite(t in 0.0f32..=1.0f32) {
                // All easing functions should produce finite values for valid input
                assert!(linear(t).is_finite());
                assert!(quad::ease_in(t).is_finite());
                assert!(quad::ease_out(t).is_finite());
                assert!(quad::ease_in_out(t).is_finite());
                assert!(cubic::ease_in(t).is_finite());
                assert!(cubic::ease_out(t).is_finite());
                assert!(cubic::ease_in_out(t).is_finite());
                assert!(sine::ease_in(t).is_finite());
                assert!(sine::ease_out(t).is_finite());
                assert!(sine::ease_in_out(t).is_finite());
                assert!(bounce::ease_out(t).is_finite());
            }

            #[test]
            fn test_easing_functions_boundaries_property(t in 0.0f32..=1.0f32) {
                // For t=0, all functions should return 0
                if (t - 0.0).abs() < f32::EPSILON {
                    assert_abs_diff_eq!(quad::ease_in(t), 0.0, epsilon = 1e-6);
                    assert_abs_diff_eq!(cubic::ease_in(t), 0.0, epsilon = 1e-6);
                }

                // For t=1, all functions should return 1
                if (t - 1.0).abs() < f32::EPSILON {
                    assert_abs_diff_eq!(quad::ease_in(t), 1.0, epsilon = 1e-6);
                    assert_abs_diff_eq!(cubic::ease_in(t), 1.0, epsilon = 1e-6);
                }
            }
        }
    }
}