x11-overlay 0.1.0

A library for creating overlay interfaces on X11 systems using Cairo for rendering
Documentation
use super::font::{FontDesc, FontManager};
use super::Color;
use anyhow::{Context, Result};
use cairo::{Context as CairoContext, Format, ImageSurface};

#[derive(Debug, Clone)]
pub struct TextStyle {
    pub font: FontDesc,
    pub color: Color,
    pub background: Option<Color>,
}

impl TextStyle {
    pub fn new(font: FontDesc, color: Color) -> Self {
        Self {
            font,
            color,
            background: None,
        }
    }

    pub fn with_background(mut self, background: Color) -> Self {
        self.background = Some(background);
        self
    }
}

impl Default for TextStyle {
    fn default() -> Self {
        Self {
            font: FontDesc::new("sans-serif", 12.0),
            color: Color::WHITE,
            background: None,
        }
    }
}

#[derive(Debug)]
pub struct TextMetrics {
    pub width: f64,
    pub height: f64,
    pub ascent: f64,
    pub descent: f64,
    pub x_advance: f64,
    pub y_advance: f64,
}

pub struct TextRenderer {
    font_manager: FontManager,
    surface: ImageSurface,
    context: CairoContext,
}

impl TextRenderer {
    pub fn new(width: i32, height: i32) -> Result<Self> {
        let surface = ImageSurface::create(Format::ARgb32, width, height)
            .context("Failed to create Cairo surface")?;

        let context = CairoContext::new(&surface).context("Failed to create Cairo context")?;

        let font_manager = FontManager::new().context("Failed to initialize font manager")?;

        Ok(Self {
            font_manager,
            surface,
            context,
        })
    }

    pub fn resize(&mut self, width: i32, height: i32) -> Result<()> {
        self.surface = ImageSurface::create(Format::ARgb32, width, height)
            .context("Failed to recreate Cairo surface")?;

        self.context =
            CairoContext::new(&self.surface).context("Failed to recreate Cairo context")?;

        Ok(())
    }

    pub fn clear(&self) {
        self.context.save().unwrap();
        self.context.set_operator(cairo::Operator::Clear);
        self.context.paint().unwrap();
        self.context.restore().unwrap();
    }

    pub fn clear_with_color(&self, color: Color) {
        self.context.save().unwrap();
        self.context
            .set_source_rgba(color.r, color.g, color.b, color.a);
        self.context.set_operator(cairo::Operator::Source);
        self.context.paint().unwrap();
        self.context.restore().unwrap();
    }

    fn apply_font(&self, font_desc: &FontDesc) -> Result<()> {
        self.context.select_font_face(
            &font_desc.family,
            font_desc.slant.to_cairo_slant(),
            font_desc.weight.to_cairo_weight(),
        );
        self.context.set_font_size(font_desc.size);
        Ok(())
    }

    pub fn measure_text(&self, text: &str, style: &TextStyle) -> Result<TextMetrics> {
        self.apply_font(&style.font)?;

        let text_extents = self
            .context
            .text_extents(text)
            .context("Failed to get text extents")?;

        let font_extents = self
            .context
            .font_extents()
            .context("Failed to get font extents")?;

        Ok(TextMetrics {
            width: text_extents.width(),
            height: text_extents.height(),
            ascent: font_extents.ascent(),
            descent: font_extents.descent(),
            x_advance: text_extents.x_advance(),
            y_advance: text_extents.y_advance(),
        })
    }

    pub fn render_text(&self, text: &str, x: f64, y: f64, style: &TextStyle) -> Result<()> {
        self.apply_font(&style.font)?;

        if let Some(bg_color) = style.background {
            let metrics = self.measure_text(text, style)?;

            self.context.save().unwrap();
            self.context
                .set_source_rgba(bg_color.r, bg_color.g, bg_color.b, bg_color.a);
            self.context
                .rectangle(x, y - metrics.ascent, metrics.width, metrics.height);
            self.context.fill().unwrap();
            self.context.restore().unwrap();
        }

        self.context.move_to(x, y);
        self.context
            .set_source_rgba(style.color.r, style.color.g, style.color.b, style.color.a);
        self.context
            .show_text(text)
            .context("Failed to render text")?;

        Ok(())
    }

    pub fn render_text_multiline(
        &self,
        text: &str,
        x: f64,
        y: f64,
        max_width: f64,
        style: &TextStyle,
    ) -> Result<f64> {
        let metrics = self.measure_text("M", style)?;
        let line_height = metrics.ascent + metrics.descent;
        let mut current_y = y;

        let lines = self.wrap_text(text, max_width, style)?;

        for line in lines {
            self.render_text(&line, x, current_y, style)?;
            current_y += line_height;
        }

        Ok(current_y - y)
    }

    fn wrap_text(&self, text: &str, max_width: f64, style: &TextStyle) -> Result<Vec<String>> {
        let mut lines = Vec::new();
        let words: Vec<&str> = text.split_whitespace().collect();

        if words.is_empty() {
            return Ok(lines);
        }

        let mut current_line = String::new();

        for word in words {
            let test_line = if current_line.is_empty() {
                word.to_string()
            } else {
                format!("{} {}", current_line, word)
            };

            let metrics = self.measure_text(&test_line, style)?;

            if metrics.width <= max_width {
                current_line = test_line;
            } else {
                if !current_line.is_empty() {
                    lines.push(current_line);
                }
                current_line = word.to_string();
            }
        }

        if !current_line.is_empty() {
            lines.push(current_line);
        }

        Ok(lines)
    }

    pub fn get_surface_data(&mut self) -> Result<Vec<u8>> {
        // Ensure all drawing operations are complete
        self.surface.flush();

        // Drop the current context to release exclusive access to the surface
        let _width = self.surface.width();
        let _height = self.surface.height();

        // Create a dummy surface to temporarily replace the context
        // This releases the surface for exclusive access
        let dummy_surface =
            ImageSurface::create(Format::ARgb32, 1, 1).context("Failed to create dummy surface")?;
        let dummy_context =
            CairoContext::new(&dummy_surface).context("Failed to create dummy context")?;

        // Replace the context temporarily
        let _old_context = std::mem::replace(&mut self.context, dummy_context);

        // Now we can safely access the surface data
        let data_result = self
            .surface
            .data()
            .context("Failed to get surface data")
            .map(|data| data.to_vec());

        // Recreate the context
        self.context = CairoContext::new(&self.surface)
            .context("Failed to recreate Cairo context after surface data access")?;

        data_result
    }

    pub fn get_width(&self) -> i32 {
        self.surface.width()
    }

    pub fn get_height(&self) -> i32 {
        self.surface.height()
    }

    pub fn font_manager(&self) -> &FontManager {
        &self.font_manager
    }
}