From 0d0481ce5e494e6476b1d2078abeb096c18f8855 Mon Sep 17 00:00:00 2001 From: mkorje Date: Mon, 2 Jun 2025 18:57:26 +1000 Subject: [PATCH] Support multiple fonts in math --- crates/typst-layout/src/math/accent.rs | 20 ++-- crates/typst-layout/src/math/attach.rs | 53 ++++----- crates/typst-layout/src/math/frac.rs | 29 ++--- crates/typst-layout/src/math/fragment.rs | 86 +++++++++----- crates/typst-layout/src/math/lr.rs | 7 +- crates/typst-layout/src/math/mat.rs | 15 +-- crates/typst-layout/src/math/mod.rs | 82 +++++--------- crates/typst-layout/src/math/root.rs | 57 +++++----- crates/typst-layout/src/math/shared.rs | 131 +++++++++++++++------- crates/typst-layout/src/math/text.rs | 55 ++++----- crates/typst-layout/src/math/underover.rs | 19 ++-- crates/typst-library/src/math/style.rs | 5 +- crates/typst-library/src/text/mod.rs | 1 + tests/ref/issue-2214-baseline-math.png | Bin 868 -> 884 bytes tests/ref/math-font-fallback.png | Bin 402 -> 492 bytes tests/ref/math-lr-scripts.png | Bin 611 -> 607 bytes tests/ref/math-nested-normal-layout.png | Bin 1312 -> 1077 bytes tests/suite/math/attach.typ | 2 +- tests/suite/math/mat.typ | 9 +- tests/suite/math/text.typ | 2 +- 20 files changed, 305 insertions(+), 268 deletions(-) diff --git a/crates/typst-layout/src/math/accent.rs b/crates/typst-layout/src/math/accent.rs index bd404c4dd..3ac8a7a1a 100644 --- a/crates/typst-layout/src/math/accent.rs +++ b/crates/typst-layout/src/math/accent.rs @@ -4,8 +4,7 @@ use typst_library::layout::{Em, Frame, Point, Size}; use typst_library::math::{Accent, AccentElem}; use super::{ - style_cramped, style_dtls, style_flac, FrameFragment, GlyphFragment, MathContext, - MathFragment, + style_cramped, style_dtls, style_flac, FrameFragment, MathContext, MathFragment, }; /// How much the accent can be shorter than the base. @@ -31,14 +30,16 @@ pub fn layout_accent( let width = elem.size(styles).relative_to(base.width()); - // Try to replace the accent glyph with its flattened variant. - let flattened_base_height = scaled!(ctx, styles, flattened_accent_base_height); - let flac = style_flac(); - let accent_styles = - if base.ascent() > flattened_base_height { styles.chain(&flac) } else { styles }; - let Accent(c) = elem.accent; - let mut glyph = GlyphFragment::new(ctx.font, accent_styles, c, elem.span()); + let mut glyph = ctx.layout_into_glyph(c, elem.span(), styles)?; + let flattened_base_height = value!(glyph.text, flattened_accent_base_height); + let accent_base_height = value!(glyph.text, accent_base_height); + + // Try to replace the accent glyph with its flattened variant. + let flac = style_flac(); + if base.ascent() > flattened_base_height { + glyph = ctx.layout_into_glyph(c, elem.span(), styles.chain(&flac))?; + } // Forcing the accent to be at least as large as the base makes it too // wide in many case. @@ -51,7 +52,6 @@ pub fn layout_accent( // baseline. Therefore, the default gap is the accent's negated descent // minus the accent base height. Only if the base is very small, we need // a larger gap so that the accent doesn't move too low. - let accent_base_height = scaled!(ctx, styles, accent_base_height); let gap = -accent.descent() - base.ascent().min(accent_base_height); let size = Size::new(base.width(), accent.height() + gap + base.height()); let accent_pos = Point::with_x(base_attach - accent_attach); diff --git a/crates/typst-layout/src/math/attach.rs b/crates/typst-layout/src/math/attach.rs index 84480264e..9dd7ea6ec 100644 --- a/crates/typst-layout/src/math/attach.rs +++ b/crates/typst-layout/src/math/attach.rs @@ -1,14 +1,16 @@ use typst_library::diag::SourceResult; -use typst_library::foundations::{Packed, StyleChain, SymbolElem}; +use typst_library::foundations::{Packed, StyleChain}; use typst_library::layout::{Abs, Axis, Corner, Frame, Point, Rel, Size}; use typst_library::math::{ AttachElem, EquationElem, LimitsElem, PrimesElem, ScriptsElem, StretchElem, }; +use typst_library::text::Font; +use typst_syntax::Span; use typst_utils::OptionExt; use super::{ - stretch_fragment, style_for_subscript, style_for_superscript, FrameFragment, Limits, - MathContext, MathFragment, + find_math_font, stretch_fragment, style_for_subscript, style_for_superscript, + FrameFragment, Limits, MathContext, MathFragment, }; macro_rules! measure { @@ -102,14 +104,12 @@ pub fn layout_primes( 4 => '⁗', _ => unreachable!(), }; - let f = ctx.layout_into_fragment(&SymbolElem::packed(c), styles)?; + let f = ctx.layout_into_glyph(c, elem.span(), styles)?; ctx.push(f); } count => { // Custom amount of primes - let prime = ctx - .layout_into_fragment(&SymbolElem::packed('′'), styles)? - .into_frame(); + let prime = ctx.layout_into_glyph('′', elem.span(), 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()); @@ -173,18 +173,21 @@ fn layout_attachments( ) -> SourceResult<()> { let base_class = base.class(); + // TODO: should use base's font. + let font = find_math_font(ctx.engine, styles, Span::detached())?; + // 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]) + compute_script_shifts(&font, 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()]); + compute_limit_shifts(&font, styles, &base, [t.as_ref(), b.as_ref()]); // Calculate the final frame height. let ascent = base @@ -214,7 +217,7 @@ fn layout_attachments( // `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); + let space_after_script = constant!(font, styles, space_after_script); // Calculate the distance each pre-script extends to the left of the base's // width. @@ -363,7 +366,7 @@ fn compute_limit_widths( /// 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, + font: &Font, styles: StyleChain, base: &MathFragment, [t, b]: [Option<&MathFragment>; 2], @@ -374,14 +377,14 @@ fn compute_limit_shifts( // 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); + let upper_gap_min = constant!(font, styles, upper_limit_gap_min); + let upper_rise_min = constant!(font, 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); + let lower_gap_min = constant!(font, styles, lower_limit_gap_min); + let lower_drop_min = constant!(font, styles, lower_limit_baseline_drop_min); base.descent() + lower_drop_min.max(lower_gap_min + b.ascent()) }); @@ -392,25 +395,25 @@ fn compute_limit_shifts( /// 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, + font: &Font, styles: StyleChain, base: &MathFragment, [tl, tr, bl, br]: [&Option; 4], ) -> (Abs, Abs) { let sup_shift_up = if EquationElem::cramped_in(styles) { - scaled!(ctx, styles, superscript_shift_up_cramped) + constant!(font, styles, superscript_shift_up_cramped) } else { - scaled!(ctx, styles, superscript_shift_up) + constant!(font, styles, superscript_shift_up) }; - let sup_bottom_min = scaled!(ctx, styles, superscript_bottom_min); + let sup_bottom_min = constant!(font, 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); + constant!(font, styles, superscript_bottom_max_with_subscript); + let sup_drop_max = constant!(font, styles, superscript_baseline_drop_max); + let gap_min = constant!(font, styles, sub_superscript_gap_min); + let sub_shift_down = constant!(font, styles, subscript_shift_down); + let sub_top_max = constant!(font, styles, subscript_top_max); + let sub_drop_min = constant!(font, styles, subscript_baseline_drop_min); let mut shift_up = Abs::zero(); let mut shift_down = Abs::zero(); diff --git a/crates/typst-layout/src/math/frac.rs b/crates/typst-layout/src/math/frac.rs index 4534a4ba1..8aebe651b 100644 --- a/crates/typst-layout/src/math/frac.rs +++ b/crates/typst-layout/src/math/frac.rs @@ -7,7 +7,7 @@ use typst_library::visualize::{FixedStroke, Geometry}; use typst_syntax::Span; use super::{ - style_for_denominator, style_for_numerator, FrameFragment, GlyphFragment, + find_math_font, style_for_denominator, style_for_numerator, FrameFragment, MathContext, DELIM_SHORT_FALL, }; @@ -49,29 +49,30 @@ fn layout_frac_like( binom: bool, span: Span, ) -> SourceResult<()> { - let short_fall = DELIM_SHORT_FALL.resolve(styles); - let axis = scaled!(ctx, styles, axis_height); - let thickness = scaled!(ctx, styles, fraction_rule_thickness); - let shift_up = scaled!( - ctx, styles, + let font = find_math_font(ctx.engine, styles, span)?; + let axis = constant!(font, styles, axis_height); + let thickness = constant!(font, styles, fraction_rule_thickness); + let shift_up = constant!( + font, styles, text: fraction_numerator_shift_up, display: fraction_numerator_display_style_shift_up, ); - let shift_down = scaled!( - ctx, styles, + let shift_down = constant!( + font, styles, text: fraction_denominator_shift_down, display: fraction_denominator_display_style_shift_down, ); - let num_min = scaled!( - ctx, styles, + let num_min = constant!( + font, styles, text: fraction_numerator_gap_min, display: fraction_num_display_style_gap_min, ); - let denom_min = scaled!( - ctx, styles, + let denom_min = constant!( + font, styles, text: fraction_denominator_gap_min, display: fraction_denom_display_style_gap_min, ); + let short_fall = DELIM_SHORT_FALL.resolve(styles); let num_style = style_for_numerator(styles); let num = ctx.layout_into_frame(num, styles.chain(&num_style))?; @@ -109,12 +110,12 @@ fn layout_frac_like( frame.push_frame(denom_pos, denom); if binom { - let mut left = GlyphFragment::new(ctx.font, styles, '(', span); + let mut left = ctx.layout_into_glyph('(', span, styles)?; left.stretch_vertical(ctx, height, short_fall); left.center_on_axis(); ctx.push(left); ctx.push(FrameFragment::new(styles, frame)); - let mut right = GlyphFragment::new(ctx.font, styles, ')', span); + let mut right = ctx.layout_into_glyph(')', span, styles)?; right.stretch_vertical(ctx, height, short_fall); right.center_on_axis(); ctx.push(right); diff --git a/crates/typst-layout/src/math/fragment.rs b/crates/typst-layout/src/math/fragment.rs index 738ffdc35..d6f835bba 100644 --- a/crates/typst-layout/src/math/fragment.rs +++ b/crates/typst-layout/src/math/fragment.rs @@ -3,14 +3,17 @@ use std::fmt::{self, Debug, Formatter}; use rustybuzz::{BufferFlags, UnicodeBuffer}; use ttf_parser::math::{GlyphAssembly, GlyphConstruction, GlyphPart}; use ttf_parser::GlyphId; -use typst_library::diag::warning; +use typst_library::diag::{bail, warning, SourceResult}; use typst_library::foundations::StyleChain; use typst_library::introspection::Tag; use typst_library::layout::{ Abs, Axes, Axis, Corner, Em, Frame, FrameItem, Point, Size, VAlignment, }; use typst_library::math::{EquationElem, MathSize}; -use typst_library::text::{features, language, Font, Glyph, TextElem, TextItem}; +use typst_library::text::{ + families, features, language, variant, Font, Glyph, TextElem, TextItem, +}; +use typst_library::World; use typst_syntax::Span; use typst_utils::{default_math_class, Get}; use unicode_math_class::MathClass; @@ -258,18 +261,51 @@ pub struct GlyphFragment { } impl GlyphFragment { - pub fn new(font: &Font, styles: StyleChain, c: char, span: Span) -> Self { - Self::try_new(font, styles, c.encode_utf8(&mut [0; 4]), span).unwrap() - } - - pub fn try_new( - font: &Font, + pub fn new( + ctx: &MathContext, styles: StyleChain, - str: &str, + text: &str, span: Span, - ) -> Option { + ) -> SourceResult> { + let families = families(styles); + let variant = variant(styles); + let fallback = TextElem::fallback_in(styles); + let end = text.char_indices().nth(1).map(|(i, _)| i).unwrap_or(text.len()); + + // Find the next available family. + let world = ctx.engine.world; + let book = world.book(); + let mut selection = None; + for family in families { + selection = book + .select(family.as_str(), variant) + .and_then(|id| world.font(id)) + .filter(|font| { + font.ttf().tables().math.and_then(|math| math.constants).is_some() + }) + .filter(|_| family.covers().is_none_or(|cov| cov.is_match(&text[..end]))); + if selection.is_some() { + break; + } + } + + // Do font fallback if the families are exhausted and fallback is enabled. + if selection.is_none() && fallback { + selection = book + .select_fallback(None, variant, text) + .and_then(|id| world.font(id)) + .filter(|font| { + font.ttf().tables().math.and_then(|math| math.constants).is_some() + }); + } + + // Error out if no math font could be found at all. + let Some(font) = selection else { + bail!(span, "current font does not support math"); + }; + let mut buffer = UnicodeBuffer::new(); - buffer.push_str(str); + buffer.push_str(text); buffer.set_language(language(styles)); // TODO: Bug in rustybuzz? rustybuzz::script::SCRIPT_MATH does not work. // i.e. ssty is not applied @@ -282,7 +318,7 @@ impl GlyphFragment { let features = features(styles); let plan = create_shape_plan( - font, + &font, buffer.direction(), buffer.script(), buffer.language().as_ref(), @@ -290,20 +326,18 @@ impl GlyphFragment { ); let buffer = rustybuzz::shape_with_plan(font.rusty(), &plan, buffer); - if buffer.len() != 1 { - return None; + + match buffer.len() { + 0 => return Ok(None), + 1 => {} + _ => unreachable!(), } let info = buffer.glyph_infos()[0]; let pos = buffer.glyph_positions()[0]; - // TODO: add support for coverage and fallback, like in normal text shaping. - if info.glyph_id == 0 { - return None; - } - let cluster = info.cluster as usize; - let c = str[cluster..].chars().next().unwrap(); + let c = text[cluster..].chars().next().unwrap(); let limits = Limits::for_char(c); let class = EquationElem::class_in(styles) .or_else(|| default_math_class(c)) @@ -316,14 +350,14 @@ impl GlyphFragment { stroke: TextElem::stroke_in(styles).map(|s| s.unwrap_or_default()), lang: TextElem::lang_in(styles), region: TextElem::region_in(styles), - text: str.into(), + text: text.into(), glyphs: vec![Glyph { id: info.glyph_id as u16, x_advance: font.to_em(pos.x_advance), x_offset: Em::zero(), y_advance: Em::zero(), y_offset: Em::zero(), - range: 0..str.len() as u16, + range: 0..text.len() as u16, span: (span, 0), }], }; @@ -348,7 +382,7 @@ impl GlyphFragment { modifiers: FrameModifiers::get_in(styles), }; fragment.update_glyph(); - Some(fragment) + Ok(Some(fragment)) } /// Sets element id and boxes in appropriate way without changing other @@ -505,7 +539,7 @@ impl GlyphFragment { /// to the given alignment on the axis. pub fn align_on_axis(&mut self, align: VAlignment) { let h = self.size.y; - let axis = axis_height(&self.text.font).unwrap().at(self.text.size); + let axis = value!(self.text, axis_height); self.align += self.baseline(); self.baseline = Some(align.inv().position(h + axis * 2.0)); self.align -= self.baseline(); @@ -641,10 +675,6 @@ fn kern_at_height(font: &Font, id: GlyphId, corner: Corner, height: Em) -> Optio Some(font.to_em(kern.kern(i)?.value)) } -fn axis_height(font: &Font) -> Option { - Some(font.to_em(font.ttf().tables().math?.constants?.axis_height().value)) -} - pub fn stretch_axes(font: &Font, id: GlyphId) -> Axes { let horizontal = font .ttf() diff --git a/crates/typst-layout/src/math/lr.rs b/crates/typst-layout/src/math/lr.rs index e0caf4179..544193fca 100644 --- a/crates/typst-layout/src/math/lr.rs +++ b/crates/typst-layout/src/math/lr.rs @@ -5,7 +5,9 @@ use typst_library::math::{EquationElem, LrElem, MidElem}; use typst_utils::SliceExt; use unicode_math_class::MathClass; -use super::{stretch_fragment, MathContext, MathFragment, DELIM_SHORT_FALL}; +use super::{ + find_math_font, stretch_fragment, MathContext, MathFragment, DELIM_SHORT_FALL, +}; /// Lays out an [`LrElem`]. #[typst_macros::time(name = "math.lr", span = elem.span())] @@ -33,7 +35,8 @@ pub fn layout_lr( let (start_idx, end_idx) = fragments.split_prefix_suffix(|f| f.is_ignorant()); let inner_fragments = &mut fragments[start_idx..end_idx]; - let axis = scaled!(ctx, styles, axis_height); + let font = find_math_font(ctx.engine, styles, elem.span())?; + let axis = constant!(font, styles, axis_height); let max_extent = inner_fragments .iter() .map(|fragment| (fragment.ascent() - axis).max(fragment.descent() + axis)) diff --git a/crates/typst-layout/src/math/mat.rs b/crates/typst-layout/src/math/mat.rs index 325ffbe7a..5f3aa0570 100644 --- a/crates/typst-layout/src/math/mat.rs +++ b/crates/typst-layout/src/math/mat.rs @@ -1,5 +1,5 @@ use typst_library::diag::{bail, warning, SourceResult}; -use typst_library::foundations::{Content, Packed, Resolve, StyleChain}; +use typst_library::foundations::{Content, Packed, Resolve, StyleChain, SymbolElem}; use typst_library::layout::{ Abs, Axes, Em, FixedAlignment, Frame, FrameItem, Point, Ratio, Rel, Size, }; @@ -9,8 +9,8 @@ use typst_library::visualize::{FillRule, FixedStroke, Geometry, LineCap, Shape}; use typst_syntax::Span; use super::{ - alignments, delimiter_alignment, style_for_denominator, AlignmentResult, - FrameFragment, GlyphFragment, LeftRightAlternator, MathContext, DELIM_SHORT_FALL, + alignments, delimiter_alignment, find_math_font, style_for_denominator, + AlignmentResult, FrameFragment, LeftRightAlternator, MathContext, DELIM_SHORT_FALL, }; const VERTICAL_PADDING: Ratio = Ratio::new(0.1); @@ -184,7 +184,7 @@ fn layout_body( // to ensure that normal matrices are aligned with others unless they are // way too big. let paren = - GlyphFragment::new(ctx.font, styles.chain(&denom_style), '(', Span::detached()); + ctx.layout_into_fragment(&SymbolElem::packed('('), styles.chain(&denom_style))?; for (column, col) in columns.iter().zip(&mut cols) { for (cell, (ascent, descent)) in column.iter().zip(&mut heights) { @@ -307,13 +307,14 @@ fn layout_delimiters( span: Span, ) -> SourceResult<()> { let short_fall = DELIM_SHORT_FALL.resolve(styles); - let axis = scaled!(ctx, styles, axis_height); + let font = find_math_font(ctx.engine, styles, span)?; + let axis = constant!(font, styles, axis_height); let height = frame.height(); let target = height + VERTICAL_PADDING.of(height); frame.set_baseline(height / 2.0 + axis); if let Some(left_c) = left { - let mut left = GlyphFragment::new(ctx.font, styles, left_c, span); + let mut left = ctx.layout_into_glyph(left_c, span, styles)?; left.stretch_vertical(ctx, target, short_fall); left.align_on_axis(delimiter_alignment(left_c)); ctx.push(left); @@ -322,7 +323,7 @@ fn layout_delimiters( ctx.push(FrameFragment::new(styles, frame)); if let Some(right_c) = right { - let mut right = GlyphFragment::new(ctx.font, styles, right_c, span); + let mut right = ctx.layout_into_glyph(right_c, span, styles)?; right.stretch_vertical(ctx, target, short_fall); right.align_on_axis(delimiter_alignment(right_c)); ctx.push(right); diff --git a/crates/typst-layout/src/math/mod.rs b/crates/typst-layout/src/math/mod.rs index 5fd22e578..9ca02c292 100644 --- a/crates/typst-layout/src/math/mod.rs +++ b/crates/typst-layout/src/math/mod.rs @@ -13,7 +13,7 @@ mod stretch; mod text; mod underover; -use typst_library::diag::{bail, SourceResult}; +use typst_library::diag::SourceResult; use typst_library::engine::Engine; use typst_library::foundations::{ Content, NativeElement, Packed, Resolve, StyleChain, SymbolElem, @@ -27,10 +27,7 @@ use typst_library::layout::{ use typst_library::math::*; use typst_library::model::ParElem; use typst_library::routines::{Arenas, RealizationKind}; -use typst_library::text::{ - families, variant, Font, LinebreakElem, SpaceElem, TextEdgeBounds, TextElem, -}; -use typst_library::World; +use typst_library::text::{LinebreakElem, SpaceElem, TextEdgeBounds, TextElem}; use typst_syntax::Span; use typst_utils::Numeric; use unicode_math_class::MathClass; @@ -53,12 +50,11 @@ pub fn layout_equation_inline( ) -> SourceResult> { assert!(!elem.block(styles)); - let font = find_math_font(engine, styles, elem.span())?; - let mut locator = locator.split(); - let mut ctx = MathContext::new(engine, &mut locator, region, &font); + let mut ctx = MathContext::new(engine, &mut locator, region); - let scale_style = style_for_script_scale(&ctx); + let font = find_math_font(ctx.engine, styles, elem.span())?; + let scale_style = style_for_script_scale(&font); let styles = styles.chain(&scale_style); let run = ctx.layout_into_run(&elem.body, styles)?; @@ -108,12 +104,12 @@ pub fn layout_equation_block( assert!(elem.block(styles)); let span = elem.span(); - let font = find_math_font(engine, styles, span)?; let mut locator = locator.split(); - let mut ctx = MathContext::new(engine, &mut locator, regions.base(), &font); + let mut ctx = MathContext::new(engine, &mut locator, regions.base()); - let scale_style = style_for_script_scale(&ctx); + let font = find_math_font(ctx.engine, styles, elem.span())?; + let scale_style = style_for_script_scale(&font); let styles = styles.chain(&scale_style); let full_equation_builder = ctx @@ -234,24 +230,6 @@ pub fn layout_equation_block( Ok(Fragment::frames(frames)) } -fn find_math_font( - engine: &mut Engine<'_>, - styles: StyleChain, - span: Span, -) -> SourceResult { - let variant = variant(styles); - let world = engine.world; - let Some(font) = families(styles).find_map(|family| { - let id = world.book().select(family.as_str(), variant)?; - let font = world.font(id)?; - let _ = font.ttf().tables().math?.constants?; - Some(font) - }) else { - bail!(span, "current font does not support math"); - }; - Ok(font) -} - fn add_equation_number( equation_builder: MathRunFrameBuilder, number: Frame, @@ -370,9 +348,6 @@ struct MathContext<'a, 'v, 'e> { engine: &'v mut Engine<'e>, locator: &'v mut SplitLocator<'a>, region: Region, - // Font-related. - font: &'a Font, - constants: ttf_parser::math::Constants<'a>, // Mutable. fragments: Vec, } @@ -383,19 +358,11 @@ impl<'a, 'v, 'e> MathContext<'a, 'v, 'e> { engine: &'v mut Engine<'e>, locator: &'v mut SplitLocator<'a>, base: Size, - font: &'a Font, ) -> Self { - // These unwraps are safe as the font given is one returned by the - // find_math_font function, which only returns fonts that have a math - // constants table. - let constants = font.ttf().tables().math.unwrap().constants.unwrap(); - Self { engine, locator, region: Region::new(base, Axes::splat(false)), - font, - constants, fragments: vec![], } } @@ -434,6 +401,23 @@ impl<'a, 'v, 'e> MathContext<'a, 'v, 'e> { Ok(std::mem::replace(&mut self.fragments, prev)) } + fn layout_into_glyph( + &mut self, + c: char, + span: Span, + styles: StyleChain, + ) -> SourceResult { + let prev = std::mem::take(&mut self.fragments); + let elem = SymbolElem::packed(c).spanned(span); + self.layout_into_self(&elem, styles)?; + let MathFragment::Glyph(glyph) = + std::mem::replace(&mut self.fragments, prev).remove(0) + else { + unreachable!() + }; + Ok(glyph) + } + /// Layout the given element and return the result as a /// unified [`MathFragment`]. fn layout_into_fragment( @@ -469,15 +453,7 @@ impl<'a, 'v, 'e> MathContext<'a, 'v, 'e> { styles, )?; - let outer = styles; for (elem, styles) in pairs { - // Hack because the font is fixed in math. - if styles != outer && TextElem::font_in(styles) != TextElem::font_in(outer) { - let frame = layout_external(elem, self, styles)?; - self.push(FrameFragment::new(styles, frame).with_spaced(true)); - continue; - } - layout_realized(elem, self, styles)?; } @@ -494,8 +470,9 @@ fn layout_realized( if let Some(elem) = elem.to_packed::() { ctx.push(MathFragment::Tag(elem.tag.clone())); } else if elem.is::() { - let space_width = ctx.font.space_width().unwrap_or(THICK); - ctx.push(MathFragment::Space(space_width.resolve(styles))); + let font = find_math_font(ctx.engine, styles, elem.span())?; + let space_width = font.space_width().unwrap_or(THICK).resolve(styles); + ctx.push(MathFragment::Space(space_width)); } else if elem.is::() { ctx.push(MathFragment::Linebreak); } else if let Some(elem) = elem.to_packed::() { @@ -565,7 +542,8 @@ fn layout_realized( } else { let mut frame = layout_external(elem, ctx, styles)?; if !frame.has_baseline() { - let axis = scaled!(ctx, styles, axis_height); + let font = find_math_font(ctx.engine, styles, elem.span())?; + let axis = constant!(font, styles, axis_height); frame.set_baseline(frame.height() / 2.0 + axis); } ctx.push( diff --git a/crates/typst-layout/src/math/root.rs b/crates/typst-layout/src/math/root.rs index 07b2d5619..635d9e40b 100644 --- a/crates/typst-layout/src/math/root.rs +++ b/crates/typst-layout/src/math/root.rs @@ -2,10 +2,9 @@ use typst_library::diag::SourceResult; use typst_library::foundations::{Packed, StyleChain}; use typst_library::layout::{Abs, Frame, FrameItem, Point, Size}; use typst_library::math::{EquationElem, MathSize, RootElem}; -use typst_library::text::TextElem; use typst_library::visualize::{FixedStroke, Geometry}; -use super::{style_cramped, FrameFragment, GlyphFragment, MathContext}; +use super::{find_math_font, style_cramped, FrameFragment, MathContext}; /// Lays out a [`RootElem`]. /// @@ -17,20 +16,8 @@ pub fn layout_root( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - let index = elem.index(styles); let span = elem.span(); - let gap = scaled!( - ctx, styles, - text: radical_vertical_gap, - display: radical_display_style_vertical_gap, - ); - let thickness = scaled!(ctx, styles, radical_rule_thickness); - let extra_ascender = scaled!(ctx, styles, radical_extra_ascender); - let kern_before = scaled!(ctx, styles, radical_kern_before_degree); - let kern_after = scaled!(ctx, styles, radical_kern_after_degree); - let raise_factor = percent!(ctx, radical_degree_bottom_raise_percent); - // Layout radicand. let radicand = { let cramped = style_cramped(); @@ -39,23 +26,41 @@ pub fn layout_root( let multiline = run.is_multiline(); let mut radicand = run.into_fragment(styles).into_frame(); if multiline { + let font = find_math_font(ctx.engine, styles, elem.span())?; + let axis = constant!(font, styles, axis_height); // Align the frame center line with the math axis. - radicand.set_baseline( - radicand.height() / 2.0 + scaled!(ctx, styles, axis_height), - ); + radicand.set_baseline(radicand.height() / 2.0 + axis); } radicand }; // Layout root symbol. + let mut sqrt = ctx.layout_into_glyph('√', span, styles)?; + let gap = value!( + sqrt.text, styles, + inline: radical_vertical_gap, + display: radical_display_style_vertical_gap, + ); + let thickness = value!(sqrt.text, radical_rule_thickness); + let extra_ascender = value!(sqrt.text, radical_extra_ascender); + let kern_before = value!(sqrt.text, radical_kern_before_degree); + let kern_after = value!(sqrt.text, radical_kern_after_degree); + let raise_factor = percent!(sqrt.text, radical_degree_bottom_raise_percent); + + let line = FrameItem::Shape( + Geometry::Line(Point::with_x(radicand.width())) + .stroked(FixedStroke::from_pair(sqrt.text.fill.clone(), thickness)), + span, + ); + let target = radicand.height() + thickness + gap; - let mut sqrt = GlyphFragment::new(ctx.font, styles, '√', span); sqrt.stretch_vertical(ctx, target, Abs::zero()); let sqrt = sqrt.into_frame(); // Layout the index. let sscript = EquationElem::set_size(MathSize::ScriptScript).wrap(); - let index = index + let index = elem + .index(styles) .as_ref() .map(|elem| ctx.layout_into_frame(elem, styles.chain(&sscript))) .transpose()?; @@ -107,19 +112,7 @@ pub fn layout_root( } frame.push_frame(sqrt_pos, sqrt); - frame.push( - line_pos, - FrameItem::Shape( - Geometry::Line(Point::with_x(radicand.width())).stroked( - FixedStroke::from_pair( - TextElem::fill_in(styles).as_decoration(), - thickness, - ), - ), - span, - ), - ); - + frame.push(line_pos, line); frame.push_frame(radicand_pos, radicand); ctx.push(FrameFragment::new(styles, frame)); diff --git a/crates/typst-layout/src/math/shared.rs b/crates/typst-layout/src/math/shared.rs index d555a219a..ec35a1dc1 100644 --- a/crates/typst-layout/src/math/shared.rs +++ b/crates/typst-layout/src/math/shared.rs @@ -1,59 +1,103 @@ -use ttf_parser::math::MathValue; use ttf_parser::Tag; +use typst_library::diag::{bail, SourceResult}; +use typst_library::engine::Engine; use typst_library::foundations::{Style, StyleChain}; use typst_library::layout::{Abs, Em, FixedAlignment, Frame, Point, Size, VAlignment}; use typst_library::math::{EquationElem, MathSize}; -use typst_library::text::{FontFeatures, TextElem}; +use typst_library::text::{families, variant, Font, FontFeatures, TextElem}; +use typst_library::World; +use typst_syntax::Span; use typst_utils::LazyHash; -use super::{LeftRightAlternator, MathContext, MathFragment, MathRun}; +use super::{LeftRightAlternator, MathFragment, MathRun}; -macro_rules! scaled { - ($ctx:expr, $styles:expr, text: $text:ident, display: $display:ident $(,)?) => { - match typst_library::math::EquationElem::size_in($styles) { - typst_library::math::MathSize::Display => scaled!($ctx, $styles, $display), - _ => scaled!($ctx, $styles, $text), - } - }; - ($ctx:expr, $styles:expr, $name:ident) => { - $crate::math::Scaled::scaled( - $ctx.constants.$name(), - $ctx, - typst_library::text::TextElem::size_in($styles), - ) +macro_rules! percent { + ($text:expr, $name:ident) => { + $text + .font + .ttf() + .tables() + .math + .and_then(|math| math.constants) + .map(|constants| constants.$name()) + .unwrap() as f64 + / 100.0 }; } -macro_rules! percent { - ($ctx:expr, $name:ident) => { - $ctx.constants.$name() as f64 / 100.0 +macro_rules! word { + ($text:expr, $name:ident) => { + $text + .font + .ttf() + .tables() + .math + .and_then(|math| math.constants) + .map(|constants| $text.font.to_em(constants.$name()).at($text.size)) + .unwrap() + }; +} + +macro_rules! value { + ($text:expr, $styles:expr, inline: $inline:ident, display: $display:ident $(,)?) => { + match typst_library::math::EquationElem::size_in($styles) { + typst_library::math::MathSize::Display => value!($text, $display), + _ => value!($text, $inline), + } + }; + ($text:expr, $name:ident) => { + $text + .font + .ttf() + .tables() + .math + .and_then(|math| math.constants) + .map(|constants| $text.font.to_em(constants.$name().value).at($text.size)) + .unwrap() + }; +} + +macro_rules! constant { + ($font:expr, $styles:expr, text: $text:ident, display: $display:ident $(,)?) => { + match typst_library::math::EquationElem::size_in($styles) { + typst_library::math::MathSize::Display => constant!($font, $styles, $display), + _ => constant!($font, $styles, $text), + } + }; + ($font:expr, $styles:expr, $name:ident) => { + typst_library::foundations::Resolve::resolve( + $font + .ttf() + .tables() + .math + .and_then(|math| math.constants) + .map(|constants| $font.to_em(constants.$name().value)) + .unwrap(), + $styles, + ) }; } /// How much less high scaled delimiters can be than what they wrap. pub const DELIM_SHORT_FALL: Em = Em::new(0.1); -/// Converts some unit to an absolute length with the current font & font size. -pub trait Scaled { - fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs; -} - -impl Scaled for i16 { - fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs { - ctx.font.to_em(self).at(font_size) - } -} - -impl Scaled for u16 { - fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs { - ctx.font.to_em(self).at(font_size) - } -} - -impl Scaled for MathValue<'_> { - fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs { - self.value.scaled(ctx, font_size) - } +pub fn find_math_font( + engine: &mut Engine<'_>, + styles: StyleChain, + span: Span, +) -> SourceResult { + let variant = variant(styles); + let world = engine.world; + let Some(font) = families(styles).find_map(|family| { + let id = world.book().select(family.as_str(), variant)?; + let font = world.font(id)?; + let _ = font.ttf().tables().math?.constants?; + // Take the base font as the "main" math font. + family.covers().map_or(Some(font), |_| None) + }) else { + bail!(span, "current font does not support math"); + }; + Ok(font) } /// Styles something as cramped. @@ -99,10 +143,11 @@ pub fn style_for_denominator(styles: StyleChain) -> [LazyHash