use crate::text::{measure_text, Font};
use crate::Color;
use std::fmt::Write;
#[derive(Debug, Clone)]
pub struct TextSpan {
pub text: String,
pub font: Font,
pub font_size: f64,
pub color: Color,
}
impl TextSpan {
pub fn new(text: &str, font: Font, font_size: f64, color: Color) -> Self {
Self {
text: text.to_string(),
font,
font_size,
color,
}
}
pub fn measure_width(&self) -> f64 {
measure_text(&self.text, &self.font, self.font_size)
}
}
#[derive(Debug)]
pub struct RichText {
spans: Vec<TextSpan>,
}
impl RichText {
pub fn new(spans: Vec<TextSpan>) -> Self {
Self { spans }
}
pub fn total_width(&self) -> f64 {
self.spans.iter().map(|s| s.measure_width()).sum()
}
pub fn max_font_size(&self) -> f64 {
self.spans
.iter()
.map(|s| s.font_size)
.fold(0.0_f64, f64::max)
}
pub fn spans(&self) -> &[TextSpan] {
&self.spans
}
pub(crate) fn render_operations(&self, x: f64, y: f64) -> String {
if self.spans.is_empty() {
return String::new();
}
let mut ops = String::new();
ops.push_str("BT\n");
writeln!(&mut ops, "{x:.2} {y:.2} Td").expect("write to String");
for span in &self.spans {
match span.color {
Color::Rgb(r, g, b) => {
writeln!(&mut ops, "{r:.3} {g:.3} {b:.3} rg").expect("write to String");
}
Color::Gray(gray) => {
writeln!(&mut ops, "{gray:.3} g").expect("write to String");
}
Color::Cmyk(c, m, y, k) => {
writeln!(&mut ops, "{c:.3} {m:.3} {y:.3} {k:.3} k").expect("write to String");
}
}
writeln!(
&mut ops,
"/{} {:.2} Tf",
span.font.pdf_name(),
span.font_size
)
.expect("write to String");
ops.push('(');
for ch in span.text.chars() {
match ch {
'(' => ops.push_str("\\("),
')' => ops.push_str("\\)"),
'\\' => ops.push_str("\\\\"),
'\n' => ops.push_str("\\n"),
'\r' => ops.push_str("\\r"),
'\t' => ops.push_str("\\t"),
_ => ops.push(ch),
}
}
ops.push_str(") Tj\n");
}
ops.push_str("ET\n");
ops
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty_rich_text() {
let rt = RichText::new(vec![]);
assert_eq!(rt.total_width(), 0.0);
assert_eq!(rt.max_font_size(), 0.0);
assert!(rt.render_operations(0.0, 0.0).is_empty());
}
#[test]
fn test_render_operations_contains_bt_et() {
let rt = RichText::new(vec![TextSpan::new(
"Hello",
Font::Helvetica,
12.0,
Color::black(),
)]);
let ops = rt.render_operations(50.0, 700.0);
assert!(ops.starts_with("BT\n"));
assert!(ops.ends_with("ET\n"));
assert!(ops.contains("(Hello) Tj"));
assert!(ops.contains("/Helvetica 12.00 Tf"));
}
}