use crate::core::Color;
use crate::render::text_shaper::TextShaper;
#[derive(Debug, Clone)]
pub struct TextStyle {
pub font_family: String,
pub font_size: f32,
pub color: Color,
pub bold: bool,
pub italic: bool,
pub underline: bool,
pub strikethrough: bool,
}
impl Default for TextStyle {
fn default() -> Self {
Self {
font_family: String::from("Arial"),
font_size: 14.0,
color: Color::BLACK,
bold: false,
italic: false,
underline: false,
strikethrough: false,
}
}
}
#[derive(Debug, Clone)]
pub struct TextSpan {
pub text: String,
pub style: TextStyle,
}
#[derive(Debug, Clone, Default)]
pub struct RichText {
pub spans: Vec<TextSpan>,
}
impl RichText {
pub fn new() -> Self {
Self { spans: Vec::new() }
}
pub fn add_span(&mut self, text: &str, style: TextStyle) {
if text.is_empty() {
return;
}
if let Some(last) = self.spans.last_mut() {
if last.style.font_family == style.font_family
&& (last.style.font_size - style.font_size).abs() <= f32::EPSILON
&& last.style.color == style.color
&& last.style.bold == style.bold
&& last.style.italic == style.italic
&& last.style.underline == style.underline
&& last.style.strikethrough == style.strikethrough
{
last.text.push_str(text);
return;
}
}
self.spans.push(TextSpan { text: text.to_string(), style });
}
pub fn add_text(&mut self, text: &str) {
self.add_span(text, TextStyle::default());
}
pub fn is_empty(&self) -> bool {
self.spans.is_empty()
}
pub fn plain_text(&self) -> String {
let mut out = String::with_capacity(self.spans.iter().map(|s| s.text.len()).sum());
for span in &self.spans {
out.push_str(&span.text);
}
out
}
pub fn measure(&self, shaper: &dyn TextShaper) -> (f32, f32) {
let mut total_width = 0.0f32;
let mut max_height = 0.0f32;
for span in &self.spans {
total_width += shaper.measure_width(&span.text, span.style.font_size);
let span_height = shaper.measure_height(&span.text, span.style.font_size);
if span_height > max_height {
max_height = span_height;
}
}
(total_width, max_height)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::render::text_shaper::SimpleTextShaper;
#[test]
fn test_rich_text_empty() {
let rt = RichText::new();
assert!(rt.is_empty());
assert_eq!(rt.plain_text(), "");
}
#[test]
fn test_rich_text_add_text() {
let mut rt = RichText::new();
rt.add_text("Hello");
rt.add_text(" World");
assert_eq!(rt.spans.len(), 1); assert_eq!(rt.plain_text(), "Hello World");
}
#[test]
fn test_rich_text_add_span_with_different_style() {
let mut rt = RichText::new();
rt.add_text("Normal ");
let mut style = TextStyle::default();
style.bold = true;
style.font_size = 18.0;
rt.add_span("Bold", style);
assert_eq!(rt.spans.len(), 2);
assert_eq!(rt.plain_text(), "Normal Bold");
}
#[test]
fn test_rich_text_measure_returns_non_zero() {
let shaper = SimpleTextShaper::new();
let mut rt = RichText::new();
rt.add_text("Hello");
let (w, h) = rt.measure(&shaper);
assert!(w > 0.0);
assert!(h > 0.0);
}
#[test]
fn test_rich_text_add_empty_span_does_nothing() {
let mut rt = RichText::new();
rt.add_span("", TextStyle::default());
assert!(rt.is_empty());
}
#[test]
fn test_rich_text_measure_with_multiple_spans() {
let shaper = SimpleTextShaper::new();
let mut rt = RichText::new();
rt.add_text("A");
let mut big = TextStyle::default();
big.font_size = 28.0;
rt.add_span("B", big);
let (w, h) = rt.measure(&shaper);
assert!((h - 33.6).abs() < 0.01); assert!(w > 0.0);
}
#[test]
fn test_rich_text_default_style_is_sensible() {
let style = TextStyle::default();
assert_eq!(style.font_family, "Arial");
assert!((style.font_size - 14.0).abs() < f32::EPSILON);
assert_eq!(style.color, Color::BLACK);
assert!(!style.bold);
assert!(!style.italic);
assert!(!style.underline);
assert!(!style.strikethrough);
}
}