x11-overlay 0.1.0

A library for creating overlay interfaces on X11 systems using Cairo for rendering
Documentation
use super::font::FontDesc;
use super::text::{TextMetrics, TextRenderer, TextStyle};
use super::{Color, Rect};
use anyhow::Result;

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Alignment {
    Left,
    Center,
    Right,
    Justify,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum VerticalAlignment {
    Top,
    Middle,
    Bottom,
    Baseline,
}

#[derive(Debug, Clone)]
pub struct TextLayout {
    text: String,
    style: TextStyle,
    bounds: Option<Rect>,
    alignment: Alignment,
    vertical_alignment: VerticalAlignment,
    line_spacing: f64,
    wrap: bool,
    ellipsis: bool,
}

impl TextLayout {
    pub fn new() -> Self {
        Self {
            text: String::new(),
            style: TextStyle::default(),
            bounds: None,
            alignment: Alignment::Left,
            vertical_alignment: VerticalAlignment::Baseline,
            line_spacing: 1.2,
            wrap: false,
            ellipsis: false,
        }
    }

    pub fn text<S: Into<String>>(mut self, text: S) -> Self {
        self.text = text.into();
        self
    }

    pub fn style(mut self, style: TextStyle) -> Self {
        self.style = style;
        self
    }

    pub fn font(mut self, font: FontDesc) -> Self {
        self.style.font = font;
        self
    }

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

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

    pub fn bounds(mut self, bounds: Rect) -> Self {
        self.bounds = Some(bounds);
        self
    }

    pub fn alignment(mut self, alignment: Alignment) -> Self {
        self.alignment = alignment;
        self
    }

    pub fn vertical_alignment(mut self, alignment: VerticalAlignment) -> Self {
        self.vertical_alignment = alignment;
        self
    }

    pub fn line_spacing(mut self, spacing: f64) -> Self {
        self.line_spacing = spacing;
        self
    }

    pub fn wrap(mut self, wrap: bool) -> Self {
        self.wrap = wrap;
        self
    }

    pub fn ellipsis(mut self, ellipsis: bool) -> Self {
        self.ellipsis = ellipsis;
        self
    }

    // Getter methods for accessing private fields
    pub fn get_text(&self) -> &str {
        &self.text
    }

    pub fn get_font(&self) -> &FontDesc {
        &self.style.font
    }

    pub fn get_color(&self) -> &Color {
        &self.style.color
    }

    pub fn measure(&self, renderer: &TextRenderer) -> Result<TextMetrics> {
        if let Some(bounds) = self.bounds {
            if self.wrap {
                let max_width = bounds.width as f64;
                let lines = self.get_wrapped_lines(renderer, max_width)?;
                let line_metrics = renderer.measure_text("M", &self.style)?;
                let line_height = line_metrics.ascent + line_metrics.descent;
                let total_height = lines.len() as f64 * line_height * self.line_spacing;

                let max_line_width = lines
                    .iter()
                    .map(|line| renderer.measure_text(line, &self.style))
                    .collect::<Result<Vec<_>>>()?
                    .iter()
                    .map(|m| m.width)
                    .fold(0.0, f64::max);

                Ok(TextMetrics {
                    width: max_line_width.min(max_width),
                    height: total_height,
                    ascent: line_metrics.ascent,
                    descent: line_metrics.descent,
                    x_advance: max_line_width,
                    y_advance: total_height,
                })
            } else {
                renderer.measure_text(&self.text, &self.style)
            }
        } else {
            renderer.measure_text(&self.text, &self.style)
        }
    }

    pub fn render(&self, renderer: &TextRenderer, x: i32, y: i32) -> Result<()> {
        if self.text.is_empty() {
            return Ok(());
        }

        let render_x = x as f64;
        let render_y = y as f64;

        if let Some(bounds) = self.bounds {
            if self.wrap {
                self.render_wrapped(renderer, bounds, render_x, render_y)
            } else {
                self.render_single_line(renderer, bounds, render_x, render_y)
            }
        } else {
            renderer.render_text(&self.text, render_x, render_y, &self.style)
        }
    }

    fn render_single_line(
        &self,
        renderer: &TextRenderer,
        bounds: Rect,
        x: f64,
        y: f64,
    ) -> Result<()> {
        let text = if self.ellipsis {
            self.apply_ellipsis(renderer, bounds.width as f64)?
        } else {
            self.text.clone()
        };

        let metrics = renderer.measure_text(&text, &self.style)?;
        let (aligned_x, aligned_y) = self.apply_alignment(x, y, &metrics, bounds);

        renderer.render_text(&text, aligned_x, aligned_y, &self.style)
    }

    fn render_wrapped(&self, renderer: &TextRenderer, bounds: Rect, x: f64, y: f64) -> Result<()> {
        let max_width = bounds.width as f64;
        let lines = self.get_wrapped_lines(renderer, max_width)?;

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

        let line_metrics = renderer.measure_text("M", &self.style)?;
        let line_height = (line_metrics.ascent + line_metrics.descent) * self.line_spacing;
        let total_height = lines.len() as f64 * line_height;

        let start_y = match self.vertical_alignment {
            VerticalAlignment::Top => y + line_metrics.ascent,
            VerticalAlignment::Middle => {
                y + (bounds.height as f64 - total_height) / 2.0 + line_metrics.ascent
            }
            VerticalAlignment::Bottom => {
                y + bounds.height as f64 - total_height + line_metrics.ascent
            }
            VerticalAlignment::Baseline => y,
        };

        for (i, line) in lines.iter().enumerate() {
            let line_metrics = renderer.measure_text(line, &self.style)?;
            let line_y = start_y + i as f64 * line_height;

            let line_x = match self.alignment {
                Alignment::Left => x,
                Alignment::Center => x + (bounds.width as f64 - line_metrics.width) / 2.0,
                Alignment::Right => x + bounds.width as f64 - line_metrics.width,
                Alignment::Justify => {
                    if i == lines.len() - 1 || line.split_whitespace().count() <= 1 {
                        x
                    } else {
                        self.render_justified_line(renderer, line, x, line_y, bounds.width as f64)?;
                        continue;
                    }
                }
            };

            renderer.render_text(line, line_x, line_y, &self.style)?;
        }

        Ok(())
    }

    fn render_justified_line(
        &self,
        renderer: &TextRenderer,
        line: &str,
        x: f64,
        y: f64,
        max_width: f64,
    ) -> Result<()> {
        let words: Vec<&str> = line.split_whitespace().collect();
        if words.len() <= 1 {
            return renderer.render_text(line, x, y, &self.style);
        }

        let word_widths: Result<Vec<f64>> = words
            .iter()
            .map(|word| renderer.measure_text(word, &self.style).map(|m| m.width))
            .collect();
        let word_widths = word_widths?;

        let total_word_width: f64 = word_widths.iter().sum();
        let total_space_width = max_width - total_word_width;
        let space_between_words = total_space_width / (words.len() - 1) as f64;

        let mut current_x = x;
        for (i, (word, &word_width)) in words.iter().zip(word_widths.iter()).enumerate() {
            renderer.render_text(word, current_x, y, &self.style)?;
            current_x += word_width;

            if i < words.len() - 1 {
                current_x += space_between_words;
            }
        }

        Ok(())
    }

    fn apply_alignment(&self, x: f64, y: f64, metrics: &TextMetrics, bounds: Rect) -> (f64, f64) {
        let aligned_x = match self.alignment {
            Alignment::Left => x,
            Alignment::Center => x + (bounds.width as f64 - metrics.width) / 2.0,
            Alignment::Right => x + bounds.width as f64 - metrics.width,
            Alignment::Justify => x,
        };

        let aligned_y = match self.vertical_alignment {
            VerticalAlignment::Top => y + metrics.ascent,
            VerticalAlignment::Middle => {
                y + (bounds.height as f64 - metrics.height) / 2.0 + metrics.ascent
            }
            VerticalAlignment::Bottom => y + bounds.height as f64 - metrics.descent,
            VerticalAlignment::Baseline => y,
        };

        (aligned_x, aligned_y)
    }

    fn get_wrapped_lines(&self, renderer: &TextRenderer, max_width: f64) -> Result<Vec<String>> {
        let words: Vec<&str> = self.text.split_whitespace().collect();

        if words.is_empty() {
            return Ok(Vec::new());
        }

        words
            .iter()
            .try_fold(
                (Vec::new(), String::new()),
                |(mut lines, mut current_line), &word| {
                    let test_line = if current_line.is_empty() {
                        word.to_string()
                    } else {
                        format!("{} {}", current_line, word)
                    };

                    let metrics = renderer.measure_text(&test_line, &self.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();

                        let word_metrics = renderer.measure_text(word, &self.style)?;
                        if word_metrics.width > max_width {
                            current_line =
                                self.break_long_word(renderer, word, max_width, &mut lines)?;
                        }
                    }

                    Ok::<_, anyhow::Error>((lines, current_line))
                },
            )
            .map(|(mut lines, current_line)| {
                if !current_line.is_empty() {
                    lines.push(current_line);
                }
                lines
            })
    }

    fn break_long_word(
        &self,
        renderer: &TextRenderer,
        word: &str,
        max_width: f64,
        lines: &mut Vec<String>,
    ) -> Result<String> {
        let mut current = String::new();

        for ch in word.chars() {
            let test = format!("{}{}", current, ch);
            let metrics = renderer.measure_text(&test, &self.style)?;

            if metrics.width > max_width && !current.is_empty() {
                lines.push(current);
                current = ch.to_string();
            } else {
                current.push(ch);
            }
        }

        Ok(current)
    }

    fn apply_ellipsis(&self, renderer: &TextRenderer, max_width: f64) -> Result<String> {
        let ellipsis = "...";
        let ellipsis_metrics = renderer.measure_text(ellipsis, &self.style)?;
        let available_width = max_width - ellipsis_metrics.width;

        if available_width <= 0.0 {
            return Ok(ellipsis.to_string());
        }

        let full_metrics = renderer.measure_text(&self.text, &self.style)?;
        if full_metrics.width <= max_width {
            return Ok(self.text.clone());
        }

        let mut truncated = String::new();
        for ch in self.text.chars() {
            let test = format!("{}{}", truncated, ch);
            let metrics = renderer.measure_text(&test, &self.style)?;

            if metrics.width > available_width {
                break;
            }
            truncated.push(ch);
        }

        Ok(format!("{}{}", truncated, ellipsis))
    }
}

impl Default for TextLayout {
    fn default() -> Self {
        Self::new()
    }
}