Skip to main content

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}