use std::sync::{Arc, Mutex, OnceLock};
use cosmic_text::{Attrs, Buffer, Family, FontSystem, Metrics, Shaping};
use log::info;
use svg::{self, node::Text as SvgText, node::element as svg_element};
use crate::{
color::Color,
draw::{Drawable, LayeredOutput, RenderLayer},
geometry::{Insets, Point, Size},
};
static DEFAULT_TEXT: OnceLock<TextDefinition> = OnceLock::new();
#[derive(Debug, Clone)]
pub struct TextDefinition {
font_family: String,
font_size: u16,
background_color: Option<Color>,
color: Option<Color>,
padding: Insets,
}
impl TextDefinition {
pub fn default_borrowed() -> &'static Self {
DEFAULT_TEXT.get_or_init(|| TextDefinition {
font_family: String::from("sans-serif"),
font_size: 12,
background_color: None,
color: None,
padding: Insets::uniform(4.0),
})
}
pub fn new() -> Self {
Self::default()
}
pub fn set_font_size(&mut self, size: u16) {
self.font_size = size;
}
pub fn set_font_family(&mut self, family: &str) {
self.font_family = family.to_string();
}
pub fn set_background_color(&mut self, color: Option<Color>) {
self.background_color = color;
}
pub fn set_color(&mut self, color: Option<Color>) {
self.color = color;
}
pub fn set_padding(&mut self, padding: Insets) {
self.padding = padding;
}
fn font_size(&self) -> u16 {
self.font_size
}
fn font_family(&self) -> &str {
&self.font_family
}
fn background_color(&self) -> Option<&Color> {
self.background_color.as_ref()
}
fn color(&self) -> Option<&Color> {
self.color.as_ref()
}
fn padding(&self) -> Insets {
self.padding
}
}
impl Default for TextDefinition {
fn default() -> Self {
Self {
font_size: 15,
background_color: None,
color: None,
padding: Insets::default(),
font_family: "Arial".to_string(),
}
}
}
#[derive(Debug, Clone)]
pub struct Text<'a> {
definition: &'a TextDefinition,
content: &'a str,
}
impl<'a> Text<'a> {
pub fn new(definition: &'a TextDefinition, content: &'a str) -> Self {
Self {
definition,
content,
}
}
pub fn content(&self) -> &str {
self.content
}
pub fn calculate_size(&self) -> Size {
let padding = self.definition.padding();
self.calculate_size_without_padding().add_padding(padding)
}
fn calculate_size_without_padding(&self) -> Size {
TEXT_MANAGER
.get_or_init(TextManager::new)
.calculate_text_size(self.content, self.definition)
}
}
impl<'a> Drawable for Text<'a> {
fn render_to_layers(&self, position: Point) -> LayeredOutput {
let mut output = LayeredOutput::new();
let text_size = self.calculate_size();
let padding = self.definition.padding();
let lines: Vec<&str> = self.content.lines().collect();
let text_size_without_padding = self.calculate_size_without_padding();
let line_height = if lines.is_empty() {
0.0
} else {
text_size_without_padding.height() / lines.len() as f32
};
let total_height = text_size_without_padding.height();
let y_offset = -(total_height + line_height) / 2.0;
let mut rendered_text = svg_element::Text::new("")
.set("x", position.x())
.set("y", position.y() + y_offset)
.set("text-anchor", "middle")
.set("dominant-baseline", "central")
.set("font-family", self.definition.font_family())
.set("font-size", self.definition.font_size());
if let Some(color) = self.definition.color() {
rendered_text = rendered_text
.set("fill", color.to_string())
.set("fill-opacity", color.alpha());
}
for line in lines.into_iter() {
let tspan = svg_element::TSpan::new("")
.set("x", position.x())
.set("dy", line_height)
.add(SvgText::new(line));
rendered_text = rendered_text.add(tspan);
}
if let Some(bg_color) = self.definition.background_color() {
let bg_bounds = position.to_bounds(text_size).add_padding(padding);
let bg_size = bg_bounds.to_size();
let bg_min_point = bg_bounds.min_point();
let bg = svg_element::Rectangle::new()
.set("x", bg_min_point.x())
.set("y", bg_min_point.y())
.set("width", bg_size.width())
.set("height", bg_size.height())
.set("fill", bg_color.to_string())
.set("fill-opacity", bg_color.alpha())
.set("rx", 3.0);
output.add_to_layer(RenderLayer::Background, Box::new(bg));
}
output.add_to_layer(RenderLayer::Text, Box::new(rendered_text));
output
}
fn size(&self) -> Size {
self.calculate_size() }
}
struct TextManager {
font_system: Arc<Mutex<FontSystem>>,
}
impl Default for TextManager {
fn default() -> Self {
Self::new()
}
}
impl TextManager {
fn new() -> Self {
info!("Initializing FontSystem");
Self {
font_system: Arc::new(Mutex::new(FontSystem::new())),
}
}
fn calculate_text_size(&self, text: &str, text_def: &TextDefinition) -> Size {
if text.is_empty() {
return Size::default();
}
let mut font_system = self.font_system.lock().expect("failed to lock FontSystem");
let font_size_px = text_def.font_size() as f32 * 1.33;
let line_height = font_size_px * 1.15;
let metrics = Metrics::new(font_size_px, line_height);
let mut buffer = Buffer::new(&mut font_system, metrics);
let mut buffer = buffer.borrow_with(&mut font_system);
let attrs = Attrs::new().family(Family::Name(text_def.font_family()));
buffer.set_size(None, None);
buffer.set_text(text, &attrs, Shaping::Advanced, None);
buffer.shape_until_scroll(true);
let mut max_width: f32 = 0.0;
let mut total_height: f32 = 0.0;
let layout_runs: Vec<_> = buffer.layout_runs().collect();
if !layout_runs.is_empty() {
for last in layout_runs.iter().map(|run| run.glyphs.last()) {
if let Some(last) = last {
let run_width = last.x + last.w;
max_width = max_width.max(run_width);
}
total_height += metrics.line_height;
}
} else {
max_width = text.len() as f32 * (font_size_px * 0.55);
total_height = metrics.line_height;
}
Size::new(max_width, total_height)
}
}
static TEXT_MANAGER: OnceLock<TextManager> = OnceLock::new();
#[cfg(test)]
mod tests {
use std::ptr;
use float_cmp::assert_approx_eq;
use super::*;
#[test]
fn test_text_definition_default_borrowed_returns_static() {
let borrowed1 = TextDefinition::default_borrowed();
let borrowed2 = TextDefinition::default_borrowed();
assert!(ptr::eq(borrowed1, borrowed2));
}
#[test]
fn test_text_definition_default_borrowed_values() {
let borrowed = TextDefinition::default_borrowed();
assert_eq!(borrowed.font_size(), 12);
assert_eq!(borrowed.font_family(), "sans-serif");
assert!(borrowed.background_color().is_none());
assert!(borrowed.color().is_none());
assert_approx_eq!(f32, borrowed.padding().top(), 4.0);
assert_approx_eq!(f32, borrowed.padding().right(), 4.0);
assert_approx_eq!(f32, borrowed.padding().bottom(), 4.0);
assert_approx_eq!(f32, borrowed.padding().left(), 4.0);
}
#[test]
fn test_text_definition_set_font_size() {
let mut def = TextDefinition::new();
assert_eq!(def.font_size(), 15);
def.set_font_size(24);
assert_eq!(def.font_size(), 24);
def.set_font_size(8);
assert_eq!(def.font_size(), 8);
}
#[test]
fn test_text_definition_set_font_family() {
let mut def = TextDefinition::new();
assert_eq!(def.font_family(), "Arial");
def.set_font_family("Helvetica");
assert_eq!(def.font_family(), "Helvetica");
def.set_font_family("Times New Roman");
assert_eq!(def.font_family(), "Times New Roman");
def.set_font_family("monospace");
assert_eq!(def.font_family(), "monospace");
}
#[test]
fn test_text_definition_set_background_color() {
let mut def = TextDefinition::new();
assert!(def.background_color().is_none());
let yellow = Color::new("yellow").unwrap();
def.set_background_color(Some(yellow));
assert!(def.background_color().is_some());
def.set_background_color(None);
assert!(def.background_color().is_none());
}
#[test]
fn test_text_definition_set_color() {
let mut def = TextDefinition::new();
assert!(def.color().is_none());
let blue = Color::new("blue").unwrap();
def.set_color(Some(blue));
assert!(def.color().is_some());
def.set_color(None);
assert!(def.color().is_none());
}
#[test]
fn test_text_definition_set_padding() {
let mut def = TextDefinition::new();
assert_approx_eq!(f32, def.padding().horizontal_sum(), 0.0);
assert_approx_eq!(f32, def.padding().vertical_sum(), 0.0);
def.set_padding(Insets::uniform(10.0));
assert_approx_eq!(f32, def.padding().top(), 10.0);
assert_approx_eq!(f32, def.padding().right(), 10.0);
assert_approx_eq!(f32, def.padding().bottom(), 10.0);
assert_approx_eq!(f32, def.padding().left(), 10.0);
def.set_padding(Insets::new(5.0, 10.0, 15.0, 20.0));
assert_approx_eq!(f32, def.padding().top(), 5.0);
assert_approx_eq!(f32, def.padding().right(), 10.0);
assert_approx_eq!(f32, def.padding().bottom(), 15.0);
assert_approx_eq!(f32, def.padding().left(), 20.0);
}
#[test]
fn test_text_calculate_size_empty() {
let def = TextDefinition::new();
let text = Text::new(&def, "");
let size = text.calculate_size();
assert_approx_eq!(f32, size.width(), 0.0);
assert_approx_eq!(f32, size.height(), 0.0);
}
#[test]
fn test_text_calculate_size_single_line() {
let def = TextDefinition::new();
let text = Text::new(&def, "Hello World");
let size = text.calculate_size();
assert!(size.width() > 0.0, "Width should be positive");
assert!(size.height() > 0.0, "Height should be positive");
}
#[test]
fn test_text_calculate_size_multiline() {
let def = TextDefinition::new();
let single = Text::new(&def, "Line 1");
let multi = Text::new(&def, "Line 1\nLine 2\nLine 3");
let single_size = single.calculate_size();
let multi_size = multi.calculate_size();
assert!(
multi_size.height() > single_size.height(),
"Multi-line text ({}) should be taller than single line ({})",
multi_size.height(),
single_size.height()
);
}
#[test]
fn test_text_calculate_size_includes_padding() {
let mut def_no_padding = TextDefinition::new();
def_no_padding.set_padding(Insets::uniform(0.0));
let text_no_padding = Text::new(&def_no_padding, "Test");
let size_no_padding = text_no_padding.calculate_size();
let mut def_with_padding = TextDefinition::new();
def_with_padding.set_padding(Insets::uniform(20.0));
let text_with_padding = Text::new(&def_with_padding, "Test");
let size_with_padding = text_with_padding.calculate_size();
let width_diff = size_with_padding.width() - size_no_padding.width();
let height_diff = size_with_padding.height() - size_no_padding.height();
assert_approx_eq!(f32, width_diff, 40.0);
assert_approx_eq!(f32, height_diff, 40.0);
}
#[test]
fn test_text_calculate_size_larger_font() {
let mut small_def = TextDefinition::new();
small_def.set_font_size(12);
small_def.set_padding(Insets::uniform(0.0));
let mut large_def = TextDefinition::new();
large_def.set_font_size(24);
large_def.set_padding(Insets::uniform(0.0));
let small_text = Text::new(&small_def, "Test");
let large_text = Text::new(&large_def, "Test");
let small_size = small_text.calculate_size();
let large_size = large_text.calculate_size();
assert!(
large_size.height() > small_size.height(),
"Larger font height ({}) should be greater than smaller font ({})",
large_size.height(),
small_size.height()
);
assert!(
large_size.width() > small_size.width(),
"Larger font width ({}) should be greater than smaller font ({})",
large_size.width(),
small_size.width()
);
}
#[test]
fn test_text_render_to_layers_has_content() {
let def = TextDefinition::new();
let text = Text::new(&def, "Hello");
let output = text.render_to_layers(Point::new(100.0, 100.0));
assert!(!output.is_empty());
}
#[test]
fn test_text_render_with_background_adds_layer() {
let mut def = TextDefinition::new();
def.set_background_color(Some(Color::new("yellow").unwrap()));
let text = Text::new(&def, "With Background");
let output = text.render_to_layers(Point::new(0.0, 0.0));
assert!(!output.is_empty());
let rendered = output.render();
assert!(
rendered.len() >= 2,
"Should have at least 2 layers (Background and Text), got {}",
rendered.len()
);
}
#[test]
fn test_text_content_accessor() {
let def = TextDefinition::new();
let text = Text::new(&def, "My Content");
assert_eq!(text.content(), "My Content");
let empty_text = Text::new(&def, "");
assert_eq!(empty_text.content(), "");
let multiline = Text::new(&def, "Line 1\nLine 2");
assert_eq!(multiline.content(), "Line 1\nLine 2");
}
}