oxedyne/fe2o3/fe2o3_austenite/src/math.rs
55.0 KiB, 262 runs
created by r1870400018:36109, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | //! Mathematics layout: TeX's mlist-to-hlist, simplified to the embedded text faces. |
| 2 | //! |
| 3 | //! A maths expression is an [`Atom`] tree -- symbols with a spacing [`Class`], rows, fractions and |
| 4 | //! scripts -- and [`layout`] sets it into the box-glue-penalty stream the rest of the engine draws. |
| 5 | //! The tree is measured into an internal box carrying, for each drawable, an x offset from the box's |
| 6 | //! left and a `rel` offset from the maths baseline; [`layout`] then flattens that into one [`Node::HBox`] |
| 7 | //! of leaves seated by glue and lifted by each leaf's own [`Leaf::with_shift`]. A fraction bar is a |
| 8 | //! [`LeafKind::Rule`](crate::ir::LeafKind); a numerator is raised and a script lowered by the shift, so |
| 9 | //! nothing bottoms out in a nested box -- which [`place_line`](crate::driver) would draw as a bare |
| 10 | //! rectangle. The one returned HBox is unwrapped by the caller: [`doc`](crate::doc) weaves its leaves |
| 11 | //! into a paragraph line for inline maths, or centres them on their own line for a display equation. |
| 12 | //! |
| 13 | //! The MATH-font boundary, stated plainly. New Computer Modern Math carries a real OpenType MATH table, and |
| 14 | //! this module now sets to it: the maths italic alphabet, the axis height, the fraction rule thickness, |
| 15 | //! the display-style fraction shifts and gaps, the script shifts and their minima, and the two script |
| 16 | //! scale-down percentages are all read from the font, and a delimiter or a radical grows to its content |
| 17 | //! through the font's vertical glyph variants. The plain-TeX guesses -- a quarter-em axis, half-em |
| 18 | //! shifts, seven-tenths and one-half script sizes -- survive only as the fallback for a text face with |
| 19 | //! no MATH table. The inter-atom spacing is still TeX's reduced `mu` table rather than the font's, and a |
| 20 | //! delimiter taller than the largest pre-drawn variant is not yet assembled from repeating pieces; big |
| 21 | //! operators, matrices and a maths parser are later work. See the crate's phase notes. |
| 22 | |
| 23 | use crate::theme::Theme; |
| 24 | use crate::font::ShapedText; |
| 25 | use crate::ir::{ |
| 26 | BoxNode, |
| 27 | Dims, |
| 28 | DrawOp, |
| 29 | Glue, |
| 30 | Graphic, |
| 31 | Leaf, |
| 32 | Node, |
| 33 | Sp, |
| 34 | }; |
| 35 | use crate::mathtable::MathTable; |
| 36 | |
| 37 | use oxedyne_fe2o3_core::prelude::*; |
| 38 | use oxedyne_fe2o3_font::{ |
| 39 | face::Role, |
| 40 | font::Font, |
| 41 | set::FontSet, |
| 42 | shape::Dir, |
| 43 | }; |
| 44 | use oxedyne_fe2o3_graphics::{ |
| 45 | colour::Rgba, |
| 46 | path::Path, |
| 47 | transform::Transform, |
| 48 | }; |
| 49 | |
| 50 | use std::sync::{ |
| 51 | Arc, |
| 52 | OnceLock, |
| 53 | }; |
| 54 | |
| 55 | // The maths font: New Computer Modern Math, embedded in `crate::fonts` -- Typst's own default for an |
| 56 | // equation, so a variable, an operator and a radical wear the letterforms the oracle sets. It carries the |
| 57 | // Mathematical Italic block, the upright operators and the large symbols, and an OpenType MATH table of |
| 58 | // layout constants and grown-delimiter variants, which this module reads for its shifts, gaps and variants |
| 59 | // (see the header). Until 2026-09-23 this was Latin Modern Math; some tuning notes below still name it. |
| 60 | const MATH_FONT: &[u8] = crate::fonts::MATH; |
| 61 | |
| 62 | /// The parsed maths font, built once and shared. Parsing the face is the costly part, so it is cached |
| 63 | /// behind a [`OnceLock`]; a lost race merely parses twice and keeps the first. |
| 64 | fn math_font() -> Outcome<Arc<Font>> { |
| 65 | static MATH: OnceLock<Arc<Font>> = OnceLock::new(); |
| 66 | if let Some(f) = MATH.get() { |
| 67 | return Ok(f.clone()); |
| 68 | } |
| 69 | let f = Arc::new(res!(Font::new(MATH_FONT.to_vec()))); |
| 70 | let _ = MATH.set(f.clone()); |
| 71 | Ok(f) |
| 72 | } |
| 73 | |
| 74 | /// The maths font's OpenType MATH table, parsed once and shared -- the layout constants and the grown |
| 75 | /// delimiter and radical variants. `None` if the font carries no such table, in which case the layout |
| 76 | /// falls back to the plain-TeX approximations. |
| 77 | fn math_table() -> Outcome<Option<Arc<MathTable>>> { |
| 78 | static TABLE: OnceLock<Option<Arc<MathTable>>> = OnceLock::new(); |
| 79 | if let Some(t) = TABLE.get() { |
| 80 | return Ok(t.clone()); |
| 81 | } |
| 82 | let parsed = res!(MathTable::parse(MATH_FONT)).map(Arc::new); |
| 83 | let _ = TABLE.set(parsed.clone()); |
| 84 | Ok(parsed) |
| 85 | } |
| 86 | |
| 87 | /// The glyph id a single character shapes to in the maths font, for looking the character up in the |
| 88 | /// MATH table's variants. |
| 89 | fn glyph_id(font: &Arc<Font>, size: Sp, ch: &str) -> Outcome<u32> { |
| 90 | let run = res!(font.shape(ch, size.to_pt() as f32, Dir::Ltr)); |
| 91 | match run.glyphs.first() { |
| 92 | Some(g) => Ok(g.id), |
| 93 | None => Err(err!("The maths font shaped {:?} to no glyph.", ch; Bug)), |
| 94 | } |
| 95 | } |
| 96 | |
| 97 | /// One glyph's outline flipped into the engine's y-down frame, with its ink bounding box. The outline |
| 98 | /// comes from the font y up with the baseline at zero; the flip leaves the baseline at zero and the ink |
| 99 | /// above the baseline at negative y. Returns the flipped path and its extent above and below and its |
| 100 | /// width, so a caller can seat a grown delimiter or a radical by its ink. |
| 101 | fn glyph_ink(font: &Arc<Font>, gid: u32, size: Sp) -> Outcome<(Path, Sp, Sp, Sp)> { |
| 102 | let raw = res!(font.outline(0, gid, size.to_pt() as f32)); |
| 103 | let path = res!(raw.transform(&Transform::scale(1.0, -1.0))); |
| 104 | match path.bounds(&Transform::IDENTITY) { |
| 105 | Some(b) => { |
| 106 | // y down: the top of the ink is the least y, the bottom the greatest. |
| 107 | let height = if b.y0 < 0.0 { Sp::from_pt((-b.y0) as f64) } else { Sp::ZERO }; |
| 108 | let depth = if b.y1 > 0.0 { Sp::from_pt(b.y1 as f64) } else { Sp::ZERO }; |
| 109 | let width = Sp::from_pt(b.x1.max(0.0) as f64); |
| 110 | Ok((path, height, depth, width)) |
| 111 | }, |
| 112 | None => Ok((path, Sp::ZERO, Sp::ZERO, Sp::ZERO)), // a blank glyph, nothing to seat |
| 113 | } |
| 114 | } |
| 115 | |
| 116 | /// An atom's spacing class, after TeX. The class of the atoms either side of a gap fixes the space |
| 117 | /// set there; the class also decides, for a symbol, whether it is set upright or in the maths italic. |
| 118 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] |
| 119 | pub enum Class { |
| 120 | Ord, // an ordinary atom: a variable or a number |
| 121 | Op, // a large operator or a function name (sin, log) |
| 122 | Bin, // a binary operator (+ -) |
| 123 | Rel, // a relation (= < >) |
| 124 | Open, // an opening delimiter |
| 125 | Close, // a closing delimiter |
| 126 | Punct, // punctuation |
| 127 | } |
| 128 | |
| 129 | /// An accent riding above (or a rule below) a base: the dot of a derivative, the bar of a mean, the |
| 130 | /// hat of an estimate, the vinculum of an overline. A glyph accent is a mark centred over the base; a |
| 131 | /// rule accent is a bar drawn the width of the base, over it (`overline`) or under it (`underline`). |
| 132 | #[derive(Clone, Debug)] |
| 133 | pub enum Accent { |
| 134 | Over(String), // a mark centred above the base (dot, hat, tilde, ...) |
| 135 | OverRule, // a bar the width of the base, above it (an overline) |
| 136 | UnderRule, // a bar the width of the base, below it (an underline) |
| 137 | } |
| 138 | |
| 139 | /// How a [`Atom::Matrix`] aligns and delimits its grid. A plain matrix centres each column and wraps |
| 140 | /// the grid in its delimiters; `cases` left-aligns and opens a single brace; an alignment block (a |
| 141 | /// multi-line display equation broken at `\` and aligned at `&`) alternates right- then left-aligned |
| 142 | /// columns with no gap at the alignment point, and carries no delimiters. |
| 143 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] |
| 144 | pub enum MatKind { |
| 145 | Matrix, |
| 146 | Cases, |
| 147 | Align, |
| 148 | } |
| 149 | |
| 150 | /// A node of the maths tree. `Sym` is the leaf: its text and its class. The recursive variants build |
| 151 | /// rows, fractions, scripts, accents and grids; grouping `{ ... }` is a [`Atom::Row`]. |
| 152 | #[derive(Clone, Debug)] |
| 153 | pub enum Atom { |
| 154 | Sym(String, Class), |
| 155 | Text(String), // an upright roman run: a quoted string, a `text()`/`upright()`, `d` of a differential |
| 156 | Space(i32), // a fixed horizontal space, in thousandths of an em (a `quad`, a `thin`, an `#h(..)`) |
| 157 | Row(Vec<Atom>), |
| 158 | Frac { |
| 159 | num: Box<Atom>, |
| 160 | den: Box<Atom>, |
| 161 | }, |
| 162 | Script { |
| 163 | base: Box<Atom>, |
| 164 | sup: Option<Box<Atom>>, |
| 165 | sub: Option<Box<Atom>>, |
| 166 | }, |
| 167 | Sqrt(Box<Atom>), // a square root: the radical sign and a vinculum over the radicand |
| 168 | Fence { |
| 169 | left: char, // the opening delimiter, grown to the body |
| 170 | body: Box<Atom>, |
| 171 | right: char, // the closing delimiter, grown to the body |
| 172 | }, |
| 173 | Accent { |
| 174 | base: Box<Atom>, |
| 175 | mark: Accent, |
| 176 | }, |
| 177 | Matrix { |
| 178 | rows: Vec<Vec<Atom>>, |
| 179 | left: Option<char>, // an opening delimiter grown to the grid, or none |
| 180 | right: Option<char>, // a closing delimiter grown to the grid, or none |
| 181 | kind: MatKind, |
| 182 | }, |
| 183 | Binom { |
| 184 | top: Box<Atom>, // the upper term, `binom(top, bottom)` |
| 185 | bottom: Box<Atom>, // the lower term |
| 186 | }, |
| 187 | Display(Box<Atom>), // force display style on the inner atom, Typst's `math.display(x)` |
| 188 | } |
| 189 | |
| 190 | impl Atom { |
| 191 | pub fn sym<S: Into<String>>(text: S, class: Class) -> Self { |
| 192 | Atom::Sym(text.into(), class) |
| 193 | } |
| 194 | |
| 195 | /// A variable: ordinary, and set italic when it is a single letter. |
| 196 | pub fn var<S: Into<String>>(text: S) -> Self { Atom::Sym(text.into(), Class::Ord) } |
| 197 | |
| 198 | /// A number: ordinary, but set upright because its text is not a single letter. |
| 199 | pub fn num<S: Into<String>>(text: S) -> Self { Atom::Sym(text.into(), Class::Ord) } |
| 200 | |
| 201 | pub fn op<S: Into<String>>(text: S) -> Self { Atom::Sym(text.into(), Class::Op) } |
| 202 | pub fn bin<S: Into<String>>(text: S) -> Self { Atom::Sym(text.into(), Class::Bin) } |
| 203 | pub fn rel<S: Into<String>>(text: S) -> Self { Atom::Sym(text.into(), Class::Rel) } |
| 204 | pub fn open<S: Into<String>>(text: S) -> Self { Atom::Sym(text.into(), Class::Open) } |
| 205 | pub fn close<S: Into<String>>(text: S) -> Self { Atom::Sym(text.into(), Class::Close) } |
| 206 | pub fn punct<S: Into<String>>(text: S) -> Self { Atom::Sym(text.into(), Class::Punct) } |
| 207 | |
| 208 | pub fn row(items: Vec<Atom>) -> Self { Atom::Row(items) } |
| 209 | |
| 210 | pub fn frac(num: Atom, den: Atom) -> Self { |
| 211 | Atom::Frac { num: Box::new(num), den: Box::new(den) } |
| 212 | } |
| 213 | |
| 214 | pub fn sup(base: Atom, sup: Atom) -> Self { |
| 215 | Atom::Script { base: Box::new(base), sup: Some(Box::new(sup)), sub: None } |
| 216 | } |
| 217 | |
| 218 | pub fn sub(base: Atom, sub: Atom) -> Self { |
| 219 | Atom::Script { base: Box::new(base), sup: None, sub: Some(Box::new(sub)) } |
| 220 | } |
| 221 | |
| 222 | pub fn subsup(base: Atom, sub: Atom, sup: Atom) -> Self { |
| 223 | Atom::Script { base: Box::new(base), sup: Some(Box::new(sup)), sub: Some(Box::new(sub)) } |
| 224 | } |
| 225 | |
| 226 | /// A square root over the radicand. |
| 227 | pub fn sqrt(radicand: Atom) -> Self { |
| 228 | Atom::Sqrt(Box::new(radicand)) |
| 229 | } |
| 230 | |
| 231 | /// A body between a pair of delimiters that grow to it, such as parentheses around a tall fraction. |
| 232 | pub fn fence(left: char, body: Atom, right: char) -> Self { |
| 233 | Atom::Fence { left, body: Box::new(body), right } |
| 234 | } |
| 235 | |
| 236 | /// An upright roman run: a quoted string or a `text()`/`upright()`, never set in the maths italic. |
| 237 | pub fn text<S: Into<String>>(text: S) -> Self { Atom::Text(text.into()) } |
| 238 | |
| 239 | /// A fixed horizontal space, its width in thousandths of an em (1000 = one em). |
| 240 | pub fn space(milli_em: i32) -> Self { Atom::Space(milli_em) } |
| 241 | |
| 242 | /// A base with an accent riding above it, or a rule over or under it. |
| 243 | pub fn accent(base: Atom, mark: Accent) -> Self { |
| 244 | Atom::Accent { base: Box::new(base), mark } |
| 245 | } |
| 246 | |
| 247 | /// A grid of atoms: a matrix, a `cases`, or a multi-line alignment block. |
| 248 | pub fn matrix(rows: Vec<Vec<Atom>>, left: Option<char>, right: Option<char>, kind: MatKind) -> Self { |
| 249 | Atom::Matrix { rows, left, right, kind } |
| 250 | } |
| 251 | |
| 252 | /// A binomial coefficient `binom(top, bottom)`: the two terms stacked without a rule, in parentheses. |
| 253 | pub fn binom(top: Atom, bottom: Atom) -> Self { |
| 254 | Atom::Binom { top: Box::new(top), bottom: Box::new(bottom) } |
| 255 | } |
| 256 | |
| 257 | /// Forces display style on the inner atom, Typst's `math.display(x)`. |
| 258 | pub fn display(inner: Atom) -> Self { |
| 259 | Atom::Display(Box::new(inner)) |
| 260 | } |
| 261 | } |
| 262 | |
| 263 | /// A size level, TeX's styles collapsed to the three that change the type size: running maths, a |
| 264 | /// script, and a script of a script. A fraction sets its parts one level down; a script sets its |
| 265 | /// scripts one level down; the smallest level does not shrink further. |
| 266 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] |
| 267 | enum Level { |
| 268 | Text, |
| 269 | Script, |
| 270 | ScriptScript, |
| 271 | } |
| 272 | |
| 273 | impl Level { |
| 274 | fn smaller(self) -> Level { |
| 275 | match self { |
| 276 | Level::Text => Level::Script, |
| 277 | Level::Script => Level::ScriptScript, |
| 278 | Level::ScriptScript => Level::ScriptScript, |
| 279 | } |
| 280 | } |
| 281 | } |
| 282 | |
| 283 | /// The type size at a level: the body size at text, and the two smaller sizes from the maths font's |
| 284 | /// own `scriptPercentScaleDown` and `scriptScriptPercentScaleDown`. The Computer Modern maths faces use |
| 285 | /// 70 and 50, the plain-TeX ratios, but a font may choose otherwise; the font's word is taken when it has a |
| 286 | /// MATH table. |
| 287 | fn size_for(style: &Theme, level: Level) -> Sp { |
| 288 | let body = style.text.body_size.raw(); |
| 289 | let (s_pct, ss_pct) = script_percents(); |
| 290 | match level { |
| 291 | Level::Text => style.text.body_size, |
| 292 | Level::Script => Sp(body * s_pct / 100), |
| 293 | Level::ScriptScript => Sp(body * ss_pct / 100), |
| 294 | } |
| 295 | } |
| 296 | |
| 297 | /// The maths font's two script scale-down percentages, or the plain-TeX 70 and 50 when the font carries |
| 298 | /// no MATH table. Read from the cached table, so this costs only a lookup. |
| 299 | fn script_percents() -> (i32, i32) { |
| 300 | match math_table() { |
| 301 | Ok(Some(t)) => { |
| 302 | let c = t.constants(); |
| 303 | (c.script_percent_scale_down as i32, c.script_script_percent_scale_down as i32) |
| 304 | }, |
| 305 | _ => (70, 50), |
| 306 | } |
| 307 | } |
| 308 | |
| 309 | /// A design-unit length scaled to a type size in scaled points, from the parsed table. |
| 310 | fn du(table: &Arc<MathTable>, value: i16, size_pt: f32) -> Sp { |
| 311 | Sp::from_pt(table.scaled(value, size_pt) as f64) |
| 312 | } |
| 313 | |
| 314 | /// What a drawable is: a shaped glyph run, or a horizontal bar of the given thickness (a fraction |
| 315 | /// rule). Both flatten to a leaf; the run to a [`LeafKind::Text`](crate::ir::LeafKind), the bar to a |
| 316 | /// [`LeafKind::Rule`](crate::ir::LeafKind). |
| 317 | enum Draw { |
| 318 | Glyph(ShapedText), |
| 319 | Bar(Sp), // thickness |
| 320 | Ink(Path), // a raw outline in the y-down frame, baseline at zero: a grown delimiter or radical |
| 321 | } |
| 322 | |
| 323 | /// One drawable within a maths box: its left `x` from the box's own left, its `width`, and `rel` -- |
| 324 | /// the downward offset of its reference line from the maths baseline. For a glyph `rel` is the |
| 325 | /// baseline offset (negative raises it); for a bar it is the offset of the bar's top edge. |
| 326 | struct Piece { |
| 327 | x: Sp, |
| 328 | width: Sp, |
| 329 | rel: Sp, |
| 330 | draw: Draw, |
| 331 | } |
| 332 | |
| 333 | /// A measured maths box: its drawables, its width, its extent above and below the maths baseline, and |
| 334 | /// the class it presents to whatever it sits beside. The internal form before [`emit`] flattens it to |
| 335 | /// leaves and glue. |
| 336 | struct MBox { |
| 337 | pieces: Vec<Piece>, |
| 338 | width: Sp, |
| 339 | height: Sp, // above the maths baseline |
| 340 | depth: Sp, // below the maths baseline |
| 341 | class: Class, |
| 342 | } |
| 343 | |
| 344 | /// Lays a maths expression into one [`Node::HBox`] of leaves and glue. `display` centres the box on |
| 345 | /// its own baseline (the caller sets it on a line of its own); otherwise the box's baseline is seated |
| 346 | /// at the surrounding text's ascent, so its variables sit on the paragraph's baseline. The returned |
| 347 | /// HBox is meant to be unwrapped by the caller -- its `list` woven into a line, its `dims` read for the |
| 348 | /// line's extent -- because a maths box left nested inside a paragraph line would draw as a bare |
| 349 | /// rectangle. |
| 350 | pub fn layout( |
| 351 | fonts: Arc<FontSet>, |
| 352 | style: &Theme, |
| 353 | expr: &Atom, |
| 354 | display: bool, |
| 355 | ) |
| 356 | -> Outcome<Node> |
| 357 | { |
| 358 | let m = res!(build(style, expr, Level::Text, display)); |
| 359 | |
| 360 | // The baseline's distance from the box top. A display box stands on its own, so its top is its |
| 361 | // highest ink and the baseline sits `height` below it. An inline box shares the line, so its |
| 362 | // baseline meets the surrounding text's baseline -- a body ascent below the line top. |
| 363 | let base = if display { |
| 364 | m.height |
| 365 | } else { |
| 366 | res!(ascent(&fonts, style.text.body_size)) |
| 367 | }; |
| 368 | |
| 369 | let (nodes, dims) = emit(m, base); |
| 370 | Ok(Node::HBox(BoxNode::new(nodes, dims))) |
| 371 | } |
| 372 | |
| 373 | /// The body face's ascent at a size: how far the line top sits above the baseline, the reference an |
| 374 | /// inline maths box seats its baseline against. |
| 375 | fn ascent(fonts: &Arc<FontSet>, size: Sp) -> Outcome<Sp> { |
| 376 | let sample = res!(ShapedText::new(fonts.clone(), Role::Body, Dir::Ltr, size, "0")); |
| 377 | Ok(sample.dims().height) |
| 378 | } |
| 379 | |
| 380 | /// Measures an atom into a maths box at a size level. Recursion mirrors the tree: a symbol shapes one |
| 381 | /// run, a row concatenates its atoms with class spacing, a fraction stacks two smaller boxes over a |
| 382 | /// bar or slashes them on the line, and a script hangs smaller boxes off a base. `display` distinguishes |
| 383 | /// a display equation, which stacks its fractions, from inline maths, which slashes them so a running |
| 384 | /// line never opens above a fraction. |
| 385 | fn build( |
| 386 | style: &Theme, |
| 387 | expr: &Atom, |
| 388 | level: Level, |
| 389 | display: bool, |
| 390 | ) |
| 391 | -> Outcome<MBox> |
| 392 | { |
| 393 | match expr { |
| 394 | Atom::Sym(text, class) => build_sym(style, text, *class, level, display), |
| 395 | Atom::Text(text) => build_text(style, text, level), |
| 396 | Atom::Space(mem) => Ok(build_space(style, *mem, level)), |
| 397 | Atom::Row(items) => build_row(style, items, level, display), |
| 398 | Atom::Frac { num, den } => build_frac(style, num, den, level, display, true), |
| 399 | Atom::Script { base, sup, sub } => build_script(style, base, sup.as_deref(), sub.as_deref(), level, display), |
| 400 | Atom::Sqrt(radicand) => build_sqrt(style, radicand, level, display), |
| 401 | Atom::Fence { left, body, right } => build_fence(style, *left, body, *right, level, display), |
| 402 | Atom::Accent { base, mark } => build_accent(style, base, mark, level, display), |
| 403 | Atom::Matrix { rows, left, right, kind } |
| 404 | => build_matrix(style, rows, *left, *right, *kind, level, display), |
| 405 | Atom::Binom { top, bottom } => build_binom(style, top, bottom, level, display), |
| 406 | // Display style forces the block context and the running size, so a fraction stacks and a big |
| 407 | // operator sets its limits below, as `math.display(x)` does in Typst. |
| 408 | Atom::Display(inner) => build(style, inner, Level::Text, true), |
| 409 | } |
| 410 | } |
| 411 | |
| 412 | /// Sets one symbol from the maths font. A single-letter ordinary atom is drawn from the Mathematical |
| 413 | /// Italic block -- a real maths italic, not the text italic -- while a function name, a digit, an |
| 414 | /// operator or a delimiter is the font's upright glyph. The run's ascent and descent stand in for the |
| 415 | /// atom's height and depth. |
| 416 | fn build_sym( |
| 417 | style: &Theme, |
| 418 | text: &str, |
| 419 | class: Class, |
| 420 | level: Level, |
| 421 | display: bool, |
| 422 | ) |
| 423 | -> Outcome<MBox> |
| 424 | { |
| 425 | let size = size_for(style, level); |
| 426 | // A big operator (a summation, an integral) is set from its larger display variant when it stands in |
| 427 | // a display equation at running size, the way TeX's `\displaystyle` grows it; inline and in a script |
| 428 | // it keeps its text-size glyph. |
| 429 | if display && level == Level::Text && is_bigop(text) { |
| 430 | if let Some(m) = res!(build_bigop(text, size)) { |
| 431 | return Ok(m); |
| 432 | } |
| 433 | } |
| 434 | let font = res!(math_font()); |
| 435 | let shown = math_text(text, class); |
| 436 | let shaped = res!(ShapedText::new_with_font(font, Dir::Ltr, size, &shown)); |
| 437 | let width = shaped.dims().width; // the advance, the horizontal extent |
| 438 | let (height, depth) = res!(shaped.ink_extent()); // the true ink extent, not the font's global metric |
| 439 | let piece = Piece { x: Sp::ZERO, width, rel: Sp::ZERO, draw: Draw::Glyph(shaped) }; |
| 440 | Ok(MBox { pieces: vec![piece], width, height, depth, class }) |
| 441 | } |
| 442 | |
| 443 | /// Is the symbol a large operator that grows in a display equation -- a summation, a product, an |
| 444 | /// integral, a big union or intersection? |
| 445 | fn is_bigop(text: &str) -> bool { |
| 446 | matches!(text, |
| 447 | "\u{2211}" | "\u{220F}" | "\u{2210}" | // sum, product, coproduct |
| 448 | "\u{222B}" | "\u{222C}" | "\u{222D}" | "\u{222E}" | // integral and its multiples |
| 449 | "\u{22C0}" | "\u{22C1}" | "\u{22C2}" | "\u{22C3}" | // big and, or, intersection, union |
| 450 | "\u{2A00}" | "\u{2A01}" | "\u{2A02}" | "\u{2A04}" | "\u{2A06}") // big odot, oplus, otimes, uplus, sqcup |
| 451 | } |
| 452 | |
| 453 | /// Does the symbol set its scripts as limits -- above and below in a display equation -- rather than |
| 454 | /// beside it? The big operators do, and the named limit operators (`lim`, `max`, `min`, ...), but an |
| 455 | /// integral does not: its bounds sit beside the sign even in display. |
| 456 | fn is_limits(text: &str) -> bool { |
| 457 | let integral = matches!(text, "\u{222B}" | "\u{222C}" | "\u{222D}" | "\u{222E}"); |
| 458 | if integral { |
| 459 | return false; |
| 460 | } |
| 461 | is_bigop(text) || matches!(text, |
| 462 | "lim" | "max" | "min" | "sup" | "inf" | "det" | "gcd" | "limsup" | "liminf" | "Pr") |
| 463 | } |
| 464 | |
| 465 | /// Draws a big operator from its tallest-but-one display variant, seated centred on the maths axis like |
| 466 | /// a delimiter, so a summation towers over its limits. `None` when the font offers no larger variant, in |
| 467 | /// which case the caller sets the plain glyph. |
| 468 | fn build_bigop(text: &str, size: Sp) -> Outcome<Option<MBox>> { |
| 469 | let table = res!(math_table()); |
| 470 | let tb = match &table { |
| 471 | Some(t) => t, |
| 472 | None => return Ok(None), |
| 473 | }; |
| 474 | let font = res!(math_font()); |
| 475 | let size_pt = size.to_pt() as f32; |
| 476 | let base = res!(glyph_id(&font, size, text)); |
| 477 | // A display big operator is about the next size up; ask for a variant around 1.4x the running size |
| 478 | // and take the tallest the font offers below that, which is the Computer Modern display form. |
| 479 | let target = Sp(size.raw() * 7 / 5); |
| 480 | let variant = match tb.variant_for(base as u16, target.to_pt() as f32, size_pt) { |
| 481 | Some(g) if g as u32 != base => g as u32, |
| 482 | _ => return Ok(None), // no larger form: leave the plain glyph |
| 483 | }; |
| 484 | let (path, gh, gd, gw) = res!(glyph_ink(&font, variant, size)); |
| 485 | let axis = du(tb, tb.constants().axis_height, size_pt); |
| 486 | // Centre the glyph's ink on the axis. |
| 487 | let rel = -axis - Sp((gd.raw() - gh.raw()) / 2); |
| 488 | let piece = Piece { x: Sp::ZERO, width: gw, rel, draw: Draw::Ink(path) }; |
| 489 | let height = gh - rel; // ink top above the baseline |
| 490 | let depth = rel + gd; // ink bottom below the baseline |
| 491 | Ok(Some(MBox { pieces: vec![piece], width: gw, height, depth, class: Class::Op })) |
| 492 | } |
| 493 | |
| 494 | /// Sets an upright roman run from the maths font: a quoted string, a `text()`/`upright()`, the `d` of a |
| 495 | /// differential. Unlike a single-letter ordinary, it is never remapped to the maths italic. It presents |
| 496 | /// as ordinary so it abuts what follows without an operator's spacing. |
| 497 | fn build_text( |
| 498 | style: &Theme, |
| 499 | text: &str, |
| 500 | level: Level, |
| 501 | ) |
| 502 | -> Outcome<MBox> |
| 503 | { |
| 504 | let size = size_for(style, level); |
| 505 | let font = res!(math_font()); |
| 506 | let shaped = res!(ShapedText::new_with_font(font, Dir::Ltr, size, text)); |
| 507 | let width = shaped.dims().width; |
| 508 | let (height, depth) = res!(shaped.ink_extent()); |
| 509 | let piece = Piece { x: Sp::ZERO, width, rel: Sp::ZERO, draw: Draw::Glyph(shaped) }; |
| 510 | Ok(MBox { pieces: vec![piece], width, height, depth, class: Class::Ord }) |
| 511 | } |
| 512 | |
| 513 | /// A fixed horizontal space: a zero-ink box the requested fraction of an em wide. It presents as |
| 514 | /// ordinary, so it neither adds nor sheds the automatic inter-atom spacing around it -- its width is the |
| 515 | /// whole of the gap. |
| 516 | fn build_space(style: &Theme, milli_em: i32, level: Level) -> MBox { |
| 517 | let size = size_for(style, level); |
| 518 | let width = Sp(size.raw() * milli_em / 1000); |
| 519 | MBox { pieces: Vec::new(), width, height: Sp::ZERO, depth: Sp::ZERO, class: Class::Ord } |
| 520 | } |
| 521 | |
| 522 | /// The characters actually shaped for an atom. A single-letter ordinary variable is remapped to its |
| 523 | /// Mathematical Italic codepoint, so `x` draws as a maths italic from the maths font; everything else |
| 524 | /// -- a digit, a multi-letter name, an operator -- is shaped as written, upright. |
| 525 | fn math_text(text: &str, class: Class) -> String { |
| 526 | if class == Class::Ord { |
| 527 | let mut it = text.chars(); |
| 528 | if let (Some(c), None) = (it.next(), it.next()) { |
| 529 | if let Some(m) = math_italic(c) { |
| 530 | return m.to_string(); |
| 531 | } |
| 532 | } |
| 533 | } |
| 534 | text.to_string() |
| 535 | } |
| 536 | |
| 537 | /// The Mathematical Italic codepoint of a Latin letter, or `None` for anything else. The italic small |
| 538 | /// `h` is the one hole in the block -- U+1D455 is unassigned -- and lives at U+210E, the Planck |
| 539 | /// constant, which every maths font draws as the italic h. |
| 540 | fn math_italic(c: char) -> Option<char> { |
| 541 | let cp = match c { |
| 542 | 'h' => 0x210E, |
| 543 | 'a'..='z' => 0x1D44E + (c as u32 - 'a' as u32), |
| 544 | 'A'..='Z' => 0x1D434 + (c as u32 - 'A' as u32), |
| 545 | _ => return None, |
| 546 | }; |
| 547 | char::from_u32(cp) |
| 548 | } |
| 549 | |
| 550 | /// Concatenates a row's atoms left to right, opening a class-driven space before each atom but the |
| 551 | /// first. The row presents as ordinary to whatever encloses it. |
| 552 | fn build_row( |
| 553 | style: &Theme, |
| 554 | items: &[Atom], |
| 555 | level: Level, |
| 556 | display: bool, |
| 557 | ) |
| 558 | -> Outcome<MBox> |
| 559 | { |
| 560 | let size = size_for(style, level); |
| 561 | let mut pieces: Vec<Piece> = Vec::new(); |
| 562 | let mut cursor = Sp::ZERO; |
| 563 | let mut height = Sp::ZERO; |
| 564 | let mut depth = Sp::ZERO; |
| 565 | let mut prev: Option<Class> = None; |
| 566 | |
| 567 | for atom in items { |
| 568 | let m = res!(build(style, atom, level, display)); |
| 569 | // TeX's binary-operator cancellation: a Bin with no atom to its left to bind -- at the row's |
| 570 | // start, or after another operator, a relation or an opening -- is not binary but a sign on the |
| 571 | // atom that follows, so it is retyped Ord and sheds its medium space. This is what turns the |
| 572 | // leading minus of `-b` from a subtraction with a gap into a tight unary sign. |
| 573 | let cls = match m.class { |
| 574 | Class::Bin => match prev { |
| 575 | None => Class::Ord, |
| 576 | Some(Class::Ord) | Some(Class::Close) => Class::Bin, |
| 577 | Some(_) => Class::Ord, |
| 578 | }, |
| 579 | other => other, |
| 580 | }; |
| 581 | if let Some(pc) = prev { |
| 582 | cursor += space_between(pc, cls, size); |
| 583 | } |
| 584 | place_into(&mut pieces, m.pieces, cursor, Sp::ZERO); |
| 585 | cursor += m.width; |
| 586 | if m.height > height { height = m.height; } |
| 587 | if m.depth > depth { depth = m.depth; } |
| 588 | prev = Some(cls); |
| 589 | } |
| 590 | Ok(MBox { pieces, width: cursor, height, depth, class: Class::Ord }) |
| 591 | } |
| 592 | |
| 593 | /// The inter-atom space set between two classes, in scaled points at the running size. TeX measures |
| 594 | /// these in `mu` (eighteen to the em): a thin space is three, a medium four, a thick five. The table |
| 595 | /// is simplified -- it does not reclassify a binary operator to ordinary at a row edge or beside |
| 596 | /// another operator, which real maths spacing does -- but it sets the visible cases: a relation gets |
| 597 | /// thick space, a binary operator medium, an operator or punctuation thin. |
| 598 | fn space_between(left: Class, right: Class, size: Sp) -> Sp { |
| 599 | let mu = mu_between(left, right); |
| 600 | Sp(size.raw() * mu / 18) |
| 601 | } |
| 602 | |
| 603 | /// The `mu` count between two atom classes, from TeX's spacing table, reduced to the pairs the text |
| 604 | /// faces set. |
| 605 | fn mu_between(left: Class, right: Class) -> i32 { |
| 606 | use Class::*; |
| 607 | match (left, right) { |
| 608 | (Rel, _) | (_, Rel) => 5, // thick around a relation |
| 609 | (Bin, _) | (_, Bin) => 4, // medium around a binary operator |
| 610 | (Ord, Op) | (Op, Ord) => 3, // thin between an operator and an ordinary |
| 611 | (Op, Op) => 3, |
| 612 | (Punct, _) => 3, // thin after punctuation |
| 613 | _ => 0, // delimiters and ordinaries abut |
| 614 | } |
| 615 | } |
| 616 | |
| 617 | /// Stacks a numerator over a denominator, centred over a bar seated on the maths axis. Following TeX's |
| 618 | /// style rule, a display fraction's immediate parts stay full size (Display's numerator style is Text) |
| 619 | /// while a script-level fraction's parts step down; the shifts, the gaps, the rule thickness and the |
| 620 | /// axis all come from the font's MATH constants -- the display-style variants of the fraction metrics -- |
| 621 | /// falling back to the plain-TeX guesses only when the font has no table. |
| 622 | fn build_frac( |
| 623 | style: &Theme, |
| 624 | num: &Atom, |
| 625 | den: &Atom, |
| 626 | level: Level, |
| 627 | display: bool, |
| 628 | bar: bool, |
| 629 | ) |
| 630 | -> Outcome<MBox> |
| 631 | { |
| 632 | // Inline, a real fraction is slashed on the line -- numerator, solidus, denominator abreast -- so it |
| 633 | // keeps within the line's height and never opens a gap above it. Only a display fraction stacks. A |
| 634 | // binomial (no bar) stacks even inline, as Typst does, so the slash path is taken only for a true bar. |
| 635 | if !display && bar { |
| 636 | let items = vec![ |
| 637 | num.clone(), |
| 638 | Atom::Sym("/".to_string(), Class::Ord), |
| 639 | den.clone(), |
| 640 | ]; |
| 641 | // A text/inline fraction's parts drop one size level (Text's numerator style is Script). |
| 642 | return build_row(style, &items, level.smaller(), display); |
| 643 | } |
| 644 | |
| 645 | let size = size_for(style, level); |
| 646 | let size_pt = size.to_pt() as f32; |
| 647 | let table = res!(math_table()); |
| 648 | |
| 649 | // TeX's style progression: at display style (level Text here) the parts stay full size; a fraction |
| 650 | // nested inside a script steps its parts down. The parts keep the block (display) context so a big |
| 651 | // operator in a numerator still sets its limits below and a nested fraction still stacks, both a size |
| 652 | // smaller; only the running/inline distinction was ever about slashing, and that was handled above. |
| 653 | let sub = if level == Level::Text { Level::Text } else { level.smaller() }; |
| 654 | let n = res!(build(style, num, sub, display)); |
| 655 | let d = res!(build(style, den, sub, display)); |
| 656 | |
| 657 | // The display-style fraction metrics from the MATH table, or the plain-TeX guesses without one. |
| 658 | let (axis, t, num_shift, den_shift, num_gap, den_gap) = match &table { |
| 659 | Some(tb) => { |
| 660 | let c = tb.constants(); |
| 661 | ( |
| 662 | du(tb, c.axis_height, size_pt), |
| 663 | du(tb, c.fraction_rule_thickness, size_pt), |
| 664 | du(tb, c.fraction_num_display_shift_up, size_pt), |
| 665 | du(tb, c.fraction_den_display_shift_down, size_pt), |
| 666 | du(tb, c.fraction_num_display_gap_min, size_pt), |
| 667 | du(tb, c.fraction_denom_display_gap_min, size_pt), |
| 668 | ) |
| 669 | }, |
| 670 | None => ( |
| 671 | Sp(size.raw() / 4), // axis |
| 672 | style.table.rule_thin, // bar thickness |
| 673 | Sp(size.raw() / 2), // numerator shift up |
| 674 | Sp(size.raw() / 2), // denominator shift down |
| 675 | Sp(size.raw() / 6), // numerator gap |
| 676 | Sp(size.raw() / 6), // denominator gap |
| 677 | ), |
| 678 | }; |
| 679 | |
| 680 | // The bar's top edge as a `rel` offset down from the maths baseline; above the baseline is negative. |
| 681 | // Its foot sits `t` below, at `-axis + t/2`, which the gap arithmetic below uses directly. |
| 682 | let bar_top = -axis - Sp(t.raw() / 2); |
| 683 | |
| 684 | // The numerator baseline: the font's display shift, but pushed higher if that leaves less than the |
| 685 | // least gap between the numerator's foot and the bar's top. |
| 686 | let num_lift = num_shift.raw().max(axis.raw() + t.raw() / 2 + n.depth.raw() + num_gap.raw()); |
| 687 | let num_base = Sp(-num_lift); |
| 688 | // The denominator baseline: the font's display shift, dropped further if the gap below the bar is |
| 689 | // tighter than the least. |
| 690 | let den_drop = den_shift.raw().max(d.height.raw() - axis.raw() + t.raw() / 2 + den_gap.raw()); |
| 691 | let den_base = Sp(den_drop); |
| 692 | |
| 693 | let pad = Sp(size.raw() / 8); // a small overhang of the bar past the wider part |
| 694 | let fw = n.width.raw().max(d.width.raw()); |
| 695 | let width = Sp(fw) + pad; |
| 696 | |
| 697 | let mut pieces: Vec<Piece> = Vec::new(); |
| 698 | let nx = Sp((width.raw() - n.width.raw()) / 2); |
| 699 | let dx = Sp((width.raw() - d.width.raw()) / 2); |
| 700 | place_into(&mut pieces, n.pieces, nx, num_base); |
| 701 | place_into(&mut pieces, d.pieces, dx, den_base); |
| 702 | // A binomial coefficient stacks its terms with the same gaps but no rule between them. |
| 703 | if bar { |
| 704 | pieces.push(Piece { x: Sp::ZERO, width, rel: bar_top, draw: Draw::Bar(t) }); |
| 705 | } |
| 706 | |
| 707 | let height = n.height - num_base; // num_base is negative, so this reaches above the baseline |
| 708 | let depth = den_base + d.depth; |
| 709 | Ok(MBox { pieces, width, height, depth, class: Class::Ord }) |
| 710 | } |
| 711 | |
| 712 | /// Hangs a superscript and/or a subscript off a base. The scripts are set one size level down and |
| 713 | /// lifted or dropped by the font's own MATH shifts: a superscript rides at least `superscriptShiftUp` |
| 714 | /// but higher off a tall base or to keep its foot above `superscriptBottomMin`; a subscript drops at |
| 715 | /// least `subscriptShiftDown`, deeper below a base that hangs below the baseline, and never letting its |
| 716 | /// top climb past `subscriptTopMax`. With both present, the pair is spread so the gap between the |
| 717 | /// superscript's foot and the subscript's top is no less than `subSuperscriptGapMin`. Without a MATH |
| 718 | /// table the plain-TeX fractions of the em stand in. |
| 719 | fn build_script( |
| 720 | style: &Theme, |
| 721 | base: &Atom, |
| 722 | sup: Option<&Atom>, |
| 723 | sub: Option<&Atom>, |
| 724 | level: Level, |
| 725 | display: bool, |
| 726 | ) |
| 727 | -> Outcome<MBox> |
| 728 | { |
| 729 | // A big operator or a named limit operator in a display equation sets its scripts as limits, centred |
| 730 | // above and below the sign rather than beside it. |
| 731 | if display && level == Level::Text { |
| 732 | if let Atom::Sym(text, _) = base { |
| 733 | if is_limits(text) { |
| 734 | return build_limits(style, base, sup, sub, level); |
| 735 | } |
| 736 | } |
| 737 | } |
| 738 | |
| 739 | let size = size_for(style, level); |
| 740 | let size_pt = size.to_pt() as f32; |
| 741 | let em = size.raw(); |
| 742 | let table = res!(math_table()); |
| 743 | // The base keeps the enclosing style; the scripts step down a size and are never display style. |
| 744 | let b = res!(build(style, base, level, display)); |
| 745 | |
| 746 | let mut pieces: Vec<Piece> = Vec::new(); |
| 747 | place_into(&mut pieces, b.pieces, Sp::ZERO, Sp::ZERO); |
| 748 | |
| 749 | let kern = Sp(em / 24); // a hair between the base and its scripts |
| 750 | let sx = b.width + kern; |
| 751 | let mut width = b.width; |
| 752 | let mut height = b.height; |
| 753 | let mut depth = b.depth; |
| 754 | |
| 755 | // The MATH script shifts scaled to the base size, or the plain-TeX guesses without a table. |
| 756 | let (sup_shift, sup_bottom_min, sup_drop_max, sub_shift, sub_top_max, sub_drop_min, gap_min) = |
| 757 | match &table { |
| 758 | Some(t) => { |
| 759 | let c = t.constants(); |
| 760 | ( |
| 761 | du(t, c.superscript_shift_up, size_pt), |
| 762 | du(t, c.superscript_bottom_min, size_pt), |
| 763 | du(t, c.superscript_baseline_drop_max, size_pt), |
| 764 | du(t, c.subscript_shift_down, size_pt), |
| 765 | du(t, c.subscript_top_max, size_pt), |
| 766 | du(t, c.subscript_baseline_drop_min, size_pt), |
| 767 | du(t, c.sub_superscript_gap_min, size_pt), |
| 768 | ) |
| 769 | }, |
| 770 | None => ( |
| 771 | Sp(em / 2), Sp(em / 12), Sp(em / 4), Sp(em / 4), Sp(em / 3), Sp(em / 20), Sp(em / 6), |
| 772 | ), |
| 773 | }; |
| 774 | |
| 775 | // The superscript's rise, and the subscript's drop, as positive distances from the maths baseline. |
| 776 | // Computed even when only one is present, so the combined-gap step can reconcile them. |
| 777 | let sup_m = match sup { |
| 778 | Some(s) => Some(res!(build(style, s, level.smaller(), false))), |
| 779 | None => None, |
| 780 | }; |
| 781 | let sub_m = match sub { |
| 782 | Some(s) => Some(res!(build(style, s, level.smaller(), false))), |
| 783 | None => None, |
| 784 | }; |
| 785 | |
| 786 | let mut rise = Sp::ZERO; |
| 787 | if let Some(m) = &sup_m { |
| 788 | // At least the font's shift; higher off a tall base; and enough that the foot clears the |
| 789 | // baseline by `superscriptBottomMin`. |
| 790 | let r = sup_shift.raw() |
| 791 | .max(b.height.raw() - sup_drop_max.raw()) |
| 792 | .max(sup_bottom_min.raw() + m.depth.raw()); |
| 793 | rise = Sp(r); |
| 794 | } |
| 795 | let mut drop = Sp::ZERO; |
| 796 | if let Some(m) = &sub_m { |
| 797 | // At least the font's shift; deeper below a base that descends; and enough that the top does |
| 798 | // not rise past `subscriptTopMax` above the baseline. |
| 799 | let d = sub_shift.raw() |
| 800 | .max(b.depth.raw() + sub_drop_min.raw()) |
| 801 | .max(m.height.raw() - sub_top_max.raw()); |
| 802 | drop = Sp(d); |
| 803 | } |
| 804 | |
| 805 | // With both scripts, widen the split until the vertical gap between the superscript's foot and the |
| 806 | // subscript's top reaches the font's minimum, deepening the subscript rather than raising the |
| 807 | // superscript, so the superscript keeps its natural height. |
| 808 | if let (Some(sm), Some(bm)) = (&sup_m, &sub_m) { |
| 809 | let gap = (drop.raw() - bm.height.raw()) + (rise.raw() - sm.depth.raw()); |
| 810 | if gap < gap_min.raw() { |
| 811 | drop = Sp(drop.raw() + (gap_min.raw() - gap)); |
| 812 | } |
| 813 | } |
| 814 | |
| 815 | if let Some(m) = sup_m { |
| 816 | place_into(&mut pieces, m.pieces, sx, Sp(-rise.raw())); |
| 817 | let w = sx + m.width; |
| 818 | if w > width { width = w; } |
| 819 | let top = rise + m.height; |
| 820 | if top > height { height = top; } |
| 821 | } |
| 822 | if let Some(m) = sub_m { |
| 823 | place_into(&mut pieces, m.pieces, sx, drop); |
| 824 | let w = sx + m.width; |
| 825 | if w > width { width = w; } |
| 826 | let bottom = drop + m.depth; |
| 827 | if bottom > depth { depth = bottom; } |
| 828 | } |
| 829 | |
| 830 | Ok(MBox { pieces, width, height, depth, class: b.class }) |
| 831 | } |
| 832 | |
| 833 | /// The maths axis at a size: the height a fraction bar, a relation and a grown delimiter centre on, |
| 834 | /// from the font's MATH table or the plain-TeX quarter-em without one. |
| 835 | fn axis_height(table: &Option<Arc<MathTable>>, size: Sp, size_pt: f32) -> Sp { |
| 836 | match table { |
| 837 | Some(t) => Sp::from_pt(t.scaled(t.constants().axis_height, size_pt) as f64), |
| 838 | None => Sp(size.raw() / 4), |
| 839 | } |
| 840 | } |
| 841 | |
| 842 | /// Sets a big operator with its scripts as limits, centred above and below the sign. The operator keeps |
| 843 | /// the running size (and grows to its display variant through [`build`]); each limit is set one size |
| 844 | /// level down and centred on the operator, a small gap clear of it. |
| 845 | fn build_limits( |
| 846 | style: &Theme, |
| 847 | base: &Atom, |
| 848 | sup: Option<&Atom>, |
| 849 | sub: Option<&Atom>, |
| 850 | level: Level, |
| 851 | ) |
| 852 | -> Outcome<MBox> |
| 853 | { |
| 854 | let size = size_for(style, level); |
| 855 | let gap = Sp(size.raw() / 6); // clearance between the operator and a limit |
| 856 | // The operator itself, at display size (a summation grows to its larger variant here). |
| 857 | let op = res!(build(style, base, level, true)); |
| 858 | let up = match sup { |
| 859 | Some(a) => Some(res!(build(style, a, level.smaller(), false))), |
| 860 | None => None, |
| 861 | }; |
| 862 | let lo = match sub { |
| 863 | Some(a) => Some(res!(build(style, a, level.smaller(), false))), |
| 864 | None => None, |
| 865 | }; |
| 866 | |
| 867 | let mut width = op.width; |
| 868 | if let Some(m) = &up { if m.width > width { width = m.width; } } |
| 869 | if let Some(m) = &lo { if m.width > width { width = m.width; } } |
| 870 | |
| 871 | let mut pieces: Vec<Piece> = Vec::new(); |
| 872 | let centre = |w: Sp| Sp((width.raw() - w.raw()) / 2); |
| 873 | place_into(&mut pieces, op.pieces, centre(op.width), Sp::ZERO); |
| 874 | let mut height = op.height; |
| 875 | let mut depth = op.depth; |
| 876 | |
| 877 | if let Some(m) = up { |
| 878 | // The upper limit's baseline, so its foot clears the operator's top by `gap`. |
| 879 | let drel = -(op.height + gap + m.depth); |
| 880 | let top = -drel + m.height; |
| 881 | place_into(&mut pieces, m.pieces, centre(m.width), drel); |
| 882 | if top > height { height = top; } |
| 883 | } |
| 884 | if let Some(m) = lo { |
| 885 | // The lower limit's baseline, so its top clears the operator's foot by `gap`. |
| 886 | let drel = op.depth + gap + m.height; |
| 887 | let bottom = drel + m.depth; |
| 888 | place_into(&mut pieces, m.pieces, centre(m.width), drel); |
| 889 | if bottom > depth { depth = bottom; } |
| 890 | } |
| 891 | |
| 892 | Ok(MBox { pieces, width, height, depth, class: Class::Op }) |
| 893 | } |
| 894 | |
| 895 | /// Sets an accent over a base, or a rule over or under it. A glyph accent is shaped from the maths font |
| 896 | /// and centred over the base, its foot a hair above the base's ink; an overline or underline is a bar |
| 897 | /// the width of the base, a small gap clear of it. The base keeps its size and style. |
| 898 | fn build_accent( |
| 899 | style: &Theme, |
| 900 | base: &Atom, |
| 901 | mark: &Accent, |
| 902 | level: Level, |
| 903 | display: bool, |
| 904 | ) |
| 905 | -> Outcome<MBox> |
| 906 | { |
| 907 | let size = size_for(style, level); |
| 908 | let size_pt = size.to_pt() as f32; |
| 909 | let table = res!(math_table()); |
| 910 | let b = res!(build(style, base, level, display)); |
| 911 | |
| 912 | let rule = match &table { |
| 913 | Some(t) => Sp::from_pt(t.scaled(t.constants().fraction_rule_thickness, size_pt) as f64), |
| 914 | None => style.table.rule_thin, |
| 915 | }; |
| 916 | let gap = Sp(size.raw() / 12); // clearance between the base and its accent or rule |
| 917 | |
| 918 | let mut pieces: Vec<Piece> = Vec::new(); |
| 919 | place_into(&mut pieces, b.pieces, Sp::ZERO, Sp::ZERO); |
| 920 | let mut height = b.height; |
| 921 | let mut depth = b.depth; |
| 922 | |
| 923 | match mark { |
| 924 | Accent::OverRule => { |
| 925 | let bar_top = -(b.height + gap + rule); |
| 926 | pieces.push(Piece { x: Sp::ZERO, width: b.width, rel: bar_top, draw: Draw::Bar(rule) }); |
| 927 | height = b.height + gap + rule; |
| 928 | }, |
| 929 | Accent::UnderRule => { |
| 930 | let bar_top = b.depth + gap; |
| 931 | pieces.push(Piece { x: Sp::ZERO, width: b.width, rel: bar_top, draw: Draw::Bar(rule) }); |
| 932 | depth = b.depth + gap + rule; |
| 933 | }, |
| 934 | Accent::Over(glyph) => { |
| 935 | // A spacing-accent glyph carries its own high position in the font, so it is placed by its |
| 936 | // actual outline rather than by shaping: the mark is dropped until its ink foot sits a small |
| 937 | // gap above the base's ink top, then centred over the base's width. |
| 938 | let font = res!(math_font()); |
| 939 | let gid = res!(glyph_id(&font, size, glyph)); |
| 940 | let (path, _, _, gw) = res!(glyph_ink(&font, gid, size)); |
| 941 | let (ink_top, ink_bottom) = match path.bounds(&Transform::IDENTITY) { |
| 942 | Some(bd) => (Sp::from_pt(bd.y0 as f64), Sp::from_pt(bd.y1 as f64)), |
| 943 | None => (Sp::ZERO, Sp::ZERO), |
| 944 | }; |
| 945 | // The ink foot wants to land a gap above the base top (negative is up). |
| 946 | let target = Sp(-(b.height.raw() + gap.raw())); |
| 947 | let dy = target - ink_bottom; |
| 948 | let moved = res!(path.transform(&Transform::translate(0.0, dy.to_pt() as f32))); |
| 949 | let x = Sp((b.width.raw() - gw.raw()) / 2); |
| 950 | pieces.push(Piece { x, width: gw, rel: Sp::ZERO, draw: Draw::Ink(moved) }); |
| 951 | let top = Sp(-(ink_top.raw() + dy.raw())); |
| 952 | if top > height { height = top; } |
| 953 | }, |
| 954 | } |
| 955 | |
| 956 | Ok(MBox { pieces, width: b.width, height, depth, class: Class::Ord }) |
| 957 | } |
| 958 | |
| 959 | /// Sets a grid of atoms: a matrix, a `cases`, or a multi-line alignment block. Every cell is measured, |
| 960 | /// the columns sized to their widest cell and the rows to their tallest; the grid is stacked with a row |
| 961 | /// gap and centred on the maths axis, each column aligned by the grid's kind. A matrix and a `cases` |
| 962 | /// then wrap the grid in delimiters grown to it. An alignment block carries none and butts its columns |
| 963 | /// at the alignment point, so `a &= b` reads as one line. |
| 964 | fn build_matrix( |
| 965 | style: &Theme, |
| 966 | rows: &[Vec<Atom>], |
| 967 | left: Option<char>, |
| 968 | right: Option<char>, |
| 969 | kind: MatKind, |
| 970 | level: Level, |
| 971 | _display: bool, // a grid sets its own cell style by kind, not the enclosing display flag |
| 972 | ) |
| 973 | -> Outcome<MBox> |
| 974 | { |
| 975 | let size = size_for(style, level); |
| 976 | let size_pt = size.to_pt() as f32; |
| 977 | let table = res!(math_table()); |
| 978 | let axis = axis_height(&table, size, size_pt); |
| 979 | |
| 980 | let nrows = rows.len(); |
| 981 | if nrows == 0 { |
| 982 | return Ok(MBox { pieces: Vec::new(), width: Sp::ZERO, height: Sp::ZERO, depth: Sp::ZERO, class: Class::Ord }); |
| 983 | } |
| 984 | let ncols = rows.iter().map(|r| r.len()).max().unwrap_or(0); |
| 985 | // A cell keeps the grid's level; an alignment line is display style (its fractions stack), a matrix |
| 986 | // or a `cases` cell is text style. |
| 987 | let cell_display = kind == MatKind::Align; |
| 988 | |
| 989 | // Measure every cell. |
| 990 | let mut cells: Vec<Vec<MBox>> = Vec::with_capacity(nrows); |
| 991 | for row in rows { |
| 992 | let mut cs = Vec::with_capacity(ncols); |
| 993 | for c in 0..ncols { |
| 994 | let m = match row.get(c) { |
| 995 | Some(a) => res!(build(style, a, level, cell_display)), |
| 996 | None => MBox { pieces: Vec::new(), width: Sp::ZERO, height: Sp::ZERO, depth: Sp::ZERO, class: Class::Ord }, |
| 997 | }; |
| 998 | cs.push(m); |
| 999 | } |
| 1000 | cells.push(cs); |
| 1001 | } |
| 1002 | |
| 1003 | // Column widths and per-row height and depth. |
| 1004 | let mut colw = vec![Sp::ZERO; ncols]; |
| 1005 | let mut rowh = vec![Sp::ZERO; nrows]; |
| 1006 | let mut rowd = vec![Sp::ZERO; nrows]; |
| 1007 | for (ri, row) in cells.iter().enumerate() { |
| 1008 | for (c, m) in row.iter().enumerate() { |
| 1009 | if m.width > colw[c] { colw[c] = m.width; } |
| 1010 | if m.height > rowh[ri] { rowh[ri] = m.height; } |
| 1011 | if m.depth > rowd[ri] { rowd[ri] = m.depth; } |
| 1012 | } |
| 1013 | } |
| 1014 | |
| 1015 | let (col_gap, row_gap) = match kind { |
| 1016 | MatKind::Matrix => (Sp(size.raw() * 11 / 20), Sp(size.raw() * 7 / 20)), // 0.55em, 0.35em |
| 1017 | MatKind::Cases => (Sp(size.raw() * 4 / 5), Sp(size.raw() * 2 / 5)), // 0.80em, 0.40em |
| 1018 | // An alignment line breaks at `&` before a relation (`&=`), so the gap at the alignment point is |
| 1019 | // a relation's thick space, matching the space the relation carries on its other side. |
| 1020 | MatKind::Align => (Sp(size.raw() * 5 / 18), Sp(size.raw() * 2 / 5)), // 5 mu, 0.40em |
| 1021 | }; |
| 1022 | |
| 1023 | // The grid's total width. |
| 1024 | let mut content_w = Sp::ZERO; |
| 1025 | for c in 0..ncols { |
| 1026 | content_w += colw[c]; |
| 1027 | if c + 1 < ncols { content_w += col_gap; } |
| 1028 | } |
| 1029 | |
| 1030 | // Each row's baseline, relative to the first row's baseline (positive down), then the shift that puts |
| 1031 | // the grid's vertical centre on the axis. |
| 1032 | let mut base_rel = vec![Sp::ZERO; nrows]; |
| 1033 | let mut y = Sp::ZERO; |
| 1034 | for i in 0..nrows { |
| 1035 | base_rel[i] = y; |
| 1036 | if i + 1 < nrows { |
| 1037 | y += rowd[i] + row_gap + rowh[i + 1]; |
| 1038 | } |
| 1039 | } |
| 1040 | let span_below = base_rel[nrows - 1] + rowd[nrows - 1]; // first baseline to grid foot |
| 1041 | let span_above = rowh[0]; // first baseline to grid top |
| 1042 | let centre = Sp((span_below.raw() - span_above.raw()) / 2); |
| 1043 | let shift = Sp(-axis.raw()) - centre; // grid centre onto the axis |
| 1044 | |
| 1045 | let content_h = span_above - shift; // grid top above the baseline |
| 1046 | let content_d = span_below + shift; // grid foot below the baseline |
| 1047 | |
| 1048 | // Place the cells, with the content offset for a left delimiter added later. |
| 1049 | let mut pieces: Vec<Piece> = Vec::new(); |
| 1050 | for (ri, row) in cells.into_iter().enumerate() { |
| 1051 | let by = base_rel[ri] + shift; |
| 1052 | let mut cx = Sp::ZERO; |
| 1053 | for (c, m) in row.into_iter().enumerate() { |
| 1054 | let dx = match kind { |
| 1055 | MatKind::Cases => Sp::ZERO, // left aligned |
| 1056 | MatKind::Align if c % 2 == 0 => Sp(colw[c].raw() - m.width.raw()), // right aligned |
| 1057 | MatKind::Align => Sp::ZERO, // left aligned |
| 1058 | MatKind::Matrix => Sp((colw[c].raw() - m.width.raw()) / 2), // centred |
| 1059 | }; |
| 1060 | place_into(&mut pieces, m.pieces, cx + dx, by); |
| 1061 | cx += colw[c] + col_gap; |
| 1062 | } |
| 1063 | } |
| 1064 | |
| 1065 | // Wrap the grid in its delimiters, each grown to span the grid symmetrically about the axis. |
| 1066 | if left.is_none() && right.is_none() { |
| 1067 | return Ok(MBox { pieces, width: content_w, height: content_h, depth: content_d, class: Class::Ord }); |
| 1068 | } |
| 1069 | let font = res!(math_font()); |
| 1070 | let target = delim_target(content_h + content_d); |
| 1071 | let gap = Sp(size.raw() / 8); |
| 1072 | |
| 1073 | let mut out: Vec<Piece> = Vec::new(); |
| 1074 | let mut cursor = Sp::ZERO; |
| 1075 | let mut height = content_h; |
| 1076 | let mut depth = content_d; |
| 1077 | if let Some(ch) = left { |
| 1078 | let (w, h, d) = res!(place_delim(&font, &table, ch, target, axis, size, size_pt, &mut out, cursor)); |
| 1079 | cursor += w + gap; |
| 1080 | if h > height { height = h; } |
| 1081 | if d > depth { depth = d; } |
| 1082 | } |
| 1083 | for p in pieces { |
| 1084 | out.push(Piece { x: p.x + cursor, width: p.width, rel: p.rel, draw: p.draw }); |
| 1085 | } |
| 1086 | cursor += content_w; |
| 1087 | if let Some(ch) = right { |
| 1088 | cursor += gap; |
| 1089 | let (w, h, d) = res!(place_delim(&font, &table, ch, target, axis, size, size_pt, &mut out, cursor)); |
| 1090 | cursor += w; |
| 1091 | if h > height { height = h; } |
| 1092 | if d > depth { depth = d; } |
| 1093 | } |
| 1094 | |
| 1095 | Ok(MBox { pieces: out, width: cursor, height, depth, class: Class::Ord }) |
| 1096 | } |
| 1097 | |
| 1098 | /// Sets a square root: a radical sign grown to the radicand, a vinculum ruled over it, and the radicand |
| 1099 | /// seated beneath the vinculum. The gaps, the rule thickness and the space above come from the font's |
| 1100 | /// MATH constants when it has them; the radical sign is the tallest-fitting vertical variant the MATH |
| 1101 | /// table offers, or the plain sign when the font carries no table. |
| 1102 | fn build_sqrt( |
| 1103 | style: &Theme, |
| 1104 | radicand: &Atom, |
| 1105 | level: Level, |
| 1106 | display: bool, |
| 1107 | ) |
| 1108 | -> Outcome<MBox> |
| 1109 | { |
| 1110 | let size = size_for(style, level); |
| 1111 | let size_pt = size.to_pt() as f32; |
| 1112 | let r = res!(build(style, radicand, level, display)); |
| 1113 | let table = res!(math_table()); |
| 1114 | |
| 1115 | let (gap, rule, extra) = match &table { |
| 1116 | Some(t) => { |
| 1117 | let c = t.constants(); |
| 1118 | ( |
| 1119 | Sp::from_pt(t.scaled(c.radical_vertical_gap, size_pt) as f64), |
| 1120 | Sp::from_pt(t.scaled(c.radical_rule_thickness, size_pt) as f64), |
| 1121 | Sp::from_pt(t.scaled(c.radical_extra_ascender, size_pt) as f64), |
| 1122 | ) |
| 1123 | }, |
| 1124 | None => (Sp(size.raw() / 18), style.table.rule_thin, Sp(size.raw() / 18)), |
| 1125 | }; |
| 1126 | |
| 1127 | let target = r.height + r.depth + gap + rule; // the radical sign must at least span this |
| 1128 | let font = res!(math_font()); |
| 1129 | let base = res!(glyph_id(&font, size, "\u{221A}")); |
| 1130 | let variant = match &table { |
| 1131 | Some(t) => t.variant_for(base as u16, target.to_pt() as f32, size_pt).map(|g| g as u32), |
| 1132 | None => None, |
| 1133 | }.unwrap_or(base); |
| 1134 | let (path, gh, gd, gw) = res!(glyph_ink(&font, variant, size)); |
| 1135 | |
| 1136 | let mut pieces: Vec<Piece> = Vec::new(); |
| 1137 | let kern = Sp(size.raw() / 24); |
| 1138 | |
| 1139 | // The vinculum's top edge, a rel above the baseline (negative is up): clearing the radicand by `gap`. |
| 1140 | let bar_top = -(r.height + gap + rule); |
| 1141 | // The radical sign, seated so its ink top meets the bar top. A taller variant then reaches below the |
| 1142 | // radicand, the way a radical encloses it. |
| 1143 | let sign_rel = bar_top + gh; |
| 1144 | pieces.push(Piece { x: Sp::ZERO, width: gw, rel: sign_rel, draw: Draw::Ink(path) }); |
| 1145 | |
| 1146 | // The radicand, to the right of the sign, and the vinculum ruled across it. |
| 1147 | let rx = gw + kern; |
| 1148 | place_into(&mut pieces, r.pieces, rx, Sp::ZERO); |
| 1149 | pieces.push(Piece { x: gw, width: r.width + kern, rel: bar_top, draw: Draw::Bar(rule) }); |
| 1150 | |
| 1151 | let sign_bottom = sign_rel + gd; |
| 1152 | let depth = if sign_bottom > r.depth { sign_bottom } else { r.depth }; |
| 1153 | Ok(MBox { |
| 1154 | pieces, |
| 1155 | width: gw + kern + r.width, |
| 1156 | height: r.height + gap + rule + extra, |
| 1157 | depth, |
| 1158 | class: Class::Ord, |
| 1159 | }) |
| 1160 | } |
| 1161 | |
| 1162 | /// The height a grown delimiter must reach for a content of the given symmetric height, after LaTeX's |
| 1163 | /// allowance that a delimiter may fall a little short of its content rather than jump a whole variant |
| 1164 | /// larger. The least acceptable clearance is the greater of `DelimiterFactor` (901/1000) of the content |
| 1165 | /// and the content less `DelimiterShortfall` (5 pt); the caller then takes the tightest variant reaching |
| 1166 | /// it. Covering the content outright instead runs a fence one variant larger than the New CM oracle |
| 1167 | /// wherever the content sits just above a variant -- the oversize the fidelity sweep measured -- because |
| 1168 | /// Latin Modern's variant ladder is coarse and the next size up overshoots by 15-20 %. |
| 1169 | fn delim_target(content: Sp) -> Sp { |
| 1170 | // The DelimiterFactor product is computed in i64: a tall matrix or deep fence carries a content |
| 1171 | // extent past ~36 pt, where `content.raw() * 901` overflows the i32 scaled-point domain (a debug |
| 1172 | // build panics; a release build wraps to a garbage, often negative, factor and then falls through to |
| 1173 | // `floored`). Widening only the intermediate leaves the result identical for every in-range input -- |
| 1174 | // the quotient can never exceed `content`, so narrowing back to i32 never truncates -- while a |
| 1175 | // legitimately tall content now yields the true 901/1000 factor instead of a wrapped one. |
| 1176 | let factor = Sp((content.raw() as i64 * 901 / 1000) as i32); |
| 1177 | let shortfall = Sp::from_pt(5.0); |
| 1178 | let floored = if content > shortfall { content - shortfall } else { Sp::ZERO }; |
| 1179 | if factor > floored { factor } else { floored } |
| 1180 | } |
| 1181 | |
| 1182 | /// Sets a body between a pair of delimiters grown to it. The delimiters span symmetrically about the |
| 1183 | /// maths axis, tall enough to cover the body's reach above and below that axis, less LaTeX's shortfall |
| 1184 | /// allowance ([`delim_target`]); each is the tightest-fitting vertical variant the MATH table offers, or |
| 1185 | /// the plain delimiter when the font has no table (in which case it does not grow). |
| 1186 | fn build_fence( |
| 1187 | style: &Theme, |
| 1188 | left: char, |
| 1189 | body: &Atom, |
| 1190 | right: char, |
| 1191 | level: Level, |
| 1192 | display: bool, |
| 1193 | ) |
| 1194 | -> Outcome<MBox> |
| 1195 | { |
| 1196 | let b = res!(build(style, body, level, display)); |
| 1197 | fence_around(style, left, right, b, level) |
| 1198 | } |
| 1199 | |
| 1200 | /// A binomial coefficient: the two terms stacked with no rule between them, wrapped in parentheses grown |
| 1201 | /// to the stack. Typst stacks a binom even inline, so the fraction is built bar-less and then fenced. |
| 1202 | fn build_binom( |
| 1203 | style: &Theme, |
| 1204 | top: &Atom, |
| 1205 | bottom: &Atom, |
| 1206 | level: Level, |
| 1207 | display: bool, |
| 1208 | ) |
| 1209 | -> Outcome<MBox> |
| 1210 | { |
| 1211 | let stack = res!(build_frac(style, top, bottom, level, display, false)); |
| 1212 | fence_around(style, '(', ')', stack, level) |
| 1213 | } |
| 1214 | |
| 1215 | /// Places an already-built body between a pair of delimiters grown to it, centred symmetrically about the |
| 1216 | /// maths axis. Shared by [`build_fence`], whose body comes from an [`Atom`], and [`build_binom`], whose |
| 1217 | /// body is the bar-less stacked fraction. |
| 1218 | fn fence_around( |
| 1219 | style: &Theme, |
| 1220 | left: char, |
| 1221 | right: char, |
| 1222 | b: MBox, |
| 1223 | level: Level, |
| 1224 | ) |
| 1225 | -> Outcome<MBox> |
| 1226 | { |
| 1227 | let size = size_for(style, level); |
| 1228 | let size_pt = size.to_pt() as f32; |
| 1229 | let table = res!(math_table()); |
| 1230 | let font = res!(math_font()); |
| 1231 | |
| 1232 | let axis = match &table { |
| 1233 | Some(t) => Sp::from_pt(t.scaled(t.constants().axis_height, size_pt) as f64), |
| 1234 | None => Sp(size.raw() / 4), |
| 1235 | }; |
| 1236 | // The content's symmetric reach about the axis: a delimiter is centred on the axis, so it must span |
| 1237 | // twice the body's greater reach from it. |
| 1238 | let above = b.height - axis; |
| 1239 | let below = b.depth + axis; |
| 1240 | let half = if above > below { above } else { below }; |
| 1241 | let content = half + half; |
| 1242 | |
| 1243 | let target = delim_target(content); |
| 1244 | |
| 1245 | let mut pieces: Vec<Piece> = Vec::new(); |
| 1246 | let gap = Sp(size.raw() / 12); |
| 1247 | |
| 1248 | let (lw, lh, ld) = res!(place_delim(&font, &table, left, target, axis, size, size_pt, &mut pieces, Sp::ZERO)); |
| 1249 | let mut cursor = lw + gap; |
| 1250 | let body_h = b.height; |
| 1251 | let body_d = b.depth; |
| 1252 | let body_w = b.width; |
| 1253 | place_into(&mut pieces, b.pieces, cursor, Sp::ZERO); |
| 1254 | cursor += body_w + gap; |
| 1255 | let (rw, rh, rd) = res!(place_delim(&font, &table, right, target, axis, size, size_pt, &mut pieces, cursor)); |
| 1256 | cursor += rw; |
| 1257 | |
| 1258 | Ok(MBox { |
| 1259 | pieces, |
| 1260 | width: cursor, |
| 1261 | height: body_h.max(lh).max(rh), |
| 1262 | depth: body_d.max(ld).max(rd), |
| 1263 | class: Class::Ord, |
| 1264 | }) |
| 1265 | } |
| 1266 | |
| 1267 | /// Places one delimiter for [`build_fence`], centred on the maths axis, and returns its width and its |
| 1268 | /// reach above and below the baseline. |
| 1269 | fn place_delim( |
| 1270 | font: &Arc<Font>, |
| 1271 | table: &Option<Arc<MathTable>>, |
| 1272 | ch: char, |
| 1273 | target: Sp, |
| 1274 | axis: Sp, |
| 1275 | size: Sp, |
| 1276 | size_pt: f32, |
| 1277 | pieces: &mut Vec<Piece>, |
| 1278 | x: Sp, |
| 1279 | ) |
| 1280 | -> Outcome<(Sp, Sp, Sp)> |
| 1281 | { |
| 1282 | let s = ch.to_string(); |
| 1283 | let base = res!(glyph_id(font, size, &s)); |
| 1284 | let variant = match table { |
| 1285 | Some(t) => t.variant_for(base as u16, target.to_pt() as f32, size_pt).map(|g| g as u32), |
| 1286 | None => None, |
| 1287 | }.unwrap_or(base); |
| 1288 | let (path, gh, gd, gw) = res!(glyph_ink(font, variant, size)); |
| 1289 | |
| 1290 | // Centre the glyph's ink on the axis: a glyph at rel R has its ink centre at R + (gd - gh)/2, wanted |
| 1291 | // at -axis (the axis, above the baseline). |
| 1292 | let rel = -axis - Sp((gd.raw() - gh.raw()) / 2); |
| 1293 | pieces.push(Piece { x, width: gw, rel, draw: Draw::Ink(path) }); |
| 1294 | |
| 1295 | let height = gh - rel; // ink top above the baseline, as a positive reach |
| 1296 | let depth = rel + gd; // ink bottom below the baseline |
| 1297 | Ok((gw, height, depth)) |
| 1298 | } |
| 1299 | |
| 1300 | /// Copies a child box's drawables into a parent, offset right by `dx` and down by `drel`. This is the |
| 1301 | /// one move composition needs: a row slides a child along, a fraction lifts and lowers its parts, a |
| 1302 | /// script hangs them off the base. |
| 1303 | fn place_into(dst: &mut Vec<Piece>, src: Vec<Piece>, dx: Sp, drel: Sp) { |
| 1304 | for p in src { |
| 1305 | dst.push(Piece { x: p.x + dx, width: p.width, rel: p.rel + drel, draw: p.draw }); |
| 1306 | } |
| 1307 | } |
| 1308 | |
| 1309 | /// Flattens a measured box to leaves and glue. Drawables are laid left to right; the glue before each |
| 1310 | /// carries the jump from the running cursor to the drawable's `x`, which may be negative where a |
| 1311 | /// numerator sits back over its denominator. Each leaf is shifted to `base + rel`, seating it on the |
| 1312 | /// line at its computed height above or below the baseline. A trailing glue pads the cursor out to the |
| 1313 | /// box width, so whatever follows the maths abuts it cleanly. |
| 1314 | fn emit(mbox: MBox, base: Sp) -> (Vec<Node>, Dims) { |
| 1315 | let mut pieces = mbox.pieces; |
| 1316 | pieces.sort_by_key(|p| p.x.raw()); |
| 1317 | |
| 1318 | let mut nodes: Vec<Node> = Vec::new(); |
| 1319 | let mut cursor = Sp::ZERO; |
| 1320 | for p in pieces { |
| 1321 | let gap = p.x - cursor; |
| 1322 | if gap != Sp::ZERO { |
| 1323 | nodes.push(Node::Glue(Glue::fixed(gap))); |
| 1324 | } |
| 1325 | let shift = base + p.rel; |
| 1326 | match p.draw { |
| 1327 | Draw::Glyph(shaped) => { |
| 1328 | // The run draws its baseline at the leaf's placement y, so a zero height plus the shift |
| 1329 | // puts the baseline exactly at `base + rel` below the line top. |
| 1330 | let dims = Dims::new(p.width, Sp::ZERO, Sp::ZERO); |
| 1331 | nodes.push(Node::Leaf(Leaf::text_dims(shaped, dims).with_shift(shift))); |
| 1332 | }, |
| 1333 | Draw::Bar(t) => { |
| 1334 | let dims = Dims::new(p.width, t, Sp::ZERO); |
| 1335 | nodes.push(Node::Leaf(Leaf::rule(dims).with_shift(shift))); |
| 1336 | }, |
| 1337 | Draw::Ink(path) => { |
| 1338 | // A grown delimiter or radical: its flipped outline as a one-op graphic, seated on the |
| 1339 | // line by the same shift as a glyph. The box is zero-height, so the shift alone seats it. |
| 1340 | let g = Graphic::new( |
| 1341 | vec![DrawOp::Fill { path, colour: Rgba::BLACK }], |
| 1342 | Dims::new(p.width, Sp::ZERO, Sp::ZERO)); |
| 1343 | nodes.push(Node::Leaf(Leaf::graphic(g).with_shift(shift))); |
| 1344 | }, |
| 1345 | } |
| 1346 | cursor = p.x + p.width; |
| 1347 | } |
| 1348 | if cursor < mbox.width { |
| 1349 | nodes.push(Node::Glue(Glue::fixed(mbox.width - cursor))); |
| 1350 | } |
| 1351 | |
| 1352 | // The reported extent is the maths box's own: its height above the baseline and its depth below. |
| 1353 | // The caller reads these -- an inline caller compares the height against the surrounding ascent to |
| 1354 | // find how far the maths climbs above the line, a display caller takes the height as the box's own. |
| 1355 | let dims = Dims::new(mbox.width, mbox.height, mbox.depth); |
| 1356 | (nodes, dims) |
| 1357 | } |
| 1358 | |
| 1359 | #[cfg(test)] |
| 1360 | mod tests { |
| 1361 | use super::*; |
| 1362 | |
| 1363 | // A delimiter target for a tall matrix (content extent well past 36 pt) once overflowed the i32 |
| 1364 | // scaled-point product `content.raw() * 901`: a debug build panicked, a release build wrapped to a |
| 1365 | // garbage factor. The widened arithmetic must yield the true DelimiterFactor (901/1000) instead. |
| 1366 | #[test] |
| 1367 | fn delim_target_tall_content_no_overflow() { |
| 1368 | // 50 pt of content: content.raw() * 901 = 2.95e9, past i32::MAX (2.147e9). |
| 1369 | let content = Sp::from_pt(50.0); |
| 1370 | let got = delim_target(content); |
| 1371 | // 0.901 * 50 pt = 45.05 pt, which exceeds content less the 5 pt shortfall (45 pt), so the factor |
| 1372 | // wins. A wrapped product would have given a negative or tiny factor, falling through to 45 pt. |
| 1373 | let expect = Sp((content.raw() as i64 * 901 / 1000) as i32); |
| 1374 | assert_eq!(got, expect); |
| 1375 | assert!(got.raw() > 0); |
| 1376 | assert!(got > content - Sp::from_pt(5.0)); |
| 1377 | } |
| 1378 | |
| 1379 | // In-range content is unchanged by the widening: below ~36 pt the i32 product never overflowed, so |
| 1380 | // the result must match exactly what it always was. |
| 1381 | #[test] |
| 1382 | fn delim_target_small_content_unchanged() { |
| 1383 | let content = Sp::from_pt(20.0); |
| 1384 | let got = delim_target(content); |
| 1385 | assert_eq!(got, Sp(content.raw() * 901 / 1000)); |
| 1386 | } |
| 1387 | } |