Packages

Print-quality PDF from Markdown + Elixir, powered by Typst

Current section

Files

Jump to
folio vendor typst crates typst-layout src math scripts.rs
Raw

vendor/typst/crates/typst-layout/src/math/scripts.rs

// Can be re-enabled once `Option::map_or_default` is stable in our MSRV.
#![allow(unstable_name_collisions)]
// Is unused in compiler versions where `Option::map_or_default` is stable.
#[allow(unused_imports)]
use typst_utils::OptionExt;
use typst_library::diag::SourceResult;
use typst_library::foundations::StyleChain;
use typst_library::layout::{Abs, Axis, Corner, Frame, Point, Size};
use typst_library::math::EquationElem;
use typst_library::math::ir::{MathProperties, PrimesItem, ScriptsItem};
use typst_library::text::Font;
use super::MathContext;
use super::fragment::{FrameFragment, MathFragment};
macro_rules! measure {
($e: ident, $attr: ident) => {
$e.as_ref().map(|e| e.$attr()).unwrap_or_default()
};
}
/// Lays out a [`ScriptsItem`].
#[typst_macros::time(name = "math scripts layout", span = props.span)]
pub fn layout_scripts(
item: &ScriptsItem,
ctx: &mut MathContext,
styles: StyleChain,
props: &MathProperties,
) -> SourceResult<()> {
macro_rules! layout {
($content:ident) => {
item.$content
.as_ref()
.map(|script| ctx.layout_into_fragment(&script, styles))
.transpose()?
};
}
// Layout the top and bottom attachments early so we can measure their
// widths, in order to calculate what the stretch size is relative to.
let t = layout!(top);
let b = layout!(bottom);
let relative_to_width = measure!(t, width).max(measure!(b, width));
item.base.set_stretch_relative_to(relative_to_width, Axis::X);
let base = ctx.layout_into_fragment(&item.base, styles)?;
let fragments = [
layout!(top_left),
t,
layout!(top_right),
layout!(bottom_left),
b,
layout!(bottom_right),
];
layout_attachments(ctx, props, item.base.styles().unwrap_or(styles), base, fragments)
}
/// Lays out a [`PrimesItem`].
#[typst_macros::time(name = "math primes layout", span = props.span)]
pub fn layout_primes(
item: &PrimesItem,
ctx: &mut MathContext,
styles: StyleChain,
props: &MathProperties,
) -> SourceResult<()> {
let prime = ctx.layout_into_fragment(&item.prime, styles)?.into_frame();
let width = prime.width() * (item.count + 1) as f64 / 2.0;
let mut frame = Frame::soft(Size::new(width, prime.height()));
frame.set_baseline(prime.ascent());
for i in 0..item.count {
frame.push_frame(
Point::new(prime.width() * (i as f64 / 2.0), Abs::zero()),
prime.clone(),
)
}
ctx.push(FrameFragment::new(props, styles, frame).with_text_like(true));
Ok(())
}
/// Lay out the attachments.
fn layout_attachments(
ctx: &mut MathContext,
props: &MathProperties,
styles: StyleChain,
base: MathFragment,
[tl, t, tr, bl, b, br]: [Option<MathFragment>; 6],
) -> SourceResult<()> {
let (font, size) = base.font(ctx, styles);
let cramped = styles.get(EquationElem::cramped);
// Calculate the distance from the base's baseline to the superscripts' and
// subscripts' baseline.
let (tx_shift, bx_shift) = if [&tl, &tr, &bl, &br].iter().all(|e| e.is_none()) {
(Abs::zero(), Abs::zero())
} else {
compute_script_shifts(&font, size, cramped, &base, [&tl, &tr, &bl, &br])
};
// Calculate the distance from the base's baseline to the top attachment's
// and bottom attachment's baseline.
let (t_shift, b_shift) =
compute_limit_shifts(&font, size, &base, [t.as_ref(), b.as_ref()]);
// Calculate the final frame height.
let ascent = base
.ascent()
.max(tx_shift + measure!(tr, ascent))
.max(tx_shift + measure!(tl, ascent))
.max(t_shift + measure!(t, ascent));
let descent = base
.descent()
.max(bx_shift + measure!(br, descent))
.max(bx_shift + measure!(bl, descent))
.max(b_shift + measure!(b, descent));
let height = ascent + descent;
// Calculate the vertical position of each element in the final frame.
let base_y = ascent - base.ascent();
let tx_y = |tx: &MathFragment| ascent - tx_shift - tx.ascent();
let bx_y = |bx: &MathFragment| ascent + bx_shift - bx.ascent();
let t_y = |t: &MathFragment| ascent - t_shift - t.ascent();
let b_y = |b: &MathFragment| ascent + b_shift - b.ascent();
// Calculate the distance each limit extends to the left and right of the
// base's width.
let ((t_pre_width, t_post_width), (b_pre_width, b_post_width)) =
compute_limit_widths(&base, [t.as_ref(), b.as_ref()]);
// `space_after_script` is extra spacing that is at the start before each
// pre-script, and at the end after each post-script (see the MathConstants
// table in the OpenType MATH spec).
let space_after_script = font.math().space_after_script.at(size);
// Calculate the distance each pre-script extends to the left of the base's
// width.
let (tl_pre_width, bl_pre_width) = compute_pre_script_widths(
&base,
[tl.as_ref(), bl.as_ref()],
(tx_shift, bx_shift),
space_after_script,
);
// Calculate the distance each post-script extends to the right of the
// base's width. Also calculate each post-script's kerning (we need this for
// its position later).
let ((tr_post_width, tr_kern), (br_post_width, br_kern)) = compute_post_script_widths(
&base,
[tr.as_ref(), br.as_ref()],
(tx_shift, bx_shift),
space_after_script,
);
// Calculate the final frame width.
let pre_width = t_pre_width.max(b_pre_width).max(tl_pre_width).max(bl_pre_width);
let base_width = base.width();
let post_width = t_post_width.max(b_post_width).max(tr_post_width).max(br_post_width);
let width = pre_width + base_width + post_width;
// Calculate the horizontal position of each element in the final frame.
let base_x = pre_width;
let tl_x = pre_width - tl_pre_width + space_after_script;
let bl_x = pre_width - bl_pre_width + space_after_script;
let tr_x = pre_width + base_width + tr_kern;
let br_x = pre_width + base_width + br_kern;
let t_x = pre_width - t_pre_width;
let b_x = pre_width - b_pre_width;
// Create the final frame.
let mut frame = Frame::soft(Size::new(width, height));
frame.set_baseline(ascent);
frame.push_frame(Point::new(base_x, base_y), base.into_frame());
macro_rules! layout {
($e: ident, $x: ident, $y: ident) => {
if let Some($e) = $e {
frame.push_frame(Point::new($x, $y(&$e)), $e.into_frame());
}
};
}
layout!(tl, tl_x, tx_y); // pre-superscript
layout!(bl, bl_x, bx_y); // pre-subscript
layout!(tr, tr_x, tx_y); // post-superscript
layout!(br, br_x, bx_y); // post-subscript
layout!(t, t_x, t_y); // upper-limit
layout!(b, b_x, b_y); // lower-limit
// Done!
ctx.push(FrameFragment::new(props, styles, frame));
Ok(())
}
/// Calculate the distance each post-script extends to the right of the base's
/// width, as well as its kerning value. Requires the distance from the base's
/// baseline to each post-script's baseline to obtain the correct kerning value.
/// Returns 2 tuples of two lengths, each first containing the distance the
/// post-script extends left of the base's width and second containing the
/// post-script's kerning value. The first tuple is for the post-superscript,
/// and the second is for the post-subscript.
fn compute_post_script_widths(
base: &MathFragment,
[tr, br]: [Option<&MathFragment>; 2],
(tr_shift, br_shift): (Abs, Abs),
space_after_post_script: Abs,
) -> ((Abs, Abs), (Abs, Abs)) {
let tr_values = tr.map_or_default(|tr| {
let kern = math_kern(base, tr, tr_shift, Corner::TopRight);
(space_after_post_script + tr.width() + kern, kern)
});
// The base's bounding box already accounts for its italic correction, so we
// need to shift the post-subscript left by the base's italic correction
// (see the kerning algorithm as described in the OpenType MATH spec).
let br_values = br.map_or_default(|br| {
let kern = math_kern(base, br, br_shift, Corner::BottomRight)
- base.italics_correction();
(space_after_post_script + br.width() + kern, kern)
});
(tr_values, br_values)
}
/// Calculate the distance each pre-script extends to the left of the base's
/// width. Requires the distance from the base's baseline to each pre-script's
/// baseline to obtain the correct kerning value.
/// Returns two lengths, the first being the distance the pre-superscript
/// extends left of the base's width and the second being the distance the
/// pre-subscript extends left of the base's width.
fn compute_pre_script_widths(
base: &MathFragment,
[tl, bl]: [Option<&MathFragment>; 2],
(tl_shift, bl_shift): (Abs, Abs),
space_before_pre_script: Abs,
) -> (Abs, Abs) {
let tl_pre_width = tl.map_or_default(|tl| {
let kern = math_kern(base, tl, tl_shift, Corner::TopLeft);
space_before_pre_script + tl.width() + kern
});
let bl_pre_width = bl.map_or_default(|bl| {
let kern = math_kern(base, bl, bl_shift, Corner::BottomLeft);
space_before_pre_script + bl.width() + kern
});
(tl_pre_width, bl_pre_width)
}
/// Calculate the distance each limit extends beyond the base's width, in each
/// direction. Can be a negative value if the limit does not extend beyond the
/// base's width, indicating how far into the base's width the limit extends.
/// Returns 2 tuples of two lengths, each first containing the distance the
/// limit extends leftward beyond the base's width and second containing the
/// distance the limit extends rightward beyond the base's width. The first
/// tuple is for the upper-limit, and the second is for the lower-limit.
fn compute_limit_widths(
base: &MathFragment,
[t, b]: [Option<&MathFragment>; 2],
) -> ((Abs, Abs), (Abs, Abs)) {
// The upper- (lower-) limit is shifted to the right (left) of the base's
// center by half the base's italic correction.
let delta = base.italics_correction() / 2.0;
let t_widths = t.map_or_default(|t| {
let half = (t.width() - base.width()) / 2.0;
(half - delta, half + delta)
});
let b_widths = b.map_or_default(|b| {
let half = (b.width() - base.width()) / 2.0;
(half + delta, half - delta)
});
(t_widths, b_widths)
}
/// Calculate the distance from the base's baseline to each limit's baseline.
/// Returns two lengths, the first being the distance to the upper-limit's
/// baseline and the second being the distance to the lower-limit's baseline.
fn compute_limit_shifts(
font: &Font,
font_size: Abs,
base: &MathFragment,
[t, b]: [Option<&MathFragment>; 2],
) -> (Abs, Abs) {
// `upper_gap_min` and `lower_gap_min` give gaps to the descender and
// ascender of the limits respectively, whereas `upper_rise_min` and
// `lower_drop_min` give gaps to each limit's baseline (see the
// MathConstants table in the OpenType MATH spec).
let t_shift = t.map_or_default(|t| {
let upper_gap_min = font.math().upper_limit_gap_min.at(font_size);
let upper_rise_min = font.math().upper_limit_baseline_rise_min.at(font_size);
base.ascent() + upper_rise_min.max(upper_gap_min + t.descent())
});
let b_shift = b.map_or_default(|b| {
let lower_gap_min = font.math().lower_limit_gap_min.at(font_size);
let lower_drop_min = font.math().lower_limit_baseline_drop_min.at(font_size);
base.descent() + lower_drop_min.max(lower_gap_min + b.ascent())
});
(t_shift, b_shift)
}
/// Calculate the distance from the base's baseline to each script's baseline.
/// Returns two lengths, the first being the distance to the superscripts'
/// baseline and the second being the distance to the subscripts' baseline.
fn compute_script_shifts(
font: &Font,
font_size: Abs,
cramped: bool,
base: &MathFragment,
[tl, tr, bl, br]: [&Option<MathFragment>; 4],
) -> (Abs, Abs) {
let sup_shift_up = (if cramped {
font.math().superscript_shift_up_cramped
} else {
font.math().superscript_shift_up
})
.at(font_size);
let sup_bottom_min = font.math().superscript_bottom_min.at(font_size);
let sup_bottom_max_with_sub =
font.math().superscript_bottom_max_with_subscript.at(font_size);
let sup_drop_max = font.math().superscript_baseline_drop_max.at(font_size);
let gap_min = font.math().sub_superscript_gap_min.at(font_size);
let sub_shift_down = font.math().subscript_shift_down.at(font_size);
let sub_top_max = font.math().subscript_top_max.at(font_size);
let sub_drop_min = font.math().subscript_baseline_drop_min.at(font_size);
let mut shift_up = Abs::zero();
let mut shift_down = Abs::zero();
let is_text_like = base.is_text_like();
if tl.is_some() || tr.is_some() {
let ascent = base.base_ascent();
shift_up = shift_up
.max(sup_shift_up)
.max(if is_text_like { Abs::zero() } else { ascent - sup_drop_max })
.max(sup_bottom_min + measure!(tl, descent))
.max(sup_bottom_min + measure!(tr, descent));
}
if bl.is_some() || br.is_some() {
let descent = base.base_descent();
shift_down = shift_down
.max(sub_shift_down)
.max(if is_text_like { Abs::zero() } else { descent + sub_drop_min })
.max(measure!(bl, ascent) - sub_top_max)
.max(measure!(br, ascent) - sub_top_max);
}
for (sup, sub) in [(tl, bl), (tr, br)] {
if let (Some(sup), Some(sub)) = (&sup, &sub) {
let sup_bottom = shift_up - sup.descent();
let sub_top = sub.ascent() - shift_down;
let gap = sup_bottom - sub_top;
if gap >= gap_min {
continue;
}
let increase = gap_min - gap;
let sup_only =
(sup_bottom_max_with_sub - sup_bottom).clamp(Abs::zero(), increase);
let rest = (increase - sup_only) / 2.0;
shift_up += sup_only + rest;
shift_down += rest;
}
}
(shift_up, shift_down)
}
/// Calculate the kerning value for a script with respect to the base. A
/// positive value means shifting the script further away from the base, whereas
/// a negative value means shifting the script closer to the base. Requires the
/// distance from the base's baseline to the script's baseline, as well as the
/// script's corner (tl, tr, bl, br).
fn math_kern(base: &MathFragment, script: &MathFragment, shift: Abs, pos: Corner) -> Abs {
// This process is described under the MathKernInfo table in the OpenType
// MATH spec.
let (corr_height_top, corr_height_bot) = match pos {
// Calculate two correction heights for superscripts:
// - The distance from the superscript's baseline to the top of the
// base's bounding box.
// - The distance from the base's baseline to the bottom of the
// superscript's bounding box.
Corner::TopLeft | Corner::TopRight => {
(base.ascent() - shift, shift - script.descent())
}
// Calculate two correction heights for subscripts:
// - The distance from the base's baseline to the top of the
// subscript's bounding box.
// - The distance from the subscript's baseline to the bottom of the
// base's bounding box.
Corner::BottomLeft | Corner::BottomRight => {
(script.ascent() - shift, shift - base.descent())
}
};
// Calculate the sum of kerning values for each correction height.
let summed_kern = |height| {
let base_kern = base.kern_at_height(pos, height);
let attach_kern = script.kern_at_height(pos.inv(), height);
base_kern + attach_kern
};
// Take the smaller kerning amount (and so the larger value). Note that
// there is a bug in the spec (as of 2024-08-15): it says to take the
// minimum of the two sums, but as the kerning value is usually negative it
// really means the smaller kern. The current wording of the spec could
// result in glyphs colliding.
summed_kern(corr_height_top).max(summed_kern(corr_height_bot))
}