typst_layout/math/
attach.rs

1// Can be re-enabled once `Option::map_or_default` is stable in our MSRV.
2#![allow(unstable_name_collisions)]
3
4// Is unused in compiler versions where `Option::map_or_default` is stable.
5#[allow(unused_imports)]
6use typst_utils::OptionExt;
7
8use typst_library::diag::SourceResult;
9use typst_library::foundations::{Packed, StyleChain, SymbolElem};
10use typst_library::layout::{Abs, Axis, Corner, Frame, Point, Rel, Size};
11use typst_library::math::{
12    AttachElem, EquationElem, LimitsElem, PrimesElem, ScriptsElem, StretchElem,
13};
14use typst_library::text::Font;
15
16use super::{
17    FrameFragment, Limits, MathContext, MathFragment, stretch_fragment,
18    style_for_subscript, style_for_superscript,
19};
20
21macro_rules! measure {
22    ($e: ident, $attr: ident) => {
23        $e.as_ref().map(|e| e.$attr()).unwrap_or_default()
24    };
25}
26
27/// Lays out an [`AttachElem`].
28#[typst_macros::time(name = "math.attach", span = elem.span())]
29pub fn layout_attach(
30    elem: &Packed<AttachElem>,
31    ctx: &mut MathContext,
32    styles: StyleChain,
33) -> SourceResult<()> {
34    let merged = elem.merge_base();
35    let elem = merged.as_ref().unwrap_or(elem);
36    let stretch = stretch_size(styles, elem);
37
38    let mut base = ctx.layout_into_fragment(&elem.base, styles)?;
39    let sup_style = style_for_superscript(styles);
40    let sup_style_chain = styles.chain(&sup_style);
41    let tl = elem.tl.get_cloned(sup_style_chain);
42    let tr = elem.tr.get_cloned(sup_style_chain);
43    let primed = tr.as_ref().is_some_and(|content| content.is::<PrimesElem>());
44    let t = elem.t.get_cloned(sup_style_chain);
45
46    let sub_style = style_for_subscript(styles);
47    let sub_style_chain = styles.chain(&sub_style);
48    let bl = elem.bl.get_cloned(sub_style_chain);
49    let br = elem.br.get_cloned(sub_style_chain);
50    let b = elem.b.get_cloned(sub_style_chain);
51
52    let limits = base.limits().active(styles);
53    let (t, tr) = match (t, tr) {
54        (Some(t), Some(tr)) if primed && !limits => (None, Some(tr + t)),
55        (Some(t), None) if !limits => (None, Some(t)),
56        (t, tr) => (t, tr),
57    };
58    let (b, br) = if limits || br.is_some() { (b, br) } else { (None, b) };
59
60    macro_rules! layout {
61        ($content:ident, $style_chain:ident) => {
62            $content
63                .map(|elem| ctx.layout_into_fragment(&elem, $style_chain))
64                .transpose()
65        };
66    }
67
68    // Layout the top and bottom attachments early so we can measure their
69    // widths, in order to calculate what the stretch size is relative to.
70    let t = layout!(t, sup_style_chain)?;
71    let b = layout!(b, sub_style_chain)?;
72    if let Some(stretch) = stretch {
73        let relative_to_width = measure!(t, width).max(measure!(b, width));
74        stretch_fragment(
75            ctx,
76            &mut base,
77            Some(Axis::X),
78            Some(relative_to_width),
79            stretch,
80            Abs::zero(),
81        );
82    }
83
84    let fragments = [
85        layout!(tl, sup_style_chain)?,
86        t,
87        layout!(tr, sup_style_chain)?,
88        layout!(bl, sub_style_chain)?,
89        b,
90        layout!(br, sub_style_chain)?,
91    ];
92
93    layout_attachments(ctx, styles, base, fragments)
94}
95
96/// Lays out a [`PrimeElem`].
97#[typst_macros::time(name = "math.primes", span = elem.span())]
98pub fn layout_primes(
99    elem: &Packed<PrimesElem>,
100    ctx: &mut MathContext,
101    styles: StyleChain,
102) -> SourceResult<()> {
103    match elem.count {
104        count @ 1..=4 => {
105            let c = match count {
106                1 => '′',
107                2 => '″',
108                3 => '‴',
109                4 => '⁗',
110                _ => unreachable!(),
111            };
112            let f = ctx.layout_into_fragment(
113                &SymbolElem::packed(c).spanned(elem.span()),
114                styles,
115            )?;
116            ctx.push(f);
117        }
118        count => {
119            // Custom amount of primes
120            let prime = ctx
121                .layout_into_fragment(
122                    &SymbolElem::packed('′').spanned(elem.span()),
123                    styles,
124                )?
125                .into_frame();
126            let width = prime.width() * (count + 1) as f64 / 2.0;
127            let mut frame = Frame::soft(Size::new(width, prime.height()));
128            frame.set_baseline(prime.ascent());
129
130            for i in 0..count {
131                frame.push_frame(
132                    Point::new(prime.width() * (i as f64 / 2.0), Abs::zero()),
133                    prime.clone(),
134                )
135            }
136            ctx.push(FrameFragment::new(styles, frame).with_text_like(true));
137        }
138    }
139    Ok(())
140}
141
142/// Lays out a [`ScriptsElem`].
143#[typst_macros::time(name = "math.scripts", span = elem.span())]
144pub fn layout_scripts(
145    elem: &Packed<ScriptsElem>,
146    ctx: &mut MathContext,
147    styles: StyleChain,
148) -> SourceResult<()> {
149    let mut fragment = ctx.layout_into_fragment(&elem.body, styles)?;
150    fragment.set_limits(Limits::Never);
151    ctx.push(fragment);
152    Ok(())
153}
154
155/// Lays out a [`LimitsElem`].
156#[typst_macros::time(name = "math.limits", span = elem.span())]
157pub fn layout_limits(
158    elem: &Packed<LimitsElem>,
159    ctx: &mut MathContext,
160    styles: StyleChain,
161) -> SourceResult<()> {
162    let limits = if elem.inline.get(styles) { Limits::Always } else { Limits::Display };
163    let mut fragment = ctx.layout_into_fragment(&elem.body, styles)?;
164    fragment.set_limits(limits);
165    ctx.push(fragment);
166    Ok(())
167}
168
169/// Get the size to stretch the base to.
170fn stretch_size(styles: StyleChain, elem: &Packed<AttachElem>) -> Option<Rel<Abs>> {
171    // Extract from an EquationElem.
172    let mut base = &elem.base;
173    while let Some(equation) = base.to_packed::<EquationElem>() {
174        base = &equation.body;
175    }
176
177    base.to_packed::<StretchElem>()
178        .map(|stretch| stretch.size.resolve(styles))
179}
180
181/// Lay out the attachments.
182fn layout_attachments(
183    ctx: &mut MathContext,
184    styles: StyleChain,
185    base: MathFragment,
186    [tl, t, tr, bl, b, br]: [Option<MathFragment>; 6],
187) -> SourceResult<()> {
188    let class = base.class();
189    let (font, size) = base.font(ctx, styles);
190    let cramped = styles.get(EquationElem::cramped);
191
192    // Calculate the distance from the base's baseline to the superscripts' and
193    // subscripts' baseline.
194    let (tx_shift, bx_shift) = if [&tl, &tr, &bl, &br].iter().all(|e| e.is_none()) {
195        (Abs::zero(), Abs::zero())
196    } else {
197        compute_script_shifts(&font, size, cramped, &base, [&tl, &tr, &bl, &br])
198    };
199
200    // Calculate the distance from the base's baseline to the top attachment's
201    // and bottom attachment's baseline.
202    let (t_shift, b_shift) =
203        compute_limit_shifts(&font, size, &base, [t.as_ref(), b.as_ref()]);
204
205    // Calculate the final frame height.
206    let ascent = base
207        .ascent()
208        .max(tx_shift + measure!(tr, ascent))
209        .max(tx_shift + measure!(tl, ascent))
210        .max(t_shift + measure!(t, ascent));
211    let descent = base
212        .descent()
213        .max(bx_shift + measure!(br, descent))
214        .max(bx_shift + measure!(bl, descent))
215        .max(b_shift + measure!(b, descent));
216    let height = ascent + descent;
217
218    // Calculate the vertical position of each element in the final frame.
219    let base_y = ascent - base.ascent();
220    let tx_y = |tx: &MathFragment| ascent - tx_shift - tx.ascent();
221    let bx_y = |bx: &MathFragment| ascent + bx_shift - bx.ascent();
222    let t_y = |t: &MathFragment| ascent - t_shift - t.ascent();
223    let b_y = |b: &MathFragment| ascent + b_shift - b.ascent();
224
225    // Calculate the distance each limit extends to the left and right of the
226    // base's width.
227    let ((t_pre_width, t_post_width), (b_pre_width, b_post_width)) =
228        compute_limit_widths(&base, [t.as_ref(), b.as_ref()]);
229
230    // `space_after_script` is extra spacing that is at the start before each
231    // pre-script, and at the end after each post-script (see the MathConstants
232    // table in the OpenType MATH spec).
233    let space_after_script = font.math().space_after_script.at(size);
234
235    // Calculate the distance each pre-script extends to the left of the base's
236    // width.
237    let (tl_pre_width, bl_pre_width) = compute_pre_script_widths(
238        &base,
239        [tl.as_ref(), bl.as_ref()],
240        (tx_shift, bx_shift),
241        space_after_script,
242    );
243
244    // Calculate the distance each post-script extends to the right of the
245    // base's width. Also calculate each post-script's kerning (we need this for
246    // its position later).
247    let ((tr_post_width, tr_kern), (br_post_width, br_kern)) = compute_post_script_widths(
248        &base,
249        [tr.as_ref(), br.as_ref()],
250        (tx_shift, bx_shift),
251        space_after_script,
252    );
253
254    // Calculate the final frame width.
255    let pre_width = t_pre_width.max(b_pre_width).max(tl_pre_width).max(bl_pre_width);
256    let base_width = base.width();
257    let post_width = t_post_width.max(b_post_width).max(tr_post_width).max(br_post_width);
258    let width = pre_width + base_width + post_width;
259
260    // Calculate the horizontal position of each element in the final frame.
261    let base_x = pre_width;
262    let tl_x = pre_width - tl_pre_width + space_after_script;
263    let bl_x = pre_width - bl_pre_width + space_after_script;
264    let tr_x = pre_width + base_width + tr_kern;
265    let br_x = pre_width + base_width + br_kern;
266    let t_x = pre_width - t_pre_width;
267    let b_x = pre_width - b_pre_width;
268
269    // Create the final frame.
270    let mut frame = Frame::soft(Size::new(width, height));
271    frame.set_baseline(ascent);
272    frame.push_frame(Point::new(base_x, base_y), base.into_frame());
273
274    macro_rules! layout {
275        ($e: ident, $x: ident, $y: ident) => {
276            if let Some($e) = $e {
277                frame.push_frame(Point::new($x, $y(&$e)), $e.into_frame());
278            }
279        };
280    }
281
282    layout!(tl, tl_x, tx_y); // pre-superscript
283    layout!(bl, bl_x, bx_y); // pre-subscript
284    layout!(tr, tr_x, tx_y); // post-superscript
285    layout!(br, br_x, bx_y); // post-subscript
286    layout!(t, t_x, t_y); // upper-limit
287    layout!(b, b_x, b_y); // lower-limit
288
289    // Done! Note that we retain the class of the base.
290    ctx.push(FrameFragment::new(styles, frame).with_class(class));
291
292    Ok(())
293}
294
295/// Calculate the distance each post-script extends to the right of the base's
296/// width, as well as its kerning value. Requires the distance from the base's
297/// baseline to each post-script's baseline to obtain the correct kerning value.
298/// Returns 2 tuples of two lengths, each first containing the distance the
299/// post-script extends left of the base's width and second containing the
300/// post-script's kerning value. The first tuple is for the post-superscript,
301/// and the second is for the post-subscript.
302fn compute_post_script_widths(
303    base: &MathFragment,
304    [tr, br]: [Option<&MathFragment>; 2],
305    (tr_shift, br_shift): (Abs, Abs),
306    space_after_post_script: Abs,
307) -> ((Abs, Abs), (Abs, Abs)) {
308    let tr_values = tr.map_or_default(|tr| {
309        let kern = math_kern(base, tr, tr_shift, Corner::TopRight);
310        (space_after_post_script + tr.width() + kern, kern)
311    });
312
313    // The base's bounding box already accounts for its italic correction, so we
314    // need to shift the post-subscript left by the base's italic correction
315    // (see the kerning algorithm as described in the OpenType MATH spec).
316    let br_values = br.map_or_default(|br| {
317        let kern = math_kern(base, br, br_shift, Corner::BottomRight)
318            - base.italics_correction();
319        (space_after_post_script + br.width() + kern, kern)
320    });
321
322    (tr_values, br_values)
323}
324
325/// Calculate the distance each pre-script extends to the left of the base's
326/// width. Requires the distance from the base's baseline to each pre-script's
327/// baseline to obtain the correct kerning value.
328/// Returns two lengths, the first being the distance the pre-superscript
329/// extends left of the base's width and the second being the distance the
330/// pre-subscript extends left of the base's width.
331fn compute_pre_script_widths(
332    base: &MathFragment,
333    [tl, bl]: [Option<&MathFragment>; 2],
334    (tl_shift, bl_shift): (Abs, Abs),
335    space_before_pre_script: Abs,
336) -> (Abs, Abs) {
337    let tl_pre_width = tl.map_or_default(|tl| {
338        let kern = math_kern(base, tl, tl_shift, Corner::TopLeft);
339        space_before_pre_script + tl.width() + kern
340    });
341
342    let bl_pre_width = bl.map_or_default(|bl| {
343        let kern = math_kern(base, bl, bl_shift, Corner::BottomLeft);
344        space_before_pre_script + bl.width() + kern
345    });
346
347    (tl_pre_width, bl_pre_width)
348}
349
350/// Calculate the distance each limit extends beyond the base's width, in each
351/// direction. Can be a negative value if the limit does not extend beyond the
352/// base's width, indicating how far into the base's width the limit extends.
353/// Returns 2 tuples of two lengths, each first containing the distance the
354/// limit extends leftward beyond the base's width and second containing the
355/// distance the limit extends rightward beyond the base's width. The first
356/// tuple is for the upper-limit, and the second is for the lower-limit.
357fn compute_limit_widths(
358    base: &MathFragment,
359    [t, b]: [Option<&MathFragment>; 2],
360) -> ((Abs, Abs), (Abs, Abs)) {
361    // The upper- (lower-) limit is shifted to the right (left) of the base's
362    // center by half the base's italic correction.
363    let delta = base.italics_correction() / 2.0;
364
365    let t_widths = t.map_or_default(|t| {
366        let half = (t.width() - base.width()) / 2.0;
367        (half - delta, half + delta)
368    });
369
370    let b_widths = b.map_or_default(|b| {
371        let half = (b.width() - base.width()) / 2.0;
372        (half + delta, half - delta)
373    });
374
375    (t_widths, b_widths)
376}
377
378/// Calculate the distance from the base's baseline to each limit's baseline.
379/// Returns two lengths, the first being the distance to the upper-limit's
380/// baseline and the second being the distance to the lower-limit's baseline.
381fn compute_limit_shifts(
382    font: &Font,
383    font_size: Abs,
384    base: &MathFragment,
385    [t, b]: [Option<&MathFragment>; 2],
386) -> (Abs, Abs) {
387    // `upper_gap_min` and `lower_gap_min` give gaps to the descender and
388    // ascender of the limits respectively, whereas `upper_rise_min` and
389    // `lower_drop_min` give gaps to each limit's baseline (see the
390    // MathConstants table in the OpenType MATH spec).
391    let t_shift = t.map_or_default(|t| {
392        let upper_gap_min = font.math().upper_limit_gap_min.at(font_size);
393        let upper_rise_min = font.math().upper_limit_baseline_rise_min.at(font_size);
394        base.ascent() + upper_rise_min.max(upper_gap_min + t.descent())
395    });
396
397    let b_shift = b.map_or_default(|b| {
398        let lower_gap_min = font.math().lower_limit_gap_min.at(font_size);
399        let lower_drop_min = font.math().lower_limit_baseline_drop_min.at(font_size);
400        base.descent() + lower_drop_min.max(lower_gap_min + b.ascent())
401    });
402
403    (t_shift, b_shift)
404}
405
406/// Calculate the distance from the base's baseline to each script's baseline.
407/// Returns two lengths, the first being the distance to the superscripts'
408/// baseline and the second being the distance to the subscripts' baseline.
409fn compute_script_shifts(
410    font: &Font,
411    font_size: Abs,
412    cramped: bool,
413    base: &MathFragment,
414    [tl, tr, bl, br]: [&Option<MathFragment>; 4],
415) -> (Abs, Abs) {
416    let sup_shift_up = (if cramped {
417        font.math().superscript_shift_up_cramped
418    } else {
419        font.math().superscript_shift_up
420    })
421    .at(font_size);
422
423    let sup_bottom_min = font.math().superscript_bottom_min.at(font_size);
424    let sup_bottom_max_with_sub =
425        font.math().superscript_bottom_max_with_subscript.at(font_size);
426    let sup_drop_max = font.math().superscript_baseline_drop_max.at(font_size);
427    let gap_min = font.math().sub_superscript_gap_min.at(font_size);
428    let sub_shift_down = font.math().subscript_shift_down.at(font_size);
429    let sub_top_max = font.math().subscript_top_max.at(font_size);
430    let sub_drop_min = font.math().subscript_baseline_drop_min.at(font_size);
431
432    let mut shift_up = Abs::zero();
433    let mut shift_down = Abs::zero();
434    let is_text_like = base.is_text_like();
435
436    if tl.is_some() || tr.is_some() {
437        let ascent = match &base {
438            MathFragment::Frame(frame) => frame.base_ascent,
439            _ => base.ascent(),
440        };
441        shift_up = shift_up
442            .max(sup_shift_up)
443            .max(if is_text_like { Abs::zero() } else { ascent - sup_drop_max })
444            .max(sup_bottom_min + measure!(tl, descent))
445            .max(sup_bottom_min + measure!(tr, descent));
446    }
447
448    if bl.is_some() || br.is_some() {
449        let descent = match &base {
450            MathFragment::Frame(frame) => frame.base_descent,
451            _ => base.descent(),
452        };
453        shift_down = shift_down
454            .max(sub_shift_down)
455            .max(if is_text_like { Abs::zero() } else { descent + sub_drop_min })
456            .max(measure!(bl, ascent) - sub_top_max)
457            .max(measure!(br, ascent) - sub_top_max);
458    }
459
460    for (sup, sub) in [(tl, bl), (tr, br)] {
461        if let (Some(sup), Some(sub)) = (&sup, &sub) {
462            let sup_bottom = shift_up - sup.descent();
463            let sub_top = sub.ascent() - shift_down;
464            let gap = sup_bottom - sub_top;
465            if gap >= gap_min {
466                continue;
467            }
468
469            let increase = gap_min - gap;
470            let sup_only =
471                (sup_bottom_max_with_sub - sup_bottom).clamp(Abs::zero(), increase);
472            let rest = (increase - sup_only) / 2.0;
473            shift_up += sup_only + rest;
474            shift_down += rest;
475        }
476    }
477
478    (shift_up, shift_down)
479}
480
481/// Calculate the kerning value for a script with respect to the base. A
482/// positive value means shifting the script further away from the base, whereas
483/// a negative value means shifting the script closer to the base. Requires the
484/// distance from the base's baseline to the script's baseline, as well as the
485/// script's corner (tl, tr, bl, br).
486fn math_kern(base: &MathFragment, script: &MathFragment, shift: Abs, pos: Corner) -> Abs {
487    // This process is described under the MathKernInfo table in the OpenType
488    // MATH spec.
489
490    let (corr_height_top, corr_height_bot) = match pos {
491        // Calculate two correction heights for superscripts:
492        // - The distance from the superscript's baseline to the top of the
493        //   base's bounding box.
494        // - The distance from the base's baseline to the bottom of the
495        //   superscript's bounding box.
496        Corner::TopLeft | Corner::TopRight => {
497            (base.ascent() - shift, shift - script.descent())
498        }
499        // Calculate two correction heights for subscripts:
500        // - The distance from the base's baseline to the top of the
501        //   subscript's bounding box.
502        // - The distance from the subscript's baseline to the bottom of the
503        //   base's bounding box.
504        Corner::BottomLeft | Corner::BottomRight => {
505            (script.ascent() - shift, shift - base.descent())
506        }
507    };
508
509    // Calculate the sum of kerning values for each correction height.
510    let summed_kern = |height| {
511        let base_kern = base.kern_at_height(pos, height);
512        let attach_kern = script.kern_at_height(pos.inv(), height);
513        base_kern + attach_kern
514    };
515
516    // Take the smaller kerning amount (and so the larger value). Note that
517    // there is a bug in the spec (as of 2024-08-15): it says to take the
518    // minimum of the two sums, but as the kerning value is usually negative it
519    // really means the smaller kern. The current wording of the spec could
520    // result in glyphs colliding.
521    summed_kern(corr_height_top).max(summed_kern(corr_height_bot))
522}