Skip to main content

latex_rust/layout/
mod.rs

1//! TeX-faithful math box model. Dimensions are [`Dim`](crate::Dim).
2
3mod engine;
4mod metrics;
5mod numbering;
6mod space;
7mod style;
8
9pub use engine::{layout, layout_with_numbering};
10pub use metrics::MathParams;
11pub use numbering::{NumberFormat, NumberStyle, NumberingConfig, NumberingState};
12pub use style::MathStyle;
13
14use crate::color::Color;
15use crate::dim::Dim;
16use crate::error::Error;
17use crate::font::MathFont;
18
19/// Outline scale at which a renderer draws a glyph box.
20///
21/// The layout engine sizes every glyph box as the font's metrics for that
22/// glyph times the style scale: 1 at text size, `ScriptPercentScaleDown` at
23/// script size and `ScriptScriptPercentScaleDown` at scriptscript size. It does
24/// not record the scale on [`BoxContent::Glyph`], because adding a field to
25/// that public variant would break the 1.x API (latex-rust 2.0.0 adds one).
26/// So the renderers recover it from the box: the first of width, height and
27/// depth whose font metric is non-zero is compared exactly (the arithmetic is
28/// exact rational) with the metric times each style scale. A box that matches
29/// none of them, for example one built by hand with other dimensions, or a
30/// glyph the face does not have, is drawn at text size as before.
31pub(crate) fn glyph_scale(font: &MathFont, bx: &MathBox, ch: char, glyph_id: u16) -> Dim {
32    let Ok(m) = font.glyph_id(ch, glyph_id) else {
33        return Dim::one();
34    };
35    let pairs = [
36        (&bx.width, &m.advance),
37        (&bx.height, &m.height),
38        (&bx.depth, &m.depth),
39    ];
40    let Some((actual, metric)) = pairs.into_iter().find(|(_, metric)| !metric.is_zero()) else {
41        return Dim::one();
42    };
43    let same = |a: &Dim, b: &Dim| a.cmp(b) == Some(core::cmp::Ordering::Equal);
44    if same(actual, metric) {
45        return Dim::one();
46    }
47    let Ok(params) = MathParams::from_font(font) else {
48        return Dim::one();
49    };
50    for style in [MathStyle::Script, MathStyle::ScriptScript] {
51        let s = params.scale(style);
52        if same(actual, &(metric * &s)) {
53            return s;
54        }
55    }
56    Dim::one()
57}
58
59/// What a box contains.
60///
61/// Glyphs carry an OpenType id from the math face. Lists compose children.
62/// Color wrappers do not change dimensions. [`BoxContent::Line`] and
63/// [`BoxContent::Frame`] are decorations from cancel / boxed constructs.
64#[derive(Clone, Debug, PartialEq, Eq)]
65pub enum BoxContent {
66    /// Empty box (strut or placeholder).
67    Empty,
68    /// Solid rule (fraction bar, vinculum). No glyph.
69    Rule,
70    /// A single character whose metrics came from the math font.
71    Glyph {
72        /// Character.
73        ch: char,
74        /// OpenType glyph id.
75        glyph_id: u16,
76    },
77    /// Horizontal list. Width is the sum of children.
78    HList(Vec<MathBox>),
79    /// Vertical list. Height/depth stack on the baseline of the first box.
80    VList(Vec<MathBox>),
81    /// Horizontal kern (atom spacing, `\hspace`).
82    Kern(Dim),
83    /// Color wrapper. Dimensions match the inner box. Sets SVG `fill`.
84    Color(Color, Box<MathBox>),
85    /// Background color (`\colorbox`). Inner glyphs keep the default fill.
86    BackColor(Color, Box<MathBox>),
87    /// Children share the left edge; each child's [`MathBox::shift`] is its baseline.
88    Overlap(Vec<MathBox>),
89    /// Diagonal or free line in em, relative to the box left and baseline (`y` up).
90    Line {
91        /// Start x (em from left).
92        x1: Dim,
93        /// Start y (em above baseline).
94        y1: Dim,
95        /// End x.
96        x2: Dim,
97        /// End y.
98        y2: Dim,
99        /// Stroke thickness (em).
100        thickness: Dim,
101    },
102    /// Stroked rectangle around the inner box. Inner is laid out at the same origin.
103    Frame {
104        /// Rule thickness (em).
105        thickness: Dim,
106        /// Border color. `None` inherits the current fill (`\boxed`).
107        stroke: Option<Color>,
108        /// Contents inside the frame.
109        inner: Box<MathBox>,
110    },
111}
112
113/// TeX-style box: width, height above baseline, depth below, italic correction.
114///
115/// # Examples
116///
117/// ```
118/// use latex_rust::{Dim, MathBox};
119///
120/// let packed = MathBox::hpack(vec![
121///     MathBox::rule(Dim::one(), Dim::zero(), Dim::zero()),
122///     MathBox::rule(Dim::ratio(1, 2), Dim::zero(), Dim::zero()),
123/// ]);
124/// assert_eq!(packed.width, Dim::ratio(3, 2));
125/// ```
126#[derive(Clone, Debug, PartialEq, Eq)]
127pub struct MathBox {
128    /// Width.
129    pub width: Dim,
130    /// Height above the baseline.
131    pub height: Dim,
132    /// Depth below the baseline.
133    pub depth: Dim,
134    /// Italic correction.
135    pub italic: Dim,
136    /// Baseline raise relative to the parent list (positive is up).
137    pub shift: Dim,
138    /// Payload.
139    pub content: BoxContent,
140}
141
142impl MathBox {
143    /// Zero-size empty box.
144    #[must_use]
145    pub fn empty() -> Self {
146        Self {
147            width: Dim::zero(),
148            height: Dim::zero(),
149            depth: Dim::zero(),
150            italic: Dim::zero(),
151            shift: Dim::zero(),
152            content: BoxContent::Empty,
153        }
154    }
155
156    /// Rule with explicit dimensions (fraction bar, strut).
157    #[must_use]
158    pub fn rule(width: Dim, height: Dim, depth: Dim) -> Self {
159        Self {
160            width,
161            height,
162            depth,
163            italic: Dim::zero(),
164            shift: Dim::zero(),
165            content: BoxContent::Rule,
166        }
167    }
168
169    /// Horizontal kern of `width` (zero height and depth).
170    #[must_use]
171    pub fn kern(width: Dim) -> Self {
172        Self {
173            width: width.clone(),
174            height: Dim::zero(),
175            depth: Dim::zero(),
176            italic: Dim::zero(),
177            shift: Dim::zero(),
178            content: BoxContent::Kern(width),
179        }
180    }
181
182    /// Box from a font glyph. Errors if the face has no glyph for `ch`.
183    pub fn from_glyph(font: &MathFont, ch: char) -> Result<Self, Error> {
184        let g = font.glyph(ch)?;
185        Ok(Self {
186            width: g.advance,
187            height: g.height,
188            depth: g.depth,
189            italic: font.italic_correction(g.glyph_id),
190            shift: Dim::zero(),
191            content: BoxContent::Glyph {
192                ch,
193                glyph_id: g.glyph_id,
194            },
195        })
196    }
197
198    /// Pack boxes in a row. Width sums; height and depth are maxima.
199    #[must_use]
200    pub fn hpack(children: Vec<Self>) -> Self {
201        let mut width = Dim::zero();
202        let mut height = Dim::zero();
203        let mut depth = Dim::zero();
204        for c in &children {
205            width = &width + &c.width;
206            height = height.max(&c.height);
207            depth = depth.max(&c.depth);
208        }
209        Self {
210            width,
211            height,
212            depth,
213            italic: Dim::zero(),
214            shift: Dim::zero(),
215            content: BoxContent::HList(children),
216        }
217    }
218
219    /// Pack boxes in a column, first child on the baseline.
220    ///
221    /// Subsequent children sit below the previous (height + depth stacked).
222    #[must_use]
223    pub fn vpack(children: Vec<Self>) -> Self {
224        if children.is_empty() {
225            return Self::empty();
226        }
227        let mut width = Dim::zero();
228        let height = children[0].height.clone();
229        let mut depth = children[0].depth.clone();
230        for c in children.iter().skip(1) {
231            width = width.max(&c.width);
232            depth = &depth + &c.height;
233            depth = &depth + &c.depth;
234        }
235        width = width.max(&children[0].width);
236        Self {
237            width,
238            height,
239            depth,
240            italic: Dim::zero(),
241            shift: Dim::zero(),
242            content: BoxContent::VList(children),
243        }
244    }
245
246    /// Raise this box's baseline by `shift` (positive is up).
247    #[must_use]
248    pub fn with_shift(mut self, shift: Dim) -> Self {
249        self.shift = shift;
250        self
251    }
252
253    /// Gold-stable width/height/depth decimal string.
254    #[must_use]
255    pub fn dim_gold(&self) -> String {
256        format!(
257            "w={} h={} d={}",
258            self.width.to_dec_string(),
259            self.height.to_dec_string(),
260            self.depth.to_dec_string()
261        )
262    }
263}