Skip to main content

latex_rust/render/egui/
mod.rs

1//! egui primitive backend. Milestone 10; optional feature `egui`.
2//!
3//! Without the feature every entry point is [`Error::Unsupported`].
4//! With `features = ["egui"]` a [`MathBox`] becomes `egui::Shape`
5//! meshes and rects — no SVG intermediate.
6
7#[cfg(feature = "egui")]
8mod emit;
9#[cfg(feature = "egui")]
10mod tessellate;
11
12use crate::color::Color;
13use crate::dim::Dim;
14use crate::error::Error;
15use crate::font::MathFont;
16use crate::layout::MathBox;
17
18/// Options for [`shapes`] / [`paint_egui`].
19///
20/// Default: 14 pt, black fill, text style.
21///
22/// # Examples
23///
24/// ```
25/// use latex_rust::{Color, Dim, EguiOptions};
26///
27/// let mut opt = EguiOptions::new();
28/// opt.font_size_pt = Dim::from_i64(14);
29/// opt.color = Color::rgb(0, 0, 0);
30/// opt.display = false;
31/// ```
32#[derive(Clone, Debug)]
33pub struct EguiOptions {
34    /// Em size in points (same meaning as [`crate::SvgOptions::font_size_pt`]).
35    pub font_size_pt: Dim,
36    /// Default glyph fill.
37    pub color: Color,
38    /// When using [`latex_to_shapes`], pick display vs text style.
39    pub display: bool,
40}
41
42impl Default for EguiOptions {
43    fn default() -> Self {
44        Self {
45            font_size_pt: Dim::from_i64(14),
46            color: Color::rgb(0, 0, 0),
47            display: false,
48        }
49    }
50}
51
52impl EguiOptions {
53    /// 14 pt, black fill, text style.
54    #[must_use]
55    pub fn new() -> Self {
56        Self::default()
57    }
58}
59
60/// Probe the egui backend: tessellate `tree` or return [`Error::Unsupported`].
61///
62/// Without `features = ["egui"]` this is [`Error::Unsupported`]. With the
63/// feature, use [`shapes`] to keep the emitted primitives.
64///
65/// # Arguments
66///
67/// * `tree` — box model from [`crate::layout()`].
68/// * `font` — face that supplied the glyph ids on `tree`.
69///
70/// # Returns
71///
72/// `Ok(())` when the feature is on and tessellation succeeds.
73///
74/// # Errors
75///
76/// * [`Error::Unsupported`] — `egui` feature off, or a box that cannot tessellate.
77/// * [`Error::Font`] — missing glyph outline.
78/// * [`Error::InvalidOption`] — non-positive font size (feature on).
79///
80/// # Examples
81///
82/// ```
83/// use latex_rust::{render_egui, MathBox, MathFont};
84///
85/// let font = MathFont::stix_two_math().unwrap();
86/// let r = render_egui(&MathBox::empty(), &font);
87/// #[cfg(not(feature = "egui"))]
88/// assert!(r.is_err());
89/// #[cfg(feature = "egui")]
90/// assert!(r.is_ok());
91/// ```
92pub fn render_egui(tree: &MathBox, font: &MathFont) -> Result<(), Error> {
93    #[cfg(not(feature = "egui"))]
94    {
95        let _ = (tree, font);
96        Err(Error::Unsupported {
97            what: "egui renderer".into(),
98        })
99    }
100    #[cfg(feature = "egui")]
101    {
102        let _ = shapes(tree, font, &EguiOptions::new(), egui::Pos2::ZERO, 1.0)?;
103        Ok(())
104    }
105}
106
107/// `MathBox` → egui shapes and the layout bounding rect.
108///
109/// `pixels_per_point` is egui's device pixel ratio. Zero or negative is
110/// [`Error::InvalidOption`]. Glyph tessellation is cached process-wide so a
111/// later render of the same glyphs is a cache hit.
112///
113/// # Arguments
114///
115/// * `tree` — box model from [`crate::layout()`].
116/// * `font` — face that supplied the glyph ids on `tree`.
117/// * `options` — em size and default fill.
118/// * `origin` — top-left of the layout rect, in egui points.
119/// * `pixels_per_point` — device pixel ratio (must be positive).
120///
121/// # Returns
122///
123/// Shape list and the bounding `Rect` of the expression.
124///
125/// # Errors
126///
127/// * [`Error::InvalidOption`] — non-positive `pixels_per_point` or font size.
128/// * [`Error::Font`] — missing glyph outline.
129/// * [`Error::Unsupported`] — tessellation produced no triangles.
130///
131/// # Examples
132///
133/// ```
134/// # #[cfg(feature = "egui")]
135/// # {
136/// use latex_rust::{layout, parse, shapes, EguiOptions, MathFont, MathStyle};
137///
138/// let ast = parse(r"x").unwrap();
139/// let font = MathFont::stix_two_math().unwrap();
140/// let tree = layout(&ast, &font, MathStyle::Text).unwrap();
141/// let (shapes, rect) = shapes(&tree, &font, &EguiOptions::new(), egui::Pos2::ZERO, 1.0).unwrap();
142/// assert!(!shapes.is_empty());
143/// assert!(rect.width() > 0.0);
144/// # }
145/// ```
146#[cfg(feature = "egui")]
147pub fn shapes(
148    tree: &MathBox,
149    font: &MathFont,
150    options: &EguiOptions,
151    origin: egui::Pos2,
152    pixels_per_point: f32,
153) -> Result<(Vec<egui::Shape>, egui::Rect), Error> {
154    emit::shapes(tree, font, options, origin, pixels_per_point)
155}
156
157/// Parse, lay out, and emit egui shapes.
158///
159/// # Arguments
160///
161/// * `latex` — math source (see [`crate::parse()`]).
162/// * `font` — face used for layout and outlines.
163/// * `options` — em size, fill, and display vs text style.
164/// * `origin` — top-left of the layout rect, in egui points.
165/// * `pixels_per_point` — device pixel ratio (must be positive).
166///
167/// # Returns
168///
169/// Shape list and the bounding `Rect` of the expression.
170///
171/// # Errors
172///
173/// Same as [`crate::parse()`] plus [`shapes`].
174///
175/// # Examples
176///
177/// ```
178/// # #[cfg(feature = "egui")]
179/// # {
180/// use latex_rust::{latex_to_shapes, EguiOptions, MathFont};
181///
182/// let font = MathFont::stix_two_math().unwrap();
183/// let (shapes, _) = latex_to_shapes(r"x", &font, &EguiOptions::new(), egui::Pos2::ZERO, 1.0).unwrap();
184/// assert!(!shapes.is_empty());
185/// # }
186/// ```
187#[cfg(feature = "egui")]
188pub fn latex_to_shapes(
189    latex: &str,
190    font: &MathFont,
191    options: &EguiOptions,
192    origin: egui::Pos2,
193    pixels_per_point: f32,
194) -> Result<(Vec<egui::Shape>, egui::Rect), Error> {
195    use crate::layout::{layout, MathStyle};
196    use crate::parser::parse;
197    let ast = parse(latex)?;
198    let style = if options.display {
199        MathStyle::Display
200    } else {
201        MathStyle::Text
202    };
203    let tree = layout(&ast, font, style)?;
204    shapes(&tree, font, options, origin, pixels_per_point)
205}
206
207/// Paint shapes through an egui [`egui::Painter`].
208///
209/// Uses `painter.ctx().pixels_per_point()` as the device pixel ratio.
210///
211/// # Arguments
212///
213/// * `tree` — box model from [`crate::layout()`].
214/// * `font` — face that supplied the glyph ids on `tree`.
215/// * `options` — em size and default fill.
216/// * `painter` — destination painter.
217/// * `origin` — top-left of the layout rect, in egui points.
218///
219/// # Returns
220///
221/// The bounding `Rect` of the painted expression.
222///
223/// # Errors
224///
225/// Same as [`shapes`].
226///
227/// # Examples
228///
229/// This entry point needs a live `egui::Painter` from an egui app. See [`shapes`]
230/// for a harness-free equivalent.
231#[cfg(feature = "egui")]
232pub fn paint_egui(
233    tree: &MathBox,
234    font: &MathFont,
235    options: &EguiOptions,
236    painter: &egui::Painter,
237    origin: egui::Pos2,
238) -> Result<egui::Rect, Error> {
239    let ppp = painter.ctx().pixels_per_point();
240    let (shapes, rect) = shapes(tree, font, options, origin, ppp)?;
241    painter.extend(shapes);
242    Ok(rect)
243}
244
245#[cfg(all(test, not(feature = "egui")))]
246mod tests {
247    use super::*;
248    use crate::font::MathFont;
249    use crate::layout::MathBox;
250
251    #[test]
252    fn egui_is_unsupported() {
253        let font = MathFont::stix_two_math().expect("STIX");
254        let err = render_egui(&MathBox::empty(), &font).expect_err("egui");
255        assert!(err.to_string().contains("egui"), "{err}");
256    }
257}