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_max_depth, 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/// What a box contains.
20///
21/// Glyphs carry an OpenType id from the math face. Lists compose children.
22/// Color wrappers do not change dimensions. [`BoxContent::Line`] and
23/// [`BoxContent::Frame`] are decorations from cancel / boxed constructs.
24#[derive(Clone, Debug, PartialEq, Eq)]
25pub enum BoxContent {
26 /// Empty box (strut or placeholder).
27 Empty,
28 /// Solid rule (fraction bar, vinculum). No glyph.
29 Rule,
30 /// A single character whose metrics came from the math font.
31 ///
32 /// Build with [`BoxContent::glyph`]; the variant is non-exhaustive.
33 #[non_exhaustive]
34 Glyph {
35 /// Character.
36 ch: char,
37 /// OpenType glyph id.
38 glyph_id: u16,
39 /// Outline scale relative to the em: 1 at text size, smaller at script
40 /// and scriptscript size. Box dimensions are already scaled; renderers
41 /// draw the outline at this factor.
42 scale: Dim,
43 },
44 /// Horizontal list. Width is the sum of children.
45 HList(Vec<MathBox>),
46 /// Vertical list. Height/depth stack on the baseline of the first box.
47 VList(Vec<MathBox>),
48 /// Horizontal kern (atom spacing, `\hspace`).
49 Kern(Dim),
50 /// Color wrapper. Dimensions match the inner box. Sets SVG `fill`.
51 Color(Color, Box<MathBox>),
52 /// Background color (`\colorbox`). Inner glyphs keep the default fill.
53 BackColor(Color, Box<MathBox>),
54 /// Children share the left edge; each child's [`MathBox::shift`] is its baseline.
55 Overlap(Vec<MathBox>),
56 /// Diagonal or free line in em, relative to the box left and baseline (`y` up).
57 Line {
58 /// Start x (em from left).
59 x1: Dim,
60 /// Start y (em above baseline).
61 y1: Dim,
62 /// End x.
63 x2: Dim,
64 /// End y.
65 y2: Dim,
66 /// Stroke thickness (em).
67 thickness: Dim,
68 },
69 /// Stroked rectangle around the inner box. Inner is laid out at the same origin.
70 Frame {
71 /// Rule thickness (em).
72 thickness: Dim,
73 /// Border color. `None` inherits the current fill (`\boxed`).
74 stroke: Option<Color>,
75 /// Contents inside the frame.
76 inner: Box<MathBox>,
77 },
78}
79
80/// TeX-style box: width, height above baseline, depth below, italic correction.
81///
82/// # Examples
83///
84/// ```
85/// use latex_rust::{Dim, MathBox};
86///
87/// let packed = MathBox::hpack(vec![
88/// MathBox::rule(Dim::one(), Dim::zero(), Dim::zero()),
89/// MathBox::rule(Dim::ratio(1, 2), Dim::zero(), Dim::zero()),
90/// ]);
91/// assert_eq!(packed.width, Dim::ratio(3, 2));
92/// ```
93#[derive(Clone, Debug, PartialEq, Eq)]
94pub struct MathBox {
95 /// Width.
96 pub width: Dim,
97 /// Height above the baseline.
98 pub height: Dim,
99 /// Depth below the baseline.
100 pub depth: Dim,
101 /// Italic correction.
102 pub italic: Dim,
103 /// Baseline raise relative to the parent list (positive is up).
104 pub shift: Dim,
105 /// Payload.
106 pub content: BoxContent,
107}
108
109impl BoxContent {
110 /// Glyph content drawn at `scale` times the em (1 for text size).
111 #[must_use]
112 pub fn glyph(ch: char, glyph_id: u16, scale: Dim) -> Self {
113 Self::Glyph {
114 ch,
115 glyph_id,
116 scale,
117 }
118 }
119}
120
121impl MathBox {
122 /// Zero-size empty box.
123 #[must_use]
124 pub fn empty() -> Self {
125 Self {
126 width: Dim::zero(),
127 height: Dim::zero(),
128 depth: Dim::zero(),
129 italic: Dim::zero(),
130 shift: Dim::zero(),
131 content: BoxContent::Empty,
132 }
133 }
134
135 /// Rule with explicit dimensions (fraction bar, strut).
136 #[must_use]
137 pub fn rule(width: Dim, height: Dim, depth: Dim) -> Self {
138 Self {
139 width,
140 height,
141 depth,
142 italic: Dim::zero(),
143 shift: Dim::zero(),
144 content: BoxContent::Rule,
145 }
146 }
147
148 /// Horizontal kern of `width` (zero height and depth).
149 #[must_use]
150 pub fn kern(width: Dim) -> Self {
151 Self {
152 width: width.clone(),
153 height: Dim::zero(),
154 depth: Dim::zero(),
155 italic: Dim::zero(),
156 shift: Dim::zero(),
157 content: BoxContent::Kern(width),
158 }
159 }
160
161 /// Box from a font glyph. Errors if the face has no glyph for `ch`.
162 pub fn from_glyph(font: &MathFont, ch: char) -> Result<Self, Error> {
163 let g = font.glyph(ch)?;
164 Ok(Self {
165 width: g.advance,
166 height: g.height,
167 depth: g.depth,
168 italic: font.italic_correction(g.glyph_id),
169 shift: Dim::zero(),
170 content: BoxContent::glyph(ch, g.glyph_id, Dim::one()),
171 })
172 }
173
174 /// Pack boxes in a row. Width sums; height and depth are maxima.
175 #[must_use]
176 pub fn hpack(children: Vec<Self>) -> Self {
177 let mut width = Dim::zero();
178 let mut height = Dim::zero();
179 let mut depth = Dim::zero();
180 for c in &children {
181 width = &width + &c.width;
182 height = height.max(&c.height);
183 depth = depth.max(&c.depth);
184 }
185 Self {
186 width,
187 height,
188 depth,
189 italic: Dim::zero(),
190 shift: Dim::zero(),
191 content: BoxContent::HList(children),
192 }
193 }
194
195 /// Pack boxes in a column, first child on the baseline.
196 ///
197 /// Subsequent children sit below the previous (height + depth stacked).
198 #[must_use]
199 pub fn vpack(children: Vec<Self>) -> Self {
200 if children.is_empty() {
201 return Self::empty();
202 }
203 let mut width = Dim::zero();
204 let height = children[0].height.clone();
205 let mut depth = children[0].depth.clone();
206 for c in children.iter().skip(1) {
207 width = width.max(&c.width);
208 depth = &depth + &c.height;
209 depth = &depth + &c.depth;
210 }
211 width = width.max(&children[0].width);
212 Self {
213 width,
214 height,
215 depth,
216 italic: Dim::zero(),
217 shift: Dim::zero(),
218 content: BoxContent::VList(children),
219 }
220 }
221
222 /// Raise this box's baseline by `shift` (positive is up).
223 #[must_use]
224 pub fn with_shift(mut self, shift: Dim) -> Self {
225 self.shift = shift;
226 self
227 }
228
229 /// Gold-stable width/height/depth decimal string.
230 #[must_use]
231 pub fn dim_gold(&self) -> String {
232 format!(
233 "w={} h={} d={}",
234 self.width.to_dec_string(),
235 self.height.to_dec_string(),
236 self.depth.to_dec_string()
237 )
238 }
239}