use crate::core::{Affine2D, Path, color::Rgba};
use crate::render::{GraphicsContext, PlainText, Renderer, TextAnchor};
use crate::text::{FontSource, TextRun, TextSpanKind};
use crate::mathtext::layout_math;
#[derive(Clone, Debug, PartialEq)]
struct PlainRun {
text: String,
x: f64,
width: f64,
}
#[derive(Clone, Debug, PartialEq)]
pub struct RichText {
pub paths: Vec<Path>,
pub width: f64,
pub ascent: f64,
pub descent: f64,
runs: Vec<Option<PlainRun>>,
font_size_px: f64,
}
impl RichText {
pub(crate) fn draw(
&self,
renderer: &mut dyn Renderer,
gc: &GraphicsContext,
place: &Affine2D,
bake: bool,
color: Rgba,
anchor: TextAnchor,
) {
for (path, run) in self.paths.iter().zip(&self.runs) {
let baked;
let (outline, transform) = if bake {
baked = path.transformed(place);
(&baked, Affine2D::identity())
} else {
(path, *place)
};
match run {
Some(run) => renderer.draw_text_run(
gc,
outline,
&transform,
color,
&PlainText {
text: &run.text,
font_size_px: self.font_size_px,
width: run.width,
anchor,
frame: Affine2D::from_translation(run.x, 0.0).then(place),
},
),
None => renderer.draw_path(gc, outline, &transform, Some(color)),
}
}
}
}
#[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 runs = 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());
runs.push(None);
}
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]));
runs.push(Some(PlainRun {
text: content.to_owned(),
x,
width: extent.width,
}));
x += extent.width;
ascent = ascent.max(extent.ascent);
descent = descent.max(extent.descent);
}
}
}
RichText {
paths,
width: x,
ascent,
descent,
runs,
font_size_px,
}
}
#[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());
}
}