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>> {
self.surface.flush();
let _width = self.surface.width();
let _height = self.surface.height();
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")?;
let _old_context = std::mem::replace(&mut self.context, dummy_context);
let data_result = self
.surface
.data()
.context("Failed to get surface data")
.map(|data| data.to_vec());
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
}
}