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