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_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}