diff options
Diffstat (limited to 'crates/typst-layout/src/math/attach.rs')
| -rw-r--r-- | crates/typst-layout/src/math/attach.rs | 515 |
1 files changed, 515 insertions, 0 deletions
diff --git a/crates/typst-layout/src/math/attach.rs b/crates/typst-layout/src/math/attach.rs new file mode 100644 index 00000000..0f9090f7 --- /dev/null +++ b/crates/typst-layout/src/math/attach.rs @@ -0,0 +1,515 @@ +use typst_library::diag::SourceResult; +use typst_library::foundations::{Packed, Smart, StyleChain}; +use typst_library::layout::{Abs, Axis, Corner, Frame, Length, Point, Rel, Size}; +use typst_library::math::{ + AttachElem, EquationElem, LimitsElem, PrimesElem, ScriptsElem, StretchElem, +}; +use typst_library::text::TextElem; +use typst_utils::OptionExt; + +use super::{ + stretch_fragment, style_for_subscript, style_for_superscript, FrameFragment, Limits, + MathContext, MathFragment, +}; + +macro_rules! measure { + ($e: ident, $attr: ident) => { + $e.as_ref().map(|e| e.$attr()).unwrap_or_default() + }; +} + +/// Lays out an [`AttachElem`]. +#[typst_macros::time(name = "math.attach", span = elem.span())] +pub fn layout_attach( + elem: &Packed<AttachElem>, + ctx: &mut MathContext, + styles: StyleChain, +) -> SourceResult<()> { + let merged = elem.merge_base(); + let elem = merged.as_ref().unwrap_or(elem); + let stretch = stretch_size(styles, elem); + + let mut base = ctx.layout_into_fragment(elem.base(), styles)?; + let sup_style = style_for_superscript(styles); + let sup_style_chain = styles.chain(&sup_style); + let tl = elem.tl(sup_style_chain); + let tr = elem.tr(sup_style_chain); + let primed = tr.as_ref().is_some_and(|content| content.is::<PrimesElem>()); + let t = elem.t(sup_style_chain); + + let sub_style = style_for_subscript(styles); + let sub_style_chain = styles.chain(&sub_style); + let bl = elem.bl(sub_style_chain); + let br = elem.br(sub_style_chain); + let b = elem.b(sub_style_chain); + + let limits = base.limits().active(styles); + let (t, tr) = match (t, tr) { + (Some(t), Some(tr)) if primed && !limits => (None, Some(tr + t)), + (Some(t), None) if !limits => (None, Some(t)), + (t, tr) => (t, tr), + }; + let (b, br) = if limits || br.is_some() { (b, br) } else { (None, b) }; + + macro_rules! layout { + ($content:ident, $style_chain:ident) => { + $content + .map(|elem| ctx.layout_into_fragment(&elem, $style_chain)) + .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!(t, sup_style_chain)?; + let b = layout!(b, sub_style_chain)?; + if let Some(stretch) = stretch { + let relative_to_width = measure!(t, width).max(measure!(b, width)); + stretch_fragment( + ctx, + styles, + &mut base, + Some(Axis::X), + Some(relative_to_width), + stretch, + Abs::zero(), + ); + } + + let fragments = [ + layout!(tl, sup_style_chain)?, + t, + layout!(tr, sup_style_chain)?, + layout!(bl, sub_style_chain)?, + b, + layout!(br, sub_style_chain)?, + ]; + + layout_attachments(ctx, styles, base, fragments) +} + +/// Lays out a [`PrimeElem`]. +#[typst_macros::time(name = "math.primes", span = elem.span())] +pub fn layout_primes( + elem: &Packed<PrimesElem>, + ctx: &mut MathContext, + styles: StyleChain, +) -> SourceResult<()> { + match *elem.count() { + count @ 1..=4 => { + let c = match count { + 1 => '′', + 2 => '″', + 3 => '‴', + 4 => '⁗', + _ => unreachable!(), + }; + let f = ctx.layout_into_fragment(&TextElem::packed(c), styles)?; + ctx.push(f); + } + count => { + // Custom amount of primes + let prime = + ctx.layout_into_fragment(&TextElem::packed('′'), styles)?.into_frame(); + let width = prime.width() * (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..count { + frame.push_frame( + Point::new(prime.width() * (i as f64 / 2.0), Abs::zero()), + prime.clone(), + ) + } + ctx.push(FrameFragment::new(ctx, styles, frame).with_text_like(true)); + } + } + Ok(()) +} + +/// Lays out a [`ScriptsElem`]. +#[typst_macros::time(name = "math.scripts", span = elem.span())] +pub fn layout_scripts( + elem: &Packed<ScriptsElem>, + ctx: &mut MathContext, + styles: StyleChain, +) -> SourceResult<()> { + let mut fragment = ctx.layout_into_fragment(elem.body(), styles)?; + fragment.set_limits(Limits::Never); + ctx.push(fragment); + Ok(()) +} + +/// Lays out a [`LimitsElem`]. +#[typst_macros::time(name = "math.limits", span = elem.span())] +pub fn layout_limits( + elem: &Packed<LimitsElem>, + ctx: &mut MathContext, + styles: StyleChain, +) -> SourceResult<()> { + let limits = if elem.inline(styles) { Limits::Always } else { Limits::Display }; + let mut fragment = ctx.layout_into_fragment(elem.body(), styles)?; + fragment.set_limits(limits); + ctx.push(fragment); + Ok(()) +} + +/// Get the size to stretch the base to, if the attach argument is true. +fn stretch_size( + styles: StyleChain, + elem: &Packed<AttachElem>, +) -> Option<Smart<Rel<Length>>> { + // Extract from an EquationElem. + let mut base = elem.base(); + if let Some(equation) = base.to_packed::<EquationElem>() { + base = equation.body(); + } + + base.to_packed::<StretchElem>().map(|stretch| stretch.size(styles)) +} + +/// Lay out the attachments. +fn layout_attachments( + ctx: &mut MathContext, + styles: StyleChain, + base: MathFragment, + [tl, t, tr, bl, b, br]: [Option<MathFragment>; 6], +) -> SourceResult<()> { + let base_class = base.class(); + + // 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(ctx, styles, &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(ctx, styles, &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 = scaled!(ctx, styles, space_after_script); + + // 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( + ctx, + &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( + ctx, + &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! Note that we retain the class of the base. + ctx.push(FrameFragment::new(ctx, styles, frame).with_class(base_class)); + + 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( + ctx: &MathContext, + 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(ctx, 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(ctx, 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( + ctx: &MathContext, + 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(ctx, 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(ctx, 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( + ctx: &MathContext, + styles: StyleChain, + 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 = scaled!(ctx, styles, upper_limit_gap_min); + let upper_rise_min = scaled!(ctx, styles, upper_limit_baseline_rise_min); + base.ascent() + upper_rise_min.max(upper_gap_min + t.descent()) + }); + + let b_shift = b.map_or_default(|b| { + let lower_gap_min = scaled!(ctx, styles, lower_limit_gap_min); + let lower_drop_min = scaled!(ctx, styles, lower_limit_baseline_drop_min); + 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( + ctx: &MathContext, + styles: StyleChain, + base: &MathFragment, + [tl, tr, bl, br]: [&Option<MathFragment>; 4], +) -> (Abs, Abs) { + let sup_shift_up = if EquationElem::cramped_in(styles) { + scaled!(ctx, styles, superscript_shift_up_cramped) + } else { + scaled!(ctx, styles, superscript_shift_up) + }; + + let sup_bottom_min = scaled!(ctx, styles, superscript_bottom_min); + let sup_bottom_max_with_sub = + scaled!(ctx, styles, superscript_bottom_max_with_subscript); + let sup_drop_max = scaled!(ctx, styles, superscript_baseline_drop_max); + let gap_min = scaled!(ctx, styles, sub_superscript_gap_min); + let sub_shift_down = scaled!(ctx, styles, subscript_shift_down); + let sub_top_max = scaled!(ctx, styles, subscript_top_max); + let sub_drop_min = scaled!(ctx, styles, subscript_baseline_drop_min); + + 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 = match &base { + MathFragment::Frame(frame) => frame.base_ascent, + _ => 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() { + shift_down = shift_down + .max(sub_shift_down) + .max(if is_text_like { Abs::zero() } else { base.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( + ctx: &MathContext, + 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(ctx, pos, height); + let attach_kern = script.kern_at_height(ctx, 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)) +} |
