latex-rust 1.0.0

Pure-Rust LaTeX math renderer. Parse to AST, TeX-faithful layout in zenith-float, emit SVG, PNG, or egui shapes. No JavaScript, no webview, no runtime.
//! `MathBox` → `egui::Shape`. Feature `egui` only.

use std::collections::HashMap;

use egui::epaint::{RectShape, Vertex};
use egui::{Color32, Mesh, Pos2, Rect, Rounding, Shape, Stroke, TextureId, Vec2};

use super::tessellate::{tessellate, GlyphTris};
use super::EguiOptions;
use crate::color::Color;
use crate::dim::Dim;
use crate::error::Error;
use crate::font::MathFont;
use crate::layout::{BoxContent, MathBox};

pub(super) fn shapes(
    tree: &MathBox,
    font: &MathFont,
    options: &EguiOptions,
    origin: Pos2,
    pixels_per_point: f32,
) -> Result<(Vec<Shape>, Rect), Error> {
    let ppp = Dim::from_ieee32_bits(pixels_per_point.to_bits());
    if ppp.is_nan() || ppp.is_zero() || is_neg(&ppp) {
        return Err(Error::InvalidOption {
            what: "pixels_per_point must be positive".into(),
        });
    }
    if options.font_size_pt.is_nan()
        || is_neg(&options.font_size_pt)
        || options.font_size_pt.is_zero()
    {
        return Err(Error::InvalidOption {
            what: "font_size_pt must be positive".into(),
        });
    }
    let em_px = &options.font_size_pt * &ppp;
    let fu_px = &em_px / &Dim::from_i64(i64::from(font.units_per_em()));
    let ox = Dim::from_ieee32_bits(origin.x.to_bits());
    let oy = Dim::from_ieee32_bits(origin.y.to_bits());
    let w = &tree.width * &em_px;
    let h = (&tree.height + &tree.depth) * &em_px;
    let rect = Rect::from_min_size(origin, Vec2::new(px(&w), px(&h)));
    let baseline = &oy + &(&tree.height * &em_px);
    let mut cache: HashMap<u16, GlyphTris> = HashMap::new();
    let mut out = Vec::new();
    emit(
        tree,
        font,
        &em_px,
        &fu_px,
        &ox,
        &baseline,
        options.color,
        &mut cache,
        &mut out,
    )?;
    Ok((out, rect))
}

fn is_neg(d: &Dim) -> bool {
    matches!(d.cmp(&Dim::zero()), Some(core::cmp::Ordering::Less))
}

fn px(d: &Dim) -> f32 {
    f32::from_bits(d.to_ieee32_bits())
}

fn color32(c: Color) -> Color32 {
    let [r, g, b, a] = c.to_rgba8();
    Color32::from_rgba_unmultiplied(r, g, b, a)
}

#[allow(clippy::too_many_arguments)]
fn emit(
    bx: &MathBox,
    font: &MathFont,
    em_px: &Dim,
    fu_px: &Dim,
    origin_x: &Dim,
    parent_baseline: &Dim,
    fill: Color,
    cache: &mut HashMap<u16, GlyphTris>,
    out: &mut Vec<Shape>,
) -> Result<(), Error> {
    let baseline = parent_baseline - &(bx.shift.clone() * em_px);
    match &bx.content {
        BoxContent::Empty | BoxContent::Kern(_) => Ok(()),
        BoxContent::Glyph { glyph_id, .. } => {
            let tris = glyph_tris(font, *glyph_id, cache)?;
            out.push(mesh_shape(tris, origin_x, &baseline, fu_px, fill));
            Ok(())
        }
        BoxContent::Rule => {
            push_fill_rect(bx, origin_x, &baseline, em_px, fill, out);
            Ok(())
        }
        BoxContent::HList(kids) => {
            let mut x = origin_x.clone();
            for k in kids {
                emit(k, font, em_px, fu_px, &x, &baseline, fill, cache, out)?;
                x = &x + &(k.width.clone() * em_px);
            }
            Ok(())
        }
        BoxContent::VList(kids) => {
            if kids.is_empty() {
                return Ok(());
            }
            emit(
                &kids[0], font, em_px, fu_px, origin_x, &baseline, fill, cache, out,
            )?;
            let mut y_below = kids[0].depth.clone() * em_px;
            for k in kids.iter().skip(1) {
                let child_base = &baseline + &y_below + &(k.height.clone() * em_px);
                emit(
                    k,
                    font,
                    em_px,
                    fu_px,
                    origin_x,
                    &child_base,
                    fill,
                    cache,
                    out,
                )?;
                y_below = &y_below + &((&k.height + &k.depth) * em_px);
            }
            Ok(())
        }
        BoxContent::Overlap(kids) => {
            for k in kids {
                emit(k, font, em_px, fu_px, origin_x, &baseline, fill, cache, out)?;
            }
            Ok(())
        }
        BoxContent::Color(c, inner) => emit(
            inner, font, em_px, fu_px, origin_x, &baseline, *c, cache, out,
        ),
        BoxContent::BackColor(c, inner) => {
            push_fill_rect(bx, origin_x, &baseline, em_px, *c, out);
            emit(
                inner, font, em_px, fu_px, origin_x, &baseline, fill, cache, out,
            )
        }
        BoxContent::Line {
            x1,
            y1,
            x2,
            y2,
            thickness,
        } => {
            let x1p = origin_x + &(x1.clone() * em_px);
            let y1p = &baseline - &(y1.clone() * em_px);
            let x2p = origin_x + &(x2.clone() * em_px);
            let y2p = &baseline - &(y2.clone() * em_px);
            let stroke = Stroke::new(px(&(thickness.clone() * em_px)), color32(fill));
            out.push(Shape::line_segment(
                [Pos2::new(px(&x1p), px(&y1p)), Pos2::new(px(&x2p), px(&y2p))],
                stroke,
            ));
            Ok(())
        }
        BoxContent::Frame {
            thickness,
            stroke,
            inner,
        } => {
            let w = &bx.width * em_px;
            let h_ink = (&bx.height + &bx.depth) * em_px;
            let t = thickness.clone() * em_px;
            let half = &t / &Dim::from_i64(2);
            let x = origin_x + &half;
            let y = &(&baseline - &(bx.height.clone() * em_px)) + &half;
            let rw = (&w - &t).max(&Dim::zero());
            let rh = (&h_ink - &t).max(&Dim::zero());
            if !rw.is_zero() && !rh.is_zero() {
                let r = Rect::from_min_size(Pos2::new(px(&x), px(&y)), Vec2::new(px(&rw), px(&rh)));
                let border = stroke.unwrap_or(fill);
                out.push(Shape::Rect(RectShape::stroke(
                    r,
                    Rounding::ZERO,
                    Stroke::new(px(&t), color32(border)),
                )));
            }
            emit(
                inner, font, em_px, fu_px, origin_x, &baseline, fill, cache, out,
            )
        }
    }
}

fn push_fill_rect(
    bx: &MathBox,
    origin_x: &Dim,
    baseline: &Dim,
    em_px: &Dim,
    fill: Color,
    out: &mut Vec<Shape>,
) {
    let w = &bx.width * em_px;
    let h_ink = (&bx.height + &bx.depth) * em_px;
    if w.is_zero() || h_ink.is_zero() {
        return;
    }
    let y = baseline - &(bx.height.clone() * em_px);
    let r = Rect::from_min_size(
        Pos2::new(px(origin_x), px(&y)),
        Vec2::new(px(&w), px(&h_ink)),
    );
    out.push(Shape::rect_filled(r, Rounding::ZERO, color32(fill)));
}

fn glyph_tris(
    font: &MathFont,
    glyph_id: u16,
    cache: &mut HashMap<u16, GlyphTris>,
) -> Result<GlyphTris, Error> {
    if let Some(t) = cache.get(&glyph_id) {
        return Ok(t.clone());
    }
    let t = tessellate(font, glyph_id)?;
    cache.insert(glyph_id, t.clone());
    Ok(t)
}

fn mesh_shape(tris: GlyphTris, origin_x: &Dim, baseline: &Dim, fu_px: &Dim, fill: Color) -> Shape {
    let col = color32(fill);
    let mut mesh = Mesh {
        indices: tris.indices,
        vertices: Vec::with_capacity(tris.vertices.len()),
        texture_id: TextureId::default(),
    };
    for (fx, fy) in tris.vertices {
        let x = origin_x + &(&fx * fu_px);
        let y = baseline - &(&fy * fu_px);
        mesh.vertices.push(Vertex {
            pos: Pos2::new(px(&x), px(&y)),
            uv: Pos2::ZERO,
            color: col,
        });
    }
    Shape::mesh(mesh)
}