From af564e589f7d9bf4d40538f9e35dfbdd78ba87ed Mon Sep 17 00:00:00 2001 From: mkorje Date: Tue, 8 Jul 2025 22:58:37 +1000 Subject: [PATCH] WIP --- crates/typst-layout/src/math/accent.rs | 28 +++-- crates/typst-layout/src/math/attach.rs | 90 ++++++++++---- crates/typst-layout/src/math/frac.rs | 38 +++--- crates/typst-layout/src/math/fragment.rs | 95 +++++++++++--- crates/typst-layout/src/math/lr.rs | 12 +- crates/typst-layout/src/math/mat.rs | 20 +-- crates/typst-layout/src/math/mod.rs | 72 +++-------- crates/typst-layout/src/math/root.rs | 70 ++++++----- crates/typst-layout/src/math/shared.rs | 143 +++++++++++++++------- crates/typst-layout/src/math/text.rs | 47 +++---- crates/typst-layout/src/math/underover.rs | 34 ++--- crates/typst-library/src/math/style.rs | 5 +- crates/typst-library/src/text/mod.rs | 1 + crates/typst-realize/src/lib.rs | 9 +- tests/ref/issue-2214-baseline-math.png | Bin 868 -> 884 bytes tests/ref/issue-852-mat-type.png | Bin 2282 -> 2287 bytes tests/ref/math-font-fallback.png | Bin 402 -> 492 bytes tests/ref/math-lr-scripts.png | Bin 611 -> 607 bytes tests/ref/math-mat-spread-2d.png | Bin 3391 -> 3351 bytes tests/ref/math-mat-spread.png | Bin 1796 -> 1779 bytes tests/ref/math-nested-normal-layout.png | Bin 1312 -> 1077 bytes tests/ref/math-size-math-content-1.png | Bin 2098 -> 2133 bytes tests/ref/math-size-math-content-2.png | Bin 1472 -> 1557 bytes tests/ref/math-size-math-content-3.png | Bin 877 -> 920 bytes tests/ref/math-text-size.png | Bin 1265 -> 1194 bytes tests/suite/math/text.typ | 2 +- 26 files changed, 399 insertions(+), 267 deletions(-) diff --git a/crates/typst-layout/src/math/accent.rs b/crates/typst-layout/src/math/accent.rs index e7f051ace..e471186bc 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. @@ -21,20 +20,24 @@ pub fn layout_accent( let accent = elem.accent; let top_accent = !accent.is_bottom(); + let base_span = elem.base.span(); + // Try to replace the base glyph with its dotless variant. let dtls = style_dtls(); let base_styles = 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)?; // 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!(ctx, base_styles, base_span, base, flattened_accent_base_height); let flac = style_flac(); let accent_styles = if top_accent && base.ascent() > flattened_base_height { styles.chain(&flac) @@ -42,23 +45,24 @@ pub fn layout_accent( styles }; - let mut glyph = - GlyphFragment::new_char(ctx.font, accent_styles, accent.0, elem.span())?; + let accent = SymbolElem::packed(accent.0).spanned(elem.span()); + let mut accent = ctx.layout_into_fragment(&accent, 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(base.font_size().unwrap_or_default()); + 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!(ctx, base_styles, base_span, base, accent_base_height); 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..a074db032 100644 --- a/crates/typst-layout/src/math/attach.rs +++ b/crates/typst-layout/src/math/attach.rs @@ -4,11 +4,13 @@ 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 { @@ -173,19 +175,20 @@ fn layout_attachments( [tl, t, tr, bl, b, br]: [Option; 6], ) -> SourceResult<()> { let base_class = base.class(); + let font = find_math_font(ctx.engine.world, 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 @@ -215,7 +218,10 @@ 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 = typst_library::foundations::Resolve::resolve( + value!(font, space_after_script), + styles, + ); // Calculate the distance each pre-script extends to the left of the base's // width. @@ -364,7 +370,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], @@ -373,16 +379,27 @@ 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 = typst_library::foundations::Resolve::resolve( + value!(font, upper_limit_gap_min), + styles, + ); + let upper_rise_min = typst_library::foundations::Resolve::resolve( + value!(font, upper_limit_baseline_rise_min), + styles, + ); 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 = typst_library::foundations::Resolve::resolve( + value!(font, lower_limit_gap_min), + styles, + ); + let lower_drop_min = typst_library::foundations::Resolve::resolve( + value!(font, lower_limit_baseline_drop_min), + styles, + ); base.descent() + lower_drop_min.max(lower_gap_min + b.ascent()) }); @@ -393,25 +410,48 @@ 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 styles.get(EquationElem::cramped) { - scaled!(ctx, styles, superscript_shift_up_cramped) - } else { - scaled!(ctx, styles, superscript_shift_up) - }; + let sup_shift_up = typst_library::foundations::Resolve::resolve( + if styles.get(EquationElem::cramped) { + value!(font, superscript_shift_up_cramped) + } else { + value!(font, superscript_shift_up) + }, + styles, + ); - 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 sup_bottom_min = typst_library::foundations::Resolve::resolve( + value!(font, superscript_bottom_min), + styles, + ); + let sup_bottom_max_with_sub = typst_library::foundations::Resolve::resolve( + value!(font, superscript_bottom_max_with_subscript), + styles, + ); + let sup_drop_max = typst_library::foundations::Resolve::resolve( + value!(font, superscript_baseline_drop_max), + styles, + ); + let gap_min = typst_library::foundations::Resolve::resolve( + value!(font, sub_superscript_gap_min), + styles, + ); + let sub_shift_down = typst_library::foundations::Resolve::resolve( + value!(font, subscript_shift_down), + styles, + ); + let sub_top_max = typst_library::foundations::Resolve::resolve( + value!(font, subscript_top_max), + styles, + ); + let sub_drop_min = typst_library::foundations::Resolve::resolve( + value!(font, subscript_baseline_drop_min), + styles, + ); 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..bc195a0b0 100644 --- a/crates/typst-layout/src/math/frac.rs +++ b/crates/typst-layout/src/math/frac.rs @@ -7,8 +7,8 @@ use typst_library::visualize::{FixedStroke, Geometry}; use typst_syntax::Span; use super::{ - style_for_denominator, style_for_numerator, FrameFragment, GlyphFragment, - MathContext, DELIM_SHORT_FALL, + style_for_denominator, style_for_numerator, FrameFragment, MathContext, + DELIM_SHORT_FALL, }; const FRAC_AROUND: Em = Em::new(0.1); @@ -50,26 +50,26 @@ fn layout_frac_like( 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, - text: fraction_numerator_shift_up, + let axis = value!(ctx, styles, span, axis_height); + let thickness = value!(ctx, styles, span, fraction_rule_thickness); + let shift_up = value!( + ctx, styles, span, + inline: fraction_numerator_shift_up, display: fraction_numerator_display_style_shift_up, ); - let shift_down = scaled!( - ctx, styles, - text: fraction_denominator_shift_down, + let shift_down = value!( + ctx, styles, span, + inline: fraction_denominator_shift_down, display: fraction_denominator_display_style_shift_down, ); - let num_min = scaled!( - ctx, styles, - text: fraction_numerator_gap_min, + let num_min = value!( + ctx, styles, span, + inline: fraction_numerator_gap_min, display: fraction_num_display_style_gap_min, ); - let denom_min = scaled!( - ctx, styles, - text: fraction_denominator_gap_min, + let denom_min = value!( + ctx, styles, span, + inline: fraction_denominator_gap_min, display: fraction_denom_display_style_gap_min, ); @@ -109,12 +109,14 @@ fn layout_frac_like( frame.push_frame(denom_pos, denom); if binom { - let mut left = GlyphFragment::new_char(ctx.font, styles, '(', span)?; + let left = SymbolElem::packed('(').spanned(span); + let mut left = ctx.layout_into_fragment(&left, 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 right = SymbolElem::packed(')').spanned(span); + let mut right = ctx.layout_into_fragment(&right, 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..41fb80023 100644 --- a/crates/typst-layout/src/math/fragment.rs +++ b/crates/typst-layout/src/math/fragment.rs @@ -11,7 +11,11 @@ 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; @@ -192,6 +196,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 +290,60 @@ 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) } /// 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. - #[comemo::memoize] + // #[comemo::memoize] pub fn new( - font: &Font, + ctx: &MathContext, styles: StyleChain, text: &str, span: Span, - ) -> SourceResult { + ) -> 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"); + }; + let mut buffer = UnicodeBuffer::new(); buffer.push_str(text); buffer.set_language(language(styles)); @@ -292,7 +358,7 @@ impl GlyphFragment { let features = features(styles); let plan = create_shape_plan( - font, + &font, buffer.direction(), buffer.script(), buffer.language().as_ref(), @@ -300,18 +366,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 +424,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..008c97044 100644 --- a/crates/typst-layout/src/math/lr.rs +++ b/crates/typst-layout/src/math/lr.rs @@ -33,12 +33,12 @@ 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 axis = value!(ctx, styles, elem.span(), fragment, axis_height); + 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..139c9278d 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, }; @@ -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,14 @@ fn layout_delimiters( span: Span, ) -> SourceResult<()> { let short_fall = DELIM_SHORT_FALL.resolve(styles); - let axis = scaled!(ctx, styles, axis_height); + let axis = value!(ctx, styles, span, 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_char(ctx.font, styles, left_c, span)?; + let left = SymbolElem::packed(left_c).spanned(span); + let mut left = ctx.layout_into_fragment(&left, styles)?; left.stretch_vertical(ctx, target - short_fall); left.center_on_axis(); ctx.push(left); @@ -329,7 +328,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 right = SymbolElem::packed(right_c).spanned(span); + let mut right = ctx.layout_into_fragment(&right, 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..ca4ae2003 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, @@ -28,10 +28,9 @@ 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, + BottomEdge, BottomEdgeMetric, LinebreakElem, RawElem, SpaceElem, TextEdgeBounds, + TextElem, TopEdge, TopEdgeMetric, }; -use typst_library::World; -use typst_syntax::Span; use typst_utils::Numeric; use unicode_math_class::MathClass; @@ -53,12 +52,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 +106,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 +232,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 +350,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 +360,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 +438,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 +455,8 @@ 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 font = find_math_font(ctx.engine.world, styles, elem.span())?; + let space_width = font.space_width().unwrap_or(THICK); ctx.push(MathFragment::Space(space_width.resolve(styles))); } else if elem.is::() { ctx.push(MathFragment::Linebreak); @@ -565,9 +525,17 @@ fn layout_realized( } else if let Some(elem) = elem.to_packed::() { self::underover::layout_overshell(elem, ctx, styles)? } else { + let local = [ + TextElem::top_edge.set(TopEdge::Metric(TopEdgeMetric::Bounds)), + TextElem::bottom_edge.set(BottomEdge::Metric(BottomEdgeMetric::Bounds)), + ] + .map(|p| p.wrap()); + let styles = if elem.is::() { styles.chain(&local) } else { styles }; + 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.world, styles, elem.span())?; + let axis = value!(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 30948e08e..0d856f8b8 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::{find_math_font, style_cramped, FrameFragment, MathContext, MathFragment}; /// Lays out a [`RootElem`]. /// @@ -17,45 +17,59 @@ 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 axis = value!(ctx, styles, span, radicand, axis_height); + let mut radicand = radicand.into_frame(); + radicand.set_baseline(radicand.height() / 2.0 + axis); + radicand + } else { + radicand.into_frame() } - radicand }; // Layout root symbol. + let sqrt = SymbolElem::packed('√').spanned(span); + let mut sqrt = ctx.layout_into_fragment(&sqrt, styles)?; + let gap = value!( + ctx, styles, span, sqrt, + inline: radical_vertical_gap, + display: radical_display_style_vertical_gap, + ); + let thickness = value!(ctx, styles, span, sqrt, radical_rule_thickness); + let extra_ascender = value!(ctx, styles, span, sqrt, radical_extra_ascender); + let kern_before = value!(ctx, styles, span, sqrt, radical_kern_before_degree); + let kern_after = value!(ctx, styles, span, sqrt, radical_kern_after_degree); + let raise_factor = + percent!(ctx, styles, span, sqrt, radical_degree_bottom_raise_percent); + + 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 +121,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..cc936b5a3 100644 --- a/crates/typst-layout/src/math/shared.rs +++ b/crates/typst-layout/src/math/shared.rs @@ -1,61 +1,119 @@ -use ttf_parser::math::MathValue; +use comemo::Tracked; 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 $(,)?) => { - match $styles.get(typst_library::math::EquationElem::size) { - typst_library::math::MathSize::Display => scaled!($ctx, $styles, $display), - _ => scaled!($ctx, $styles, $text), +macro_rules! value { + ($ctx:expr, $styles:expr, $span:expr, $fragment:expr, inline: $inline:ident, display: $display:ident $(,)?) => { + match $fragment + .math_size() + .unwrap_or_else(|| $styles.get(typst_library::math::EquationElem::size)) + { + typst_library::math::MathSize::Display => { + value!($ctx, $styles, $span, $fragment, $display) + } + _ => value!($ctx, $styles, $span, $fragment, $inline), } }; - ($ctx:expr, $styles:expr, $name:ident) => { - $crate::math::Scaled::scaled( - $ctx.constants.$name(), - $ctx, - $styles.resolve(typst_library::text::TextElem::size), - ) + ($ctx:expr, $styles:expr, $span:expr, inline: $inline:ident, display: $display:ident $(,)?) => { + match $styles.get(typst_library::math::EquationElem::size) { + typst_library::math::MathSize::Display => { + value!($ctx, $styles, $span, $display) + } + _ => value!($ctx, $styles, $span, $inline), + } + }; + ($ctx:expr, $styles:expr, $span:expr, $fragment:expr, $name:ident) => {{ + let font = match $fragment { + crate::math::MathFragment::Glyph(ref glyph) => glyph.item.font.clone(), + _ => crate::math::find_math_font($ctx.engine.world, $styles, $span)?, + }; + font.ttf() + .tables() + .math + .and_then(|math| math.constants) + .map(|constants| font.to_em(constants.$name().value)) + .unwrap() + .at($fragment + .font_size() + .unwrap_or_else(|| $styles.resolve(typst_library::text::TextElem::size))) + }}; + ($ctx:expr, $styles:expr, $span:expr, $name:ident) => {{ + let font = crate::math::find_math_font($ctx.engine.world, $styles, $span)?; + value!(font, $styles, $name) + }}; + ($font:expr, $styles:expr, $name:ident) => { + typst_library::foundations::Resolve::resolve(value!($font, $name), $styles) + }; + ($font:expr, $name:ident) => { + $font + .ttf() + .tables() + .math + .and_then(|math| math.constants) + .map(|constants| $font.to_em(constants.$name().value)) + .unwrap() }; } macro_rules! percent { - ($ctx:expr, $name:ident) => { - $ctx.constants.$name() as f64 / 100.0 + ($ctx:expr, $styles:expr, $span:expr, $fragment:expr, $name:ident) => { + match $fragment { + MathFragment::Glyph(ref glyph) => glyph.item.font.clone(), + _ => find_math_font($ctx.engine.world, $styles, $span)?, + } + .ttf() + .tables() + .math + .and_then(|math| math.constants) + .map(|constants| constants.$name()) + .unwrap() as f64 + / 100.0 }; } +macro_rules! word { + ($font:expr, $name:ident) => { + $font + .ttf() + .tables() + .math + .and_then(|math| math.constants) + .map(|constants| $font.to_em(constants.$name())) + .unwrap() + }; +} + +#[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) +} + /// 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) - } -} - /// Styles something as cramped. pub fn style_cramped() -> LazyHash