use std::collections::HashMap;
use std::num::NonZeroUsize;
use std::sync::{Arc, LazyLock, Mutex};
use ab_glyph::{Font as _, FontRef, OutlineCurve};
use lru::LruCache;
use ratex_font::FontId;
use ratex_font_loader::outline_cache;
use ratex_layout::LayoutOptions;
use ratex_types::display_item::DisplayItem;
use ratex_types::MathStyle;
use crate::geometry::Vec2;
use crate::span::{ShapedSpan, Span, SpanContext};
use crate::text::TextSpan;
use crate::vector::{Paint, PathCommand};
use crate::Keyable;
#[derive(Clone, Keyable, bon::Builder)]
#[builder(derive(Into))]
pub struct MathSpan {
#[builder(into)]
pub source: String,
pub size: Option<f32>,
#[builder(into)]
pub fill: Option<Paint>,
}
impl MathSpan {
pub fn new(source: impl Into<String>) -> Self {
Self {
source: source.into(),
size: None,
fill: None,
}
}
}
impl Span for MathSpan {
fn shape(&self, ctx: &SpanContext<'_>) -> ShapedSpan {
let size = self.size.unwrap_or(ctx.size);
let fill = self.fill.clone().unwrap_or_else(|| ctx.fill.clone());
match math_geometry(&self.source, size) {
Some(geo) => ShapedSpan {
width: geo.width,
ascent: geo.ascent,
descent: geo.descent,
paths: geo
.paths
.iter()
.map(|commands| (commands.clone(), fill.clone()))
.collect(),
},
None => TextSpan::builder()
.text(self.source.clone())
.size(size)
.fill(fill)
.build()
.shape(ctx),
}
}
}
impl From<MathSpan> for Box<dyn Span> {
fn from(span: MathSpan) -> Self {
Box::new(span)
}
}
impl<S: math_span_builder::IsComplete> From<MathSpanBuilder<S>> for Box<dyn Span> {
fn from(builder: MathSpanBuilder<S>) -> Self {
Box::new(builder.build())
}
}
struct MathGeometry {
paths: Vec<Vec<PathCommand>>,
width: f32,
ascent: f32,
descent: f32,
}
#[derive(PartialEq, Eq, Hash)]
struct MathKey {
source: String,
size_bits: u32,
}
const MATH_CACHE_CAPACITY: usize = 256;
static MATH_CACHE: LazyLock<Mutex<LruCache<MathKey, Option<Arc<MathGeometry>>>>> =
LazyLock::new(|| {
Mutex::new(LruCache::new(
NonZeroUsize::new(MATH_CACHE_CAPACITY).expect("cache capacity is non-zero"),
))
});
fn math_geometry(source: &str, size: f32) -> Option<Arc<MathGeometry>> {
let key = MathKey {
source: source.to_owned(),
size_bits: size.to_bits(),
};
if let Ok(mut cache) = MATH_CACHE.lock() {
if let Some(hit) = cache.get(&key) {
return hit.clone();
}
}
let geo = compute_geometry(source, size).map(Arc::new);
if let Ok(mut cache) = MATH_CACHE.lock() {
cache.put(key, geo.clone());
}
geo
}
fn compute_geometry(source: &str, size: f32) -> Option<MathGeometry> {
let nodes = ratex_parser::parse(source).ok()?;
let options = LayoutOptions {
style: MathStyle::Text,
..Default::default()
};
let layout_box = ratex_layout::layout(&nodes, &options);
let display_list = ratex_layout::to_display_list(&layout_box);
let em = size;
let baseline = display_list.height as f32;
let fonts = ratex_font_loader::load_fonts_for_items("", &display_list.items).ok()?;
let mut font_refs: HashMap<FontId, FontRef<'_>> = HashMap::new();
for (id, bytes) in fonts.iter() {
if let Ok(font) = FontRef::try_from_slice(bytes) {
font_refs.insert(*id, font);
}
}
let mut paths: Vec<Vec<PathCommand>> = Vec::new();
for item in &display_list.items {
match item {
DisplayItem::GlyphPath {
x,
y,
scale,
font,
char_code,
..
} => {
let font_id = FontId::parse(font).unwrap_or(FontId::MainRegular);
let Some(font_ref) = font_refs.get(&font_id) else {
continue;
};
let ch = ratex_font::katex_ttf_glyph_char(font_id, *char_code);
let glyph_id = font_ref.glyph_id(ch);
if glyph_id.0 == 0 {
continue;
}
let Some(curves) =
outline_cache::get_or_compute_outline(font_id, font_ref, glyph_id)
else {
continue;
};
if curves.is_empty() {
continue;
}
let units_per_em = font_ref.units_per_em().unwrap_or(1000.0);
let glyph_scale = (*scale as f32 * em) / units_per_em;
let origin_x = *x as f32 * em;
let origin_y = (*y as f32 - baseline) * em;
let glyph = outline_to_path(&curves, origin_x, origin_y, glyph_scale);
if !glyph.is_empty() {
paths.push(glyph);
}
}
DisplayItem::Line {
x,
y,
width,
thickness,
..
} => {
let t = (*thickness as f32 * em).max(0.5);
let x0 = *x as f32 * em;
let y0 = (*y as f32 - baseline) * em - t / 2.0;
paths.push(rect_path(x0, y0, *width as f32 * em, t));
}
DisplayItem::Rect {
x,
y,
width,
height,
..
} => {
let x0 = *x as f32 * em;
let y0 = (*y as f32 - baseline) * em;
paths.push(rect_path(x0, y0, *width as f32 * em, *height as f32 * em));
}
DisplayItem::Path {
x,
y,
commands,
fill,
..
} => {
if !*fill {
continue;
}
let origin_x = *x as f32 * em;
let origin_y = (*y as f32 - baseline) * em;
paths.push(ratex_path_to_path(commands, origin_x, origin_y, em));
}
}
}
Some(MathGeometry {
paths,
width: display_list.width as f32 * em,
ascent: baseline * em,
descent: display_list.depth as f32 * em,
})
}
fn outline_to_path(
curves: &[OutlineCurve],
origin_x: f32,
origin_y: f32,
scale: f32,
) -> Vec<PathCommand> {
let map = |p: ab_glyph::Point| Vec2(origin_x + p.x * scale, origin_y - p.y * scale);
let mut commands = Vec::new();
let mut last_end: Option<Vec2> = None;
for curve in curves {
let (start, end) = match curve {
OutlineCurve::Line(p0, p1) => (map(*p0), map(*p1)),
OutlineCurve::Quad(p0, _, p2) => (map(*p0), map(*p2)),
OutlineCurve::Cubic(p0, _, _, p3) => (map(*p0), map(*p3)),
};
let need_move = match last_end {
None => true,
Some(le) => (le.0 - start.0).abs() > 0.01 || (le.1 - start.1).abs() > 0.01,
};
if need_move {
if last_end.is_some() {
commands.push(PathCommand::Close);
}
commands.push(PathCommand::MoveTo(start));
}
match curve {
OutlineCurve::Line(_, p1) => commands.push(PathCommand::LineTo(map(*p1))),
OutlineCurve::Quad(_, p1, p2) => commands.push(PathCommand::QuadTo {
control: map(*p1),
to: map(*p2),
}),
OutlineCurve::Cubic(_, p1, p2, p3) => commands.push(PathCommand::CubicTo {
c1: map(*p1),
c2: map(*p2),
to: map(*p3),
}),
}
last_end = Some(end);
}
if last_end.is_some() {
commands.push(PathCommand::Close);
}
commands
}
fn rect_path(x: f32, y: f32, width: f32, height: f32) -> Vec<PathCommand> {
vec![
PathCommand::MoveTo(Vec2(x, y)),
PathCommand::LineTo(Vec2(x + width, y)),
PathCommand::LineTo(Vec2(x + width, y + height)),
PathCommand::LineTo(Vec2(x, y + height)),
PathCommand::Close,
]
}
fn ratex_path_to_path(
commands: &[ratex_types::path_command::PathCommand],
origin_x: f32,
origin_y: f32,
em: f32,
) -> Vec<PathCommand> {
use ratex_types::path_command::PathCommand as R;
let point = |x: f64, y: f64| Vec2(origin_x + x as f32 * em, origin_y + y as f32 * em);
commands
.iter()
.map(|cmd| match cmd {
R::MoveTo { x, y } => PathCommand::MoveTo(point(*x, *y)),
R::LineTo { x, y } => PathCommand::LineTo(point(*x, *y)),
R::QuadTo { x1, y1, x, y } => PathCommand::QuadTo {
control: point(*x1, *y1),
to: point(*x, *y),
},
R::CubicTo {
x1,
y1,
x2,
y2,
x,
y,
} => PathCommand::CubicTo {
c1: point(*x1, *y1),
c2: point(*x2, *y2),
to: point(*x, *y),
},
R::Close => PathCommand::Close,
})
.collect()
}