pub(crate) static FONT_BYTES: &[u8] = include_bytes!("../assets/LatinModernMath.otf");
mod boxer;
mod font;
mod ir;
mod parse;
mod spacing;
mod svg;
mod widget;
mod color;
pub use color::Color;
mod error;
pub use error::Error;
use iced::advanced::svg::Renderer as SvgRenderer;
use iced::advanced::text::Renderer as TextRenderer;
use iced::widget::{container, svg as svg_widget, text};
use iced::{Element, Length};
#[derive(Debug, Clone, Copy)]
pub struct MathRenderer {
font_size: f32,
display: bool,
color: Color,
}
impl MathRenderer {
pub fn new() -> Self {
MathRenderer {
font_size: 16.0,
display: false,
color: Color::BLACK,
}
}
pub fn font_size(mut self, px: f32) -> Self {
self.font_size = px;
self
}
pub fn display_style(mut self, yes: bool) -> Self {
self.display = yes;
self
}
pub fn color(mut self, c: Color) -> Self {
self.color = c;
self
}
pub fn to_svg(&self, src: &str) -> Result<Vec<u8>, Error> {
if !self.font_size.is_finite() || self.font_size <= 0.0 {
return Err(Error::InvalidFontSize(self.font_size));
}
let style = if self.display {
ir::Style::Display
} else {
ir::Style::Text
};
let node = parse::to_ir(src, self.font_size, style)
.map_err(|e| Error::Parse(e.0))?;
let b = boxer::layout(&node, style);
Ok(svg::emit(&b, self.color))
}
}
impl Default for MathRenderer {
fn default() -> Self {
MathRenderer::new()
}
}
pub fn inline<'a, Message, Theme, Renderer>(src: &str) -> Element<'a, Message, Theme, Renderer>
where
Message: 'a,
Theme: svg_widget::Catalog + text::Catalog + 'a,
<Theme as text::Catalog>::Class<'a>: From<text::StyleFn<'a, Theme>>,
Renderer: SvgRenderer + TextRenderer<Font = iced::Font> + 'a,
{
build(MathRenderer::new(), src)
}
pub fn block<'a, Message, Theme, Renderer>(src: &str) -> Element<'a, Message, Theme, Renderer>
where
Message: 'a,
Theme: svg_widget::Catalog + text::Catalog + container::Catalog + 'a,
<Theme as text::Catalog>::Class<'a>: From<text::StyleFn<'a, Theme>>,
Renderer: SvgRenderer + TextRenderer<Font = iced::Font> + 'a,
{
let el = build::<Message, Theme, Renderer>(MathRenderer::new().display_style(true), src);
container(el).center_x(Length::Fill).padding(8).into()
}
fn build<'a, Message, Theme, Renderer>(
renderer: MathRenderer,
src: &str,
) -> Element<'a, Message, Theme, Renderer>
where
Message: 'a,
Theme: svg_widget::Catalog + text::Catalog + 'a,
<Theme as text::Catalog>::Class<'a>: From<text::StyleFn<'a, Theme>>,
Renderer: SvgRenderer + TextRenderer<Font = iced::Font> + 'a,
{
match renderer.to_svg(src) {
Ok(bytes) => widget::from_svg(bytes),
Err(_) => widget::error_fallback(src.to_string()),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn to_svg_default_is_black_no_group() {
let bytes = MathRenderer::new().to_svg("x").unwrap();
let s = String::from_utf8(bytes).unwrap();
assert!(s.starts_with("<svg"));
assert!(!s.contains("<g fill"), "default black must not wrap in a group");
}
#[test]
fn to_svg_non_default_color_wraps_group() {
let bytes = MathRenderer::new()
.color(Color::rgb(0x11, 0x22, 0x33))
.to_svg("x")
.unwrap();
let s = String::from_utf8(bytes).unwrap();
assert!(s.contains(r##"<g fill="#112233">"##));
}
#[test]
fn to_svg_rejects_bad_font_size() {
for bad in [0.0_f32, -1.0, f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
let r = MathRenderer::new().font_size(bad);
assert!(matches!(r.to_svg("x"), Err(Error::InvalidFontSize(_))));
}
}
}