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}