use crate::core::Path;
use crate::text::{FontSource, TextRun, TextSpanKind};
use crate::mathtext::layout_math;
#[derive(Clone, Debug, PartialEq)]
pub struct RichText {
pub paths: Vec<Path>,
pub width: f64,
pub ascent: f64,
pub descent: f64,
}
#[must_use]
pub fn layout_rich_text(font: &FontSource, text: &str, font_size_px: f64) -> RichText {
let run = TextRun::parse(text);
let mut paths = Vec::new();
let mut x = 0.0;
let mut ascent: f64 = 0.0;
let mut descent: f64 = 0.0;
for span in run.spans() {
match span.kind() {
TextSpanKind::Math(_) => {
let layout = layout_math(span.content(), font, font_size_px);
let advance = layout.width;
ascent = ascent.max(layout.ascent);
descent = descent.max(layout.descent);
for element in layout.translated(x, 0.0).elements {
paths.push(element.path().clone());
}
x += advance;
}
TextSpanKind::Plain | TextSpanKind::RawTex(_) => {
let content = span.content();
let extent = font.measure(content, font_size_px);
paths.push(font.text_to_path(content, font_size_px, [x, 0.0]));
x += extent.width;
ascent = ascent.max(extent.ascent);
descent = descent.max(extent.descent);
}
}
}
RichText {
paths,
width: x,
ascent,
descent,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn font() -> FontSource {
FontSource::dejavu_sans()
}
#[test]
fn math_superscript_changes_layout() {
let scripted = layout_rich_text(&font(), "$x^2$", 12.0);
assert!(scripted.width > 0.0);
assert!(scripted.paths.len() > 1);
let plain = layout_rich_text(&font(), "x2", 12.0);
assert!((scripted.width - plain.width).abs() > f64::EPSILON);
assert_ne!(scripted.paths, plain.paths);
}
#[test]
fn plain_width_matches_measure() {
let text = "plain title";
let rich = layout_rich_text(&font(), text, 12.0);
let measured = font().measure(text, 12.0);
assert_eq!(rich.paths.len(), 1);
assert!((rich.width - measured.width).abs() < 1e-9);
assert!((rich.ascent - measured.ascent).abs() < 1e-9);
assert!((rich.descent - measured.descent).abs() < 1e-9);
}
#[test]
fn plain_path_matches_text_to_path() {
let text = "rizzma";
let rich = layout_rich_text(&font(), text, 12.0);
let direct = font().text_to_path(text, 12.0, [0.0, 0.0]);
assert_eq!(rich.paths, vec![direct]);
}
#[test]
fn empty_string_is_empty_layout() {
let rich = layout_rich_text(&font(), "", 12.0);
assert!(rich.paths.is_empty());
assert_eq!(rich.width, 0.0);
assert_eq!(rich.ascent, 0.0);
assert_eq!(rich.descent, 0.0);
}
#[test]
fn mixed_plain_and_math_advances_pen() {
let rich = layout_rich_text(&font(), "y = $x^2$", 12.0);
let plain_only = layout_rich_text(&font(), "y = ", 12.0);
assert!(rich.width > plain_only.width);
assert!(rich.paths.len() > plain_only.paths.len());
}
}