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}