x11-overlay 0.1.0

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

fn rect_to_rect(rect: Rectangle) -> Rect {
    Rect::new(
        rect.x as i32,
        rect.y as i32,
        rect.width as u32,
        rect.height as u32,
    )
}

/// A status panel that displays system information with text and background
pub struct StatusPanel {
    title: TextComponent,
    lines: Vec<TextComponent>,
    background: Rectangle,
    background_color: Color,
    bounds: Rectangle,
}

impl StatusPanel {
    /// Calculate bounds for a status panel without creating it
    #[allow(dead_code)]
    pub fn calculate_bounds(title: &str, lines: &[&str]) -> Rectangle {
        // Calculate title size
        let title_component =
            TextComponent::auto_sized(title, 0, 0).with_font(FontDesc::new("DejaVu Sans", 40.0));
        let title_bounds = title_component.bounds();

        let mut max_width = title_bounds.width + 20; // title width + padding
        let mut total_height = title_bounds.height + 20; // title height + padding

        // Account for all lines
        for line_text in lines {
            let line = TextComponent::auto_sized(line_text, 0, 0)
                .with_font(FontDesc::new("DejaVu Sans", 32.0));
            let line_bounds = line.bounds();
            max_width = max_width.max(line_bounds.width + 20);
            total_height += line_bounds.height + 5; // line height + spacing
        }

        // Add bottom padding
        total_height += 15;

        Rectangle {
            x: 0,
            y: 0,
            width: max_width,
            height: total_height,
        }
    }

    pub fn new(title: &str, x: i16, y: i16, _width: u16, _height: u16) -> Self {
        // Create title component with proper font first
        let title_component = TextComponent::auto_sized(title, x + 10, y + 10)
            .with_font(FontDesc::new("DejaVu Sans", 40.0))
            .with_color(Color::WHITE);

        // Start with minimal background - will be resized as content is added
        let background = Rectangle {
            x,
            y,
            width: 100,
            height: 60,
        };

        let mut panel = Self {
            title: title_component,
            lines: Vec::new(),
            background,
            background_color: Color::rgba(0.0, 0.0, 0.0, 0.8),
            bounds: background,
        };

        // Recalculate size based on title
        panel.recalculate_size();
        panel
    }

    /// Recalculate the panel size based on all its content
    fn recalculate_size(&mut self) {
        let title_bounds = self.title.bounds();
        let mut max_width = title_bounds.width + 20; // title width + padding
        let mut total_height = title_bounds.height + 20; // title height + padding

        // Account for all lines
        for line in &self.lines {
            let line_bounds = line.bounds();
            max_width = max_width.max(line_bounds.width + 20);
            total_height += line_bounds.height + 5; // line height + spacing
        }

        // Add bottom padding
        total_height += 15;

        // Update background and bounds
        self.background.width = max_width;
        self.background.height = total_height;
        self.bounds = self.background;
    }

    pub fn add_line(&mut self, text: &str) {
        // Calculate Y position based on current content
        let mut current_y = self.bounds.y + self.title.bounds().height as i16 + 15;
        for line in &self.lines {
            current_y += line.bounds().height as i16 + 5;
        }

        // Create auto-sized line with proper font
        let line = TextComponent::auto_sized(text, self.bounds.x + 10, current_y)
            .with_font(FontDesc::new("DejaVu Sans", 32.0))
            .with_color(Color::rgba(0.9, 0.9, 0.9, 1.0));

        self.lines.push(line);

        // Recalculate the entire panel size based on all content
        self.recalculate_size();
    }

    pub fn with_background_color(mut self, color: Color) -> Self {
        self.background_color = color;
        self
    }

    #[allow(dead_code)]
    pub fn clear_lines(&mut self) {
        self.lines.clear();
    }

    #[allow(dead_code)]
    pub fn update_line(&mut self, index: usize, text: &str) {
        if let Some(line) = self.lines.get_mut(index) {
            line.set_text(text);
        }
    }
}

impl Component for StatusPanel {
    fn render(&self, graphics: &mut GraphicsContext) -> Result<()> {
        // Render background
        graphics.fill_rectangle(rect_to_rect(self.background), self.background_color)?;

        // Render title and all lines using functional composition
        std::iter::once(&self.title)
            .chain(self.lines.iter())
            .try_for_each(|component| component.render(graphics))
    }

    fn bounds(&self) -> Rectangle {
        self.bounds
    }

    fn update(&mut self, _delta_time: f64) -> bool {
        // Status panels could update their content periodically
        false
    }
}

/// A notification that displays text with an optional icon area
pub struct NotificationComponent {
    text: TextComponent,
    icon_area: Rectangle,
    background: Rectangle,
    background_color: Color,
    fade_timer: f64,
    lifetime: f64,
    bounds: Rectangle,
}

impl NotificationComponent {
    /// Calculate bounds for a notification without creating it
    #[allow(dead_code)]
    pub fn calculate_bounds(message: &str, min_width: u16) -> Rectangle {
        // Create temporary text component to get size
        let text =
            TextComponent::auto_sized(message, 0, 0).with_font(FontDesc::new("DejaVu Sans", 36.0));
        let text_bounds = text.bounds();

        let required_width = (text_bounds.width + 80).max(min_width);
        let height = (text_bounds.height + 30).max(60);

        Rectangle {
            x: 0,
            y: 0,
            width: required_width,
            height,
        }
    }

    pub fn new(message: &str, x: i16, y: i16, min_width: u16, lifetime_seconds: f64) -> Self {
        // Create auto-sized text component with proper font
        let text = TextComponent::auto_sized(message, x + 60, y + 15)
            .with_font(FontDesc::new("DejaVu Sans", 36.0))
            .with_color(Color::WHITE)
            .with_alignment(crate::graphics::Alignment::Left);

        // Calculate required dimensions based on actual text size
        let text_bounds = text.bounds();
        let required_width = (text_bounds.width + 80).max(min_width); // space for icon + padding
        let height = (text_bounds.height + 30).max(60); // minimum height for icon

        let background = Rectangle {
            x,
            y,
            width: required_width,
            height,
        };

        let icon_area = Rectangle {
            x: x + 10,
            y: y + 10,
            width: 40,
            height: 40,
        };

        Self {
            text,
            icon_area,
            background,
            background_color: Color::rgba(0.2, 0.2, 0.2, 0.9),
            fade_timer: 0.0,
            lifetime: lifetime_seconds,
            bounds: background,
        }
    }

    pub fn with_background_color(mut self, color: Color) -> Self {
        self.background_color = color;
        self
    }

    pub fn is_expired(&self) -> bool {
        self.fade_timer >= self.lifetime
    }

    pub fn get_alpha(&self) -> f64 {
        let fade_start = self.lifetime - 1.0;
        if self.fade_timer >= fade_start {
            // Fade out in the last second
            (self.lifetime - self.fade_timer).max(0.0)
        } else {
            1.0
        }
    }

    fn apply_alpha_to_color(&self, color: Color) -> Color {
        let alpha = self.get_alpha();
        Color {
            r: color.r,
            g: color.g,
            b: color.b,
            a: color.a * alpha,
        }
    }
}

impl Component for NotificationComponent {
    fn render(&self, graphics: &mut GraphicsContext) -> Result<()> {
        if self.get_alpha() <= 0.0 {
            return Ok(());
        }

        // Apply alpha to colors functionally
        let bg_color = self.apply_alpha_to_color(self.background_color);
        let icon_color = self.apply_alpha_to_color(Color::rgba(0.5, 0.5, 0.5, 1.0));

        // Render components in sequence
        [(self.background, bg_color), (self.icon_area, icon_color)]
            .iter()
            .try_for_each(|(rect, color)| graphics.fill_rectangle(rect_to_rect(*rect), *color))?;

        // Render text
        self.text.render(graphics)
    }

    fn bounds(&self) -> Rectangle {
        self.bounds
    }

    fn update(&mut self, delta_time: f64) -> bool {
        self.fade_timer += delta_time;
        true // Always request redraw for animations
    }

    fn should_remove(&self) -> bool {
        self.is_expired()
    }

    fn render_priority(&self) -> i32 {
        100 // Notifications render on top
    }
}

/// A progress bar with text label
pub struct ProgressBarComponent {
    label: TextComponent,
    bar_background: Rectangle,
    bar_foreground: Rectangle,
    progress: f64, // 0.0 to 1.0
    background_color: Color,
    foreground_color: Color,
    bounds: Rectangle,
}

impl ProgressBarComponent {
    pub fn new(label: &str, x: i16, y: i16, min_width: u16) -> Self {
        // Create auto-sized label with proper font
        let label_component = TextComponent::auto_sized(label, x + 5, y + 5)
            .with_font(FontDesc::new("DejaVu Sans", 32.0))
            .with_color(Color::WHITE);

        // Calculate required dimensions based on actual label size
        let label_bounds = label_component.bounds();
        let required_width = (label_bounds.width + 10).max(min_width);
        let height = label_bounds.height + 30; // label height + bar height + spacing

        let bounds = Rectangle {
            x,
            y,
            width: required_width,
            height,
        };

        let bar_y = y + label_bounds.height as i16 + 10;
        let bar_background = Rectangle {
            x: x + 5,
            y: bar_y,
            width: required_width - 10,
            height: 20, // taller bar for better visibility with larger fonts
        };

        let bar_foreground = Rectangle {
            x: x + 5,
            y: bar_y,
            width: 0, // Will be updated based on progress
            height: 20,
        };

        Self {
            label: label_component,
            bar_background,
            bar_foreground,
            progress: 0.0,
            background_color: Color::rgba(0.3, 0.3, 0.3, 1.0),
            foreground_color: Color::rgba(0.2, 0.8, 0.2, 1.0),
            bounds,
        }
    }

    pub fn set_progress(&mut self, progress: f64) {
        self.progress = progress.clamp(0.0, 1.0);
        self.bar_foreground.width = (self.bar_background.width as f64 * self.progress) as u16;
    }

    #[allow(dead_code)]
    pub fn set_label(&mut self, label: &str) {
        self.label.set_text(label);
    }

    pub fn with_colors(mut self, background: Color, foreground: Color) -> Self {
        self.background_color = background;
        self.foreground_color = foreground;
        self
    }
}

impl Component for ProgressBarComponent {
    fn render(&self, graphics: &mut GraphicsContext) -> Result<()> {
        // Render label
        self.label.render(graphics)?;

        // Render progress bar background
        graphics.fill_rectangle(rect_to_rect(self.bar_background), self.background_color)?;

        // Render progress bar foreground
        if self.bar_foreground.width > 0 {
            graphics.fill_rectangle(rect_to_rect(self.bar_foreground), self.foreground_color)?;
        }

        Ok(())
    }

    fn bounds(&self) -> Rectangle {
        self.bounds
    }

    fn update(&mut self, _delta_time: f64) -> bool {
        false // Progress bars don't animate by themselves
    }
}