Skip to main content

x11_overlay/graphics/
text.rs

1use super::font::{FontDesc, FontManager};
2use super::Color;
3use anyhow::{Context, Result};
4use cairo::{Context as CairoContext, Format, ImageSurface};
5
6#[derive(Debug, Clone)]
7pub struct TextStyle {
8    pub font: FontDesc,
9    pub color: Color,
10    pub background: Option<Color>,
11}
12
13impl TextStyle {
14    pub fn new(font: FontDesc, color: Color) -> Self {
15        Self {
16            font,
17            color,
18            background: None,
19        }
20    }
21
22    pub fn with_background(mut self, background: Color) -> Self {
23        self.background = Some(background);
24        self
25    }
26}
27
28impl Default for TextStyle {
29    fn default() -> Self {
30        Self {
31            font: FontDesc::new("sans-serif", 12.0),
32            color: Color::WHITE,
33            background: None,
34        }
35    }
36}
37
38#[derive(Debug)]
39pub struct TextMetrics {
40    pub width: f64,
41    pub height: f64,
42    pub ascent: f64,
43    pub descent: f64,
44    pub x_advance: f64,
45    pub y_advance: f64,
46}
47
48pub struct TextRenderer {
49    font_manager: FontManager,
50    surface: ImageSurface,
51    context: CairoContext,
52}
53
54impl TextRenderer {
55    pub fn new(width: i32, height: i32) -> Result<Self> {
56        let surface = ImageSurface::create(Format::ARgb32, width, height)
57            .context("Failed to create Cairo surface")?;
58
59        let context = CairoContext::new(&surface).context("Failed to create Cairo context")?;
60
61        let font_manager = FontManager::new().context("Failed to initialize font manager")?;
62
63        Ok(Self {
64            font_manager,
65            surface,
66            context,
67        })
68    }
69
70    pub fn resize(&mut self, width: i32, height: i32) -> Result<()> {
71        self.surface = ImageSurface::create(Format::ARgb32, width, height)
72            .context("Failed to recreate Cairo surface")?;
73
74        self.context =
75            CairoContext::new(&self.surface).context("Failed to recreate Cairo context")?;
76
77        Ok(())
78    }
79
80    pub fn clear(&self) {
81        self.context.save().unwrap();
82        self.context.set_operator(cairo::Operator::Clear);
83        self.context.paint().unwrap();
84        self.context.restore().unwrap();
85    }
86
87    pub fn clear_with_color(&self, color: Color) {
88        self.context.save().unwrap();
89        self.context
90            .set_source_rgba(color.r, color.g, color.b, color.a);
91        self.context.set_operator(cairo::Operator::Source);
92        self.context.paint().unwrap();
93        self.context.restore().unwrap();
94    }
95
96    fn apply_font(&self, font_desc: &FontDesc) -> Result<()> {
97        self.context.select_font_face(
98            &font_desc.family,
99            font_desc.slant.to_cairo_slant(),
100            font_desc.weight.to_cairo_weight(),
101        );
102        self.context.set_font_size(font_desc.size);
103        Ok(())
104    }
105
106    pub fn measure_text(&self, text: &str, style: &TextStyle) -> Result<TextMetrics> {
107        self.apply_font(&style.font)?;
108
109        let text_extents = self
110            .context
111            .text_extents(text)
112            .context("Failed to get text extents")?;
113
114        let font_extents = self
115            .context
116            .font_extents()
117            .context("Failed to get font extents")?;
118
119        Ok(TextMetrics {
120            width: text_extents.width(),
121            height: text_extents.height(),
122            ascent: font_extents.ascent(),
123            descent: font_extents.descent(),
124            x_advance: text_extents.x_advance(),
125            y_advance: text_extents.y_advance(),
126        })
127    }
128
129    pub fn render_text(&self, text: &str, x: f64, y: f64, style: &TextStyle) -> Result<()> {
130        self.apply_font(&style.font)?;
131
132        if let Some(bg_color) = style.background {
133            let metrics = self.measure_text(text, style)?;
134
135            self.context.save().unwrap();
136            self.context
137                .set_source_rgba(bg_color.r, bg_color.g, bg_color.b, bg_color.a);
138            self.context
139                .rectangle(x, y - metrics.ascent, metrics.width, metrics.height);
140            self.context.fill().unwrap();
141            self.context.restore().unwrap();
142        }
143
144        self.context.move_to(x, y);
145        self.context
146            .set_source_rgba(style.color.r, style.color.g, style.color.b, style.color.a);
147        self.context
148            .show_text(text)
149            .context("Failed to render text")?;
150
151        Ok(())
152    }
153
154    pub fn render_text_multiline(
155        &self,
156        text: &str,
157        x: f64,
158        y: f64,
159        max_width: f64,
160        style: &TextStyle,
161    ) -> Result<f64> {
162        let metrics = self.measure_text("M", style)?;
163        let line_height = metrics.ascent + metrics.descent;
164        let mut current_y = y;
165
166        let lines = self.wrap_text(text, max_width, style)?;
167
168        for line in lines {
169            self.render_text(&line, x, current_y, style)?;
170            current_y += line_height;
171        }
172
173        Ok(current_y - y)
174    }
175
176    fn wrap_text(&self, text: &str, max_width: f64, style: &TextStyle) -> Result<Vec<String>> {
177        let mut lines = Vec::new();
178        let words: Vec<&str> = text.split_whitespace().collect();
179
180        if words.is_empty() {
181            return Ok(lines);
182        }
183
184        let mut current_line = String::new();
185
186        for word in words {
187            let test_line = if current_line.is_empty() {
188                word.to_string()
189            } else {
190                format!("{} {}", current_line, word)
191            };
192
193            let metrics = self.measure_text(&test_line, style)?;
194
195            if metrics.width <= max_width {
196                current_line = test_line;
197            } else {
198                if !current_line.is_empty() {
199                    lines.push(current_line);
200                }
201                current_line = word.to_string();
202            }
203        }
204
205        if !current_line.is_empty() {
206            lines.push(current_line);
207        }
208
209        Ok(lines)
210    }
211
212    pub fn get_surface_data(&mut self) -> Result<Vec<u8>> {
213        // Ensure all drawing operations are complete
214        self.surface.flush();
215
216        // Drop the current context to release exclusive access to the surface
217        let _width = self.surface.width();
218        let _height = self.surface.height();
219
220        // Create a dummy surface to temporarily replace the context
221        // This releases the surface for exclusive access
222        let dummy_surface =
223            ImageSurface::create(Format::ARgb32, 1, 1).context("Failed to create dummy surface")?;
224        let dummy_context =
225            CairoContext::new(&dummy_surface).context("Failed to create dummy context")?;
226
227        // Replace the context temporarily
228        let _old_context = std::mem::replace(&mut self.context, dummy_context);
229
230        // Now we can safely access the surface data
231        let data_result = self
232            .surface
233            .data()
234            .context("Failed to get surface data")
235            .map(|data| data.to_vec());
236
237        // Recreate the context
238        self.context = CairoContext::new(&self.surface)
239            .context("Failed to recreate Cairo context after surface data access")?;
240
241        data_result
242    }
243
244    pub fn get_width(&self) -> i32 {
245        self.surface.width()
246    }
247
248    pub fn get_height(&self) -> i32 {
249        self.surface.height()
250    }
251
252    pub fn font_manager(&self) -> &FontManager {
253        &self.font_manager
254    }
255}