x11-overlay 0.1.0

A library for creating overlay interfaces on X11 systems using Cairo for rendering
Documentation
use crate::animation::{AnimationProperty, AnimationTarget};
use crate::graphics::renderer::Color;
use crate::graphics::{GraphicsContext, Rectangle};
use crate::ui::Component;
use anyhow::Result;

#[derive(Debug, Clone, Copy)]
#[allow(dead_code)]
pub enum Position {
    TopLeft,
    TopRight,
    BottomLeft,
    #[allow(dead_code)]
    BottomRight,
    Custom {
        x: i16,
        y: i16,
    },
}

pub struct StatusIndicator {
    position: Position,
    size: u16,
    color: Color,
    screen_width: u16,
    screen_height: u16,
    #[allow(dead_code)]
    alpha: f32,
    #[allow(dead_code)]
    scale: f32,
}

impl StatusIndicator {
    pub fn new(
        position: Position,
        size: u16,
        color: Color,
        screen_width: u16,
        screen_height: u16,
    ) -> Self {
        Self {
            position,
            size,
            color,
            screen_width,
            screen_height,
            alpha: 1.0,
            scale: 1.0,
        }
    }

    #[allow(dead_code)]
    pub fn green_square(screen_width: u16, screen_height: u16) -> Self {
        Self::new(
            Position::TopRight,
            50,
            Color::GREEN,
            screen_width,
            screen_height,
        )
    }

    fn calculate_position(&self) -> (i16, i16) {
        match self.position {
            Position::TopLeft => (0, 0),
            Position::TopRight => ((self.screen_width - self.size) as i16, 0),
            Position::BottomLeft => (0, (self.screen_height - self.size) as i16),
            Position::BottomRight => (
                (self.screen_width - self.size) as i16,
                (self.screen_height - self.size) as i16,
            ),
            Position::Custom { x, y } => (x, y),
        }
    }
}

impl Component for StatusIndicator {
    fn render(&self, graphics: &mut GraphicsContext) -> Result<()> {
        let (x, y) = self.calculate_position();
        let rect = Rectangle {
            x,
            y,
            width: self.size,
            height: self.size,
        };

        graphics.renderer().fill_rectangle(rect, self.color)
    }

    fn bounds(&self) -> Rectangle {
        let (x, y) = self.calculate_position();
        Rectangle {
            x,
            y,
            width: self.size,
            height: self.size,
        }
    }

    fn update(&mut self, _delta_time: f64) -> bool {
        false // Status indicators don't need updates
    }
}

impl AnimationTarget for StatusIndicator {
    fn set_property(&mut self, property: AnimationProperty) -> Result<()> {
        match property {
            AnimationProperty::Position { x, y } => {
                self.position = Position::Custom { x, y };
            }
            AnimationProperty::Alpha(alpha) => {
                self.alpha = alpha.clamp(0.0, 1.0);
            }
            AnimationProperty::Scale(scale) => {
                self.scale = scale.max(0.0);
            }
            AnimationProperty::Color { r, g, b } => {
                self.color = Color::new((self.alpha * 255.0) as u8, r, g, b);
            }
        }
        Ok(())
    }
}

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

    #[test]
    fn test_status_indicator_creation() {
        let indicator = StatusIndicator::new(Position::TopLeft, 50, Color::RED, 1920, 1080);

        assert_eq!(indicator.size, 50);
        assert_eq!(indicator.screen_width, 1920);
        assert_eq!(indicator.screen_height, 1080);
        assert_eq!(indicator.alpha, 1.0);
        assert_eq!(indicator.scale, 1.0);
        assert_eq!(indicator.color.argb, Color::RED.argb);
    }

    #[test]
    fn test_green_square_factory() {
        let indicator = StatusIndicator::green_square(1920, 1080);

        assert_eq!(indicator.size, 50);
        assert_eq!(indicator.color.argb, Color::GREEN.argb);
        // Should be positioned at TopRight
        let (x, y) = indicator.calculate_position();
        assert_eq!(x, 1920 - 50);
        assert_eq!(y, 0);
    }

    #[test]
    fn test_position_calculation() {
        let screen_width = 1920;
        let screen_height = 1080;
        let size = 50;

        let top_left = StatusIndicator::new(
            Position::TopLeft,
            size,
            Color::RED,
            screen_width,
            screen_height,
        );
        assert_eq!(top_left.calculate_position(), (0, 0));

        let top_right = StatusIndicator::new(
            Position::TopRight,
            size,
            Color::RED,
            screen_width,
            screen_height,
        );
        assert_eq!(top_right.calculate_position(), (1870, 0));

        let bottom_left = StatusIndicator::new(
            Position::BottomLeft,
            size,
            Color::RED,
            screen_width,
            screen_height,
        );
        assert_eq!(bottom_left.calculate_position(), (0, 1030));

        let bottom_right = StatusIndicator::new(
            Position::BottomRight,
            size,
            Color::RED,
            screen_width,
            screen_height,
        );
        assert_eq!(bottom_right.calculate_position(), (1870, 1030));

        let custom = StatusIndicator::new(
            Position::Custom { x: 100, y: 200 },
            size,
            Color::RED,
            screen_width,
            screen_height,
        );
        assert_eq!(custom.calculate_position(), (100, 200));
    }

    #[test]
    fn test_bounds_calculation() {
        let indicator = StatusIndicator::new(
            Position::Custom { x: 100, y: 200 },
            50,
            Color::RED,
            1920,
            1080,
        );

        let bounds = indicator.bounds();
        assert_eq!(bounds.x, 100);
        assert_eq!(bounds.y, 200);
        assert_eq!(bounds.width, 50);
        assert_eq!(bounds.height, 50);
    }

    #[test]
    fn test_animation_property_setters() {
        let mut indicator = StatusIndicator::new(Position::TopLeft, 50, Color::RED, 1920, 1080);

        // Test position property
        indicator
            .set_property(AnimationProperty::Position { x: 100, y: 200 })
            .unwrap();
        assert_eq!(indicator.calculate_position(), (100, 200));

        // Test alpha property
        indicator
            .set_property(AnimationProperty::Alpha(0.5))
            .unwrap();
        assert_eq!(indicator.alpha, 0.5);

        // Test alpha clamping
        indicator
            .set_property(AnimationProperty::Alpha(1.5))
            .unwrap();
        assert_eq!(indicator.alpha, 1.0);

        indicator
            .set_property(AnimationProperty::Alpha(-0.5))
            .unwrap();
        assert_eq!(indicator.alpha, 0.0);

        // Test scale property
        indicator
            .set_property(AnimationProperty::Scale(2.0))
            .unwrap();
        assert_eq!(indicator.scale, 2.0);

        // Test scale minimum value
        indicator
            .set_property(AnimationProperty::Scale(-1.0))
            .unwrap();
        assert_eq!(indicator.scale, 0.0);

        // Test color property
        indicator
            .set_property(AnimationProperty::Color {
                r: 100,
                g: 150,
                b: 200,
            })
            .unwrap();
        let expected_color = Color::new((indicator.alpha * 255.0) as u8, 100, 150, 200);
        assert_eq!(indicator.color.argb, expected_color.argb);
    }

    #[test]
    fn test_position_enum_debug() {
        let positions = [
            Position::TopLeft,
            Position::TopRight,
            Position::BottomLeft,
            Position::BottomRight,
            Position::Custom { x: 10, y: 20 },
        ];

        for pos in &positions {
            let debug_str = format!("{:?}", pos);
            assert!(!debug_str.is_empty());
        }
    }

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

        proptest! {
            #[test]
            fn test_position_calculation_bounds(
                screen_width in 100u16..5000,
                screen_height in 100u16..5000,
                size in 1u16..100  // Ensure size is smaller than minimum screen dimension
            ) {
                // Only test when size fits within screen dimensions
                prop_assume!(size < screen_width && size < screen_height);

                let indicator = StatusIndicator::new(
                    Position::TopRight,
                    size,
                    Color::RED,
                    screen_width,
                    screen_height,
                );

                let (x, y) = indicator.calculate_position();

                // Position should be within screen bounds
                assert!(x >= 0);
                assert!(y >= 0);
                assert!(x + size as i16 <= screen_width as i16);
                assert!(y + size as i16 <= screen_height as i16);
            }

            #[test]
            fn test_alpha_clamping_property(alpha_input in -10.0f32..20.0) {
                let mut indicator = StatusIndicator::new(
                    Position::TopLeft,
                    50,
                    Color::RED,
                    1920,
                    1080,
                );

                indicator.set_property(AnimationProperty::Alpha(alpha_input)).unwrap();

                // Alpha should always be clamped to [0.0, 1.0]
                assert!(indicator.alpha >= 0.0);
                assert!(indicator.alpha <= 1.0);
            }

            #[test]
            fn test_scale_minimum_property(scale_input in -100.0f32..100.0) {
                let mut indicator = StatusIndicator::new(
                    Position::TopLeft,
                    50,
                    Color::RED,
                    1920,
                    1080,
                );

                indicator.set_property(AnimationProperty::Scale(scale_input)).unwrap();

                // Scale should never be negative
                assert!(indicator.scale >= 0.0);
            }

            #[test]
            fn test_custom_position_property(x: i16, y: i16) {
                let mut indicator = StatusIndicator::new(
                    Position::TopLeft,
                    50,
                    Color::RED,
                    1920,
                    1080,
                );

                indicator.set_property(AnimationProperty::Position { x, y }).unwrap();

                assert_eq!(indicator.calculate_position(), (x, y));
            }
        }
    }
}