diff --git a/Cargo.lock b/Cargo.lock index 1893f89fe..9ee869ba2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2911,7 +2911,7 @@ dependencies = [ [[package]] name = "typst-dev-assets" version = "0.13.1" -source = "git+https://github.com/typst/typst-dev-assets?rev=bfa947f#bfa947f3433d7d13a995168c40ae788a2ebfe648" +source = "git+https://github.com/typst/typst-dev-assets?rev=64f8c71#64f8c7108db88323a9d3476e9750562de753f24e" [[package]] name = "typst-docs" diff --git a/Cargo.toml b/Cargo.toml index 9657f207f..6b1fb4863 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,7 @@ typst-syntax = { path = "crates/typst-syntax", version = "0.13.1" } typst-timing = { path = "crates/typst-timing", version = "0.13.1" } typst-utils = { path = "crates/typst-utils", version = "0.13.1" } typst-assets = { git = "https://github.com/typst/typst-assets", rev = "edf0d64" } -typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "bfa947f" } +typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "64f8c71" } arrayvec = "0.7.4" az = "1.2" base64 = "0.22" diff --git a/crates/typst-layout/src/math/accent.rs b/crates/typst-layout/src/math/accent.rs index e7f051ace..e098dffdf 100644 --- a/crates/typst-layout/src/math/accent.rs +++ b/crates/typst-layout/src/math/accent.rs @@ -1,11 +1,10 @@ use typst_library::diag::SourceResult; -use typst_library::foundations::{Packed, StyleChain}; +use typst_library::foundations::{Packed, StyleChain, SymbolElem}; use typst_library::layout::{Em, Frame, Point, Size}; use typst_library::math::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. @@ -27,14 +26,17 @@ pub fn layout_accent( if top_accent && elem.dotless.get(styles) { styles.chain(&dtls) } else { styles }; let cramped = style_cramped(); - let base = ctx.layout_into_fragment(&elem.base, base_styles.chain(&cramped))?; + let base_styles = base_styles.chain(&cramped); + let base = ctx.layout_into_fragment(&elem.base, base_styles)?; + + let (font, size) = base.font(ctx, base_styles, elem.base.span())?; // Preserve class to preserve automatic spacing. let base_class = base.class(); let base_attach = base.accent_attach(); // Try to replace the accent glyph with its flattened variant. - let flattened_base_height = scaled!(ctx, styles, flattened_accent_base_height); + let flattened_base_height = value!(font, flattened_accent_base_height).at(size); let flac = style_flac(); let accent_styles = if top_accent && base.ascent() > flattened_base_height { styles.chain(&flac) @@ -42,23 +44,25 @@ pub fn layout_accent( styles }; - let mut glyph = - GlyphFragment::new_char(ctx.font, accent_styles, accent.0, elem.span())?; + let mut accent = ctx.layout_into_fragment( + &SymbolElem::packed(accent.0).spanned(elem.span()), + accent_styles, + )?; // Forcing the accent to be at least as large as the base makes it too wide // in many cases. let width = elem.size.resolve(styles).relative_to(base.width()); - let short_fall = ACCENT_SHORT_FALL.at(glyph.item.size); - glyph.stretch_horizontal(ctx, width - short_fall); - let accent_attach = glyph.accent_attach.0; - let accent = glyph.into_frame(); + let short_fall = ACCENT_SHORT_FALL.at(size); + accent.stretch_horizontal(ctx, width - short_fall); + let accent_attach = accent.accent_attach().0; + let accent = accent.into_frame(); let (gap, accent_pos, base_pos) = if top_accent { // Descent is negative because the accent's ink bottom is above the // 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 accent_base_height = value!(font, accent_base_height).at(size); let gap = -accent.descent() - base.ascent().min(accent_base_height); let accent_pos = Point::with_x(base_attach.0 - accent_attach); let base_pos = Point::with_y(accent.height() + gap); diff --git a/crates/typst-layout/src/math/attach.rs b/crates/typst-layout/src/math/attach.rs index 78b6f5515..6b6560d70 100644 --- a/crates/typst-layout/src/math/attach.rs +++ b/crates/typst-layout/src/math/attach.rs @@ -4,6 +4,8 @@ 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::{ @@ -83,7 +85,7 @@ pub fn layout_attach( layout!(br, sub_style_chain)?, ]; - layout_attachments(ctx, styles, base, fragments) + layout_attachments(ctx, styles, base, elem.base.span(), fragments) } /// Lays out a [`PrimeElem`]. @@ -102,13 +104,19 @@ pub fn layout_primes( 4 => '⁗', _ => unreachable!(), }; - let f = ctx.layout_into_fragment(&SymbolElem::packed(c), styles)?; + let f = ctx.layout_into_fragment( + &SymbolElem::packed(c).spanned(elem.span()), + styles, + )?; ctx.push(f); } count => { // Custom amount of primes let prime = ctx - .layout_into_fragment(&SymbolElem::packed('′'), styles)? + .layout_into_fragment( + &SymbolElem::packed('′').spanned(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())); @@ -170,22 +178,25 @@ fn layout_attachments( ctx: &mut MathContext, styles: StyleChain, base: MathFragment, + span: Span, [tl, t, tr, bl, b, br]: [Option; 6], ) -> SourceResult<()> { - let base_class = base.class(); + let class = base.class(); + let (font, size) = base.font(ctx, styles, span)?; + 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(ctx, styles, &base, [&tl, &tr, &bl, &br]) + 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(ctx, styles, &base, [t.as_ref(), b.as_ref()]); + compute_limit_shifts(&font, size, &base, [t.as_ref(), b.as_ref()]); // Calculate the final frame height. let ascent = base @@ -215,7 +226,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 = value!(font, space_after_script).at(size); // Calculate the distance each pre-script extends to the left of the base's // width. @@ -272,7 +283,7 @@ fn layout_attachments( layout!(b, b_x, b_y); // lower-limit // Done! Note that we retain the class of the base. - ctx.push(FrameFragment::new(styles, frame).with_class(base_class)); + ctx.push(FrameFragment::new(styles, frame).with_class(class)); Ok(()) } @@ -364,8 +375,8 @@ 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, - styles: StyleChain, + font: &Font, + font_size: Abs, base: &MathFragment, [t, b]: [Option<&MathFragment>; 2], ) -> (Abs, Abs) { @@ -373,16 +384,15 @@ fn compute_limit_shifts( // 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); + let upper_gap_min = value!(font, upper_limit_gap_min).at(font_size); + let upper_rise_min = value!(font, 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 = scaled!(ctx, styles, lower_limit_gap_min); - let lower_drop_min = scaled!(ctx, styles, lower_limit_baseline_drop_min); + let lower_gap_min = value!(font, lower_limit_gap_min).at(font_size); + let lower_drop_min = value!(font, lower_limit_baseline_drop_min).at(font_size); base.descent() + lower_drop_min.max(lower_gap_min + b.ascent()) }); @@ -393,25 +403,27 @@ 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, - styles: StyleChain, + font: &Font, + font_size: Abs, + cramped: bool, base: &MathFragment, [tl, tr, bl, br]: [&Option; 4], ) -> (Abs, Abs) { - let sup_shift_up = if styles.get(EquationElem::cramped) { - scaled!(ctx, styles, superscript_shift_up_cramped) + let sup_shift_up = (if cramped { + value!(font, superscript_shift_up_cramped) } else { - scaled!(ctx, styles, superscript_shift_up) - }; + value!(font, superscript_shift_up) + }) + .at(font_size); - let sup_bottom_min = scaled!(ctx, styles, superscript_bottom_min); + let sup_bottom_min = value!(font, superscript_bottom_min).at(font_size); 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); + value!(font, superscript_bottom_max_with_subscript).at(font_size); + let sup_drop_max = value!(font, superscript_baseline_drop_max).at(font_size); + let gap_min = value!(font, sub_superscript_gap_min).at(font_size); + let sub_shift_down = value!(font, subscript_shift_down).at(font_size); + let sub_top_max = value!(font, subscript_top_max).at(font_size); + let sub_drop_min = value!(font, subscript_baseline_drop_min).at(font_size); 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 12a2c6fd1..2b1047f08 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,33 @@ 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.world, styles, span)?; + let axis = value!(font, axis_height).resolve(styles); + let thickness = value!(font, fraction_rule_thickness).resolve(styles); + let shift_up = value!( + font, styles, text: fraction_numerator_shift_up, display: fraction_numerator_display_style_shift_up, - ); - let shift_down = scaled!( - ctx, styles, + ) + .resolve(styles); + let shift_down = value!( + font, styles, text: fraction_denominator_shift_down, display: fraction_denominator_display_style_shift_down, - ); - let num_min = scaled!( - ctx, styles, + ) + .resolve(styles); + let num_min = value!( + font, styles, text: fraction_numerator_gap_min, display: fraction_num_display_style_gap_min, - ); - let denom_min = scaled!( - ctx, styles, + ) + .resolve(styles); + let denom_min = value!( + font, styles, text: fraction_denominator_gap_min, display: fraction_denom_display_style_gap_min, - ); + ) + .resolve(styles); let num_style = style_for_numerator(styles); let num = ctx.layout_into_frame(num, styles.chain(&num_style))?; @@ -82,7 +86,7 @@ fn layout_frac_like( // Add a comma between each element. denom .iter() - .flat_map(|a| [SymbolElem::packed(','), a.clone()]) + .flat_map(|a| [SymbolElem::packed(',').spanned(span), a.clone()]) .skip(1), ), styles.chain(&denom_style), @@ -109,12 +113,18 @@ fn layout_frac_like( frame.push_frame(denom_pos, denom); if binom { - let mut left = GlyphFragment::new_char(ctx.font, styles, '(', span)?; + let short_fall = DELIM_SHORT_FALL.resolve(styles); + + let mut left = + ctx.layout_into_fragment(&SymbolElem::packed('(').spanned(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_char(ctx.font, styles, ')', span)?; + + let mut right = + ctx.layout_into_fragment(&SymbolElem::packed(')').spanned(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 758dd401f..2f5537548 100644 --- a/crates/typst-layout/src/math/fragment.rs +++ b/crates/typst-layout/src/math/fragment.rs @@ -11,12 +11,16 @@ 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::visualize::Paint; +use typst_library::World; use typst_syntax::Span; use typst_utils::{default_math_class, Get}; use unicode_math_class::MathClass; -use super::MathContext; +use super::{find_math_font, MathContext}; use crate::inline::create_shape_plan; use crate::modifiers::{FrameModifiers, FrameModify}; @@ -108,6 +112,21 @@ impl MathFragment { } } + pub fn font( + &self, + ctx: &MathContext, + styles: StyleChain, + span: Span, + ) -> SourceResult<(Font, Abs)> { + Ok(( + match self { + Self::Glyph(glyph) => glyph.item.font.clone(), + _ => find_math_font(ctx.engine.world, styles, span)?, + }, + self.font_size().unwrap_or_else(|| styles.resolve(TextElem::size)), + )) + } + pub fn font_size(&self) -> Option { match self { Self::Glyph(glyph) => Some(glyph.item.size), @@ -192,6 +211,31 @@ impl MathFragment { } } + pub fn fill(&self) -> Option { + match self { + Self::Glyph(glyph) => Some(glyph.item.fill.clone()), + _ => None, + } + } + + pub fn stretch_vertical(&mut self, ctx: &mut MathContext, height: Abs) { + if let Self::Glyph(glyph) = self { + glyph.stretch_vertical(ctx, height) + } + } + + pub fn stretch_horizontal(&mut self, ctx: &mut MathContext, width: Abs) { + if let Self::Glyph(glyph) = self { + glyph.stretch_horizontal(ctx, width) + } + } + + pub fn center_on_axis(&mut self) { + if let Self::Glyph(glyph) = self { + glyph.center_on_axis() + } + } + /// If no kern table is provided for a corner, a kerning amount of zero is /// assumed. pub fn kern_at_height(&self, corner: Corner, height: Abs) -> Abs { @@ -261,23 +305,70 @@ pub struct GlyphFragment { impl GlyphFragment { /// Calls `new` with the given character. pub fn new_char( - font: &Font, + ctx: &MathContext, styles: StyleChain, c: char, span: Span, - ) -> SourceResult { - Self::new(font, styles, c.encode_utf8(&mut [0; 4]), span) + ) -> SourceResult> { + Self::new(ctx, styles, c.encode_utf8(&mut [0; 4]), span) + } + + /// Selects a font to use and then shapes text. + pub fn new( + ctx: &MathContext, + styles: StyleChain, + text: &str, + span: Span, + ) -> SourceResult> { + let families = families(styles); + let variant = variant(styles); + let fallback = styles.get(TextElem::fallback); + 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"); + }; + + Self::shape(&font, styles, text, span) } /// Try to create a new glyph out of the given string. Will bail if the - /// result from shaping the string is not a single glyph or is a tofu. + /// result from shaping the string is more than a single glyph. #[comemo::memoize] - pub fn new( + pub fn shape( font: &Font, styles: StyleChain, text: &str, span: Span, - ) -> SourceResult { + ) -> SourceResult> { let mut buffer = UnicodeBuffer::new(); buffer.push_str(text); buffer.set_language(language(styles)); @@ -300,18 +391,15 @@ impl GlyphFragment { ); let buffer = rustybuzz::shape_with_plan(font.rusty(), &plan, buffer); - if buffer.len() != 1 { - bail!(span, "did not get a single glyph after shaping {}", text); + match buffer.len() { + 0 => return Ok(None), + 1 => {} + _ => bail!(span, "did not get a single glyph after shaping {}", text), } 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 { - bail!(span, "current font is missing a glyph for {}", text); - } - let cluster = info.cluster as usize; let c = text[cluster..].chars().next().unwrap(); let limits = Limits::for_char(c); @@ -361,7 +449,7 @@ impl GlyphFragment { modifiers: FrameModifiers::get_in(styles), }; fragment.update_glyph(); - Ok(fragment) + Ok(Some(fragment)) } /// Sets element id and boxes in appropriate way without changing other diff --git a/crates/typst-layout/src/math/lr.rs b/crates/typst-layout/src/math/lr.rs index 2348025e8..4f6d68c6b 100644 --- a/crates/typst-layout/src/math/lr.rs +++ b/crates/typst-layout/src/math/lr.rs @@ -33,12 +33,13 @@ 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 max_extent = inner_fragments - .iter() - .map(|fragment| (fragment.ascent() - axis).max(fragment.descent() + axis)) - .max() - .unwrap_or_default(); + let mut max_extent = Abs::zero(); + for fragment in inner_fragments.iter() { + let (font, size) = fragment.font(ctx, styles, elem.span())?; + let axis = value!(font, axis_height).at(size); + let extent = (fragment.ascent() - axis).max(fragment.descent() + axis); + max_extent = max_extent.max(extent); + } let relative_to = 2.0 * max_extent; let height = elem.size.resolve(styles); diff --git a/crates/typst-layout/src/math/mat.rs b/crates/typst-layout/src/math/mat.rs index 4a897a03e..31c080517 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, style_for_denominator, AlignmentResult, FrameFragment, GlyphFragment, - LeftRightAlternator, MathContext, DELIM_SHORT_FALL, + alignments, find_math_font, style_for_denominator, AlignmentResult, FrameFragment, + GlyphFragment, LeftRightAlternator, MathContext, DELIM_SHORT_FALL, }; const VERTICAL_PADDING: Ratio = Ratio::new(0.1); @@ -186,12 +186,10 @@ fn layout_body( // We pad ascent and descent with the ascent and descent of the paren // to ensure that normal matrices are aligned with others unless they are // way too big. - let paren = GlyphFragment::new_char( - ctx.font, - styles.chain(&denom_style), - '(', - Span::detached(), - )?; + // This will never panic as a paren will never shape into nothing. + let paren = + GlyphFragment::new_char(ctx, styles.chain(&denom_style), '(', Span::detached())? + .unwrap(); for (column, col) in columns.iter().zip(&mut cols) { for (cell, (ascent, descent)) in column.iter().zip(&mut heights) { @@ -314,13 +312,15 @@ 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.world, styles, span)?; + let axis = value!(font, axis_height).resolve(styles); 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_char(ctx.font, styles, left_c, span)?; + let mut left = + ctx.layout_into_fragment(&SymbolElem::packed(left_c).spanned(span), styles)?; left.stretch_vertical(ctx, target - short_fall); left.center_on_axis(); ctx.push(left); @@ -329,7 +329,8 @@ fn layout_delimiters( ctx.push(FrameFragment::new(styles, frame)); if let Some(right_c) = right { - let mut right = GlyphFragment::new_char(ctx.font, styles, right_c, span)?; + let mut right = + ctx.layout_into_fragment(&SymbolElem::packed(right_c).spanned(span), styles)?; right.stretch_vertical(ctx, target - short_fall); right.center_on_axis(); ctx.push(right); diff --git a/crates/typst-layout/src/math/mod.rs b/crates/typst-layout/src/math/mod.rs index 390835067..cccea5d8e 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,11 +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_syntax::Span; +use typst_library::text::{LinebreakElem, RawElem, SpaceElem, TextEdgeBounds, TextElem}; use typst_utils::Numeric; use unicode_math_class::MathClass; @@ -53,12 +49,11 @@ pub fn layout_equation_inline( ) -> SourceResult> { assert!(!elem.block.get(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.world, 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 +103,12 @@ pub fn layout_equation_block( assert!(elem.block.get(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.world, styles, elem.span())?; + let scale_style = style_for_script_scale(&font); let styles = styles.chain(&scale_style); let full_equation_builder = ctx @@ -234,24 +229,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 +347,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 +357,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![], } } @@ -469,17 +435,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 - && styles.get_ref(TextElem::font) != outer.get_ref(TextElem::font) - { - let frame = layout_external(elem, self, styles)?; - self.push(FrameFragment::new(styles, frame).with_spaced(true)); - continue; - } - layout_realized(elem, self, styles)?; } @@ -496,7 +452,10 @@ 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); + let space_width = find_math_font(ctx.engine.world, styles, elem.span()) + .ok() + .and_then(|font| font.space_width()) + .unwrap_or(THICK); ctx.push(MathFragment::Space(space_width.resolve(styles))); } else if elem.is::() { ctx.push(MathFragment::Linebreak); @@ -566,9 +525,11 @@ fn layout_realized( self::underover::layout_overshell(elem, ctx, styles)? } else { let mut frame = layout_external(elem, ctx, styles)?; - if !frame.has_baseline() { - let axis = scaled!(ctx, styles, axis_height); - frame.set_baseline(frame.height() / 2.0 + axis); + if !frame.has_baseline() && !elem.is::() { + if let Ok(font) = find_math_font(ctx.engine.world, styles, elem.span()) { + let axis = value!(font, axis_height).resolve(styles); + frame.set_baseline(frame.height() / 2.0 + axis); + } } ctx.push( FrameFragment::new(styles, frame) diff --git a/crates/typst-layout/src/math/root.rs b/crates/typst-layout/src/math/root.rs index 30948e08e..f1151d41b 100644 --- a/crates/typst-layout/src/math/root.rs +++ b/crates/typst-layout/src/math/root.rs @@ -1,11 +1,11 @@ use typst_library::diag::SourceResult; -use typst_library::foundations::{Packed, StyleChain}; +use typst_library::foundations::{Packed, StyleChain, SymbolElem}; 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::{style_cramped, FrameFragment, MathContext}; /// Lays out a [`RootElem`]. /// @@ -17,45 +17,62 @@ pub fn layout_root( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - let index = elem.index.get_ref(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(); let styles = styles.chain(&cramped); let run = ctx.layout_into_run(&elem.radicand, styles)?; let multiline = run.is_multiline(); - let mut radicand = run.into_fragment(styles).into_frame(); + let radicand = run.into_fragment(styles); if multiline { // Align the frame center line with the math axis. - radicand.set_baseline( - radicand.height() / 2.0 + scaled!(ctx, styles, axis_height), - ); + let (font, size) = radicand.font(ctx, styles, elem.radicand.span())?; + let axis = value!(font, axis_height).at(size); + let mut radicand = radicand.into_frame(); + radicand.set_baseline(radicand.height() / 2.0 + axis); + radicand + } else { + radicand.into_frame() } - radicand }; // Layout root symbol. + let mut sqrt = + ctx.layout_into_fragment(&SymbolElem::packed('√').spanned(span), styles)?; + + let (font, size) = sqrt.font(ctx, styles, span)?; + let thickness = value!(font, radical_rule_thickness).at(size); + let extra_ascender = value!(font, radical_extra_ascender).at(size); + let kern_before = value!(font, radical_kern_before_degree).at(size); + let kern_after = value!(font, radical_kern_after_degree).at(size); + let raise_factor = percent!(font, radical_degree_bottom_raise_percent); + let gap = value!( + font, styles, + text: radical_vertical_gap, + display: radical_display_style_vertical_gap, + ) + .at(size); + + let line = FrameItem::Shape( + Geometry::Line(Point::with_x(radicand.width())).stroked(FixedStroke::from_pair( + sqrt.fill() + .unwrap_or_else(|| styles.get_ref(TextElem::fill).as_decoration()), + thickness, + )), + span, + ); + let target = radicand.height() + thickness + gap; - let mut sqrt = GlyphFragment::new_char(ctx.font, styles, '√', span)?; sqrt.stretch_vertical(ctx, target); let sqrt = sqrt.into_frame(); // Layout the index. let sscript = EquationElem::size.set(MathSize::ScriptScript).wrap(); - let index = index + let index = elem + .index + .get_ref(styles) .as_ref() .map(|elem| ctx.layout_into_frame(elem, styles.chain(&sscript))) .transpose()?; @@ -107,19 +124,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( - styles.get_ref(TextElem::fill).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 c9d20aa68..7bdd0f07d 100644 --- a/crates/typst-layout/src/math/shared.rs +++ b/crates/typst-layout/src/math/shared.rs @@ -1,32 +1,47 @@ +use comemo::Tracked; use ttf_parser::math::MathValue; use ttf_parser::Tag; +use typst_library::diag::{bail, SourceResult}; use typst_library::foundations::{Style, StyleChain}; use typst_library::layout::{Abs, Em, FixedAlignment, Frame, Point, Size}; 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 $(,)?) => { +macro_rules! value { + ($font:expr, $styles:expr, text: $text:ident, display: $display:ident $(,)?) => { match $styles.get(typst_library::math::EquationElem::size) { - typst_library::math::MathSize::Display => scaled!($ctx, $styles, $display), - _ => scaled!($ctx, $styles, $text), + typst_library::math::MathSize::Display => value!($font, $display), + _ => value!($font, $text), } }; - ($ctx:expr, $styles:expr, $name:ident) => { - $crate::math::Scaled::scaled( - $ctx.constants.$name(), - $ctx, - $styles.resolve(typst_library::text::TextElem::size), - ) + ($font:expr, $name:ident) => { + $font + .ttf() + .tables() + .math + .and_then(|math| math.constants) + .map(|constants| { + crate::math::shared::Scaled::scaled(constants.$name(), &$font) + }) + .unwrap() }; } macro_rules! percent { - ($ctx:expr, $name:ident) => { - $ctx.constants.$name() as f64 / 100.0 + ($font:expr, $name:ident) => { + $font + .ttf() + .tables() + .math + .and_then(|math| math.constants) + .map(|constants| constants.$name()) + .unwrap() as f64 + / 100.0 }; } @@ -35,27 +50,47 @@ 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; + fn scaled(self, font: &Font) -> Em; } impl Scaled for i16 { - fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs { - ctx.font.to_em(self).at(font_size) + fn scaled(self, font: &Font) -> Em { + font.to_em(self) } } impl Scaled for u16 { - fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs { - ctx.font.to_em(self).at(font_size) + fn scaled(self, font: &Font) -> Em { + font.to_em(self) } } impl Scaled for MathValue<'_> { - fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs { - self.value.scaled(ctx, font_size) + fn scaled(self, font: &Font) -> Em { + self.value.scaled(font) } } +/// Get the current math font. +#[comemo::memoize] +pub fn find_math_font( + world: Tracked, + styles: StyleChain, + span: Span, +) -> SourceResult { + let variant = variant(styles); + 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. pub fn style_cramped() -> LazyHash