diff --git a/crates/typst-layout/src/math/mod.rs b/crates/typst-layout/src/math/mod.rs index 905e159ab..702816ee6 100644 --- a/crates/typst-layout/src/math/mod.rs +++ b/crates/typst-layout/src/math/mod.rs @@ -538,11 +538,7 @@ fn layout_realized( } else if let Some(elem) = elem.to_packed::() { self::text::layout_text(elem, ctx, styles)?; } else if let Some(elem) = elem.to_packed::() { - // This is a hack to avoid affecting layout that will be replaced in a - // later commit. - let text_elem = TextElem::new(elem.text.to_string().into()); - let packed = Packed::new(text_elem); - self::text::layout_text(&packed, ctx, styles)?; + self::text::layout_symbol(elem, ctx, styles)?; } else if let Some(elem) = elem.to_packed::() { layout_box(elem, ctx, styles)?; } else if elem.is::() { diff --git a/crates/typst-layout/src/math/text.rs b/crates/typst-layout/src/math/text.rs index 7e849c46c..6b9703aa2 100644 --- a/crates/typst-layout/src/math/text.rs +++ b/crates/typst-layout/src/math/text.rs @@ -2,7 +2,7 @@ use std::f64::consts::SQRT_2; use ecow::{eco_vec, EcoString}; use typst_library::diag::SourceResult; -use typst_library::foundations::{Packed, StyleChain, StyleVec}; +use typst_library::foundations::{Packed, StyleChain, StyleVec, SymbolElem}; use typst_library::layout::{Abs, Size}; use typst_library::math::{EquationElem, MathSize, MathVariant}; use typst_library::text::{ @@ -22,54 +22,66 @@ pub fn layout_text( ) -> SourceResult<()> { let text = &elem.text; let span = elem.span(); - let mut chars = text.chars(); - let math_size = EquationElem::size_in(styles); - let mut dtls = ctx.dtls_table.is_some(); - let fragment: MathFragment = if let Some(mut glyph) = chars - .next() - .filter(|_| chars.next().is_none()) - .map(|c| dtls_char(c, &mut dtls)) - .map(|c| styled_char(styles, c, true)) - .and_then(|c| GlyphFragment::try_new(ctx, styles, c, span)) - { - // A single letter that is available in the math font. - if dtls { - glyph.make_dotless_form(ctx); - } + let fragment = if text.contains(is_newline) { + layout_text_lines(text.split(is_newline), span, ctx, styles)? + } else { + layout_inline_text(text, span, ctx, styles)? + }; + ctx.push(fragment); + Ok(()) +} - match math_size { - MathSize::Script => { - glyph.make_script_size(ctx); - } - MathSize::ScriptScript => { - glyph.make_script_script_size(ctx); - } - _ => (), +/// Layout multiple lines of text. +fn layout_text_lines<'a>( + lines: impl Iterator, + span: Span, + ctx: &mut MathContext, + styles: StyleChain, +) -> SourceResult { + let mut fragments = vec![]; + for (i, line) in lines.enumerate() { + if i != 0 { + fragments.push(MathFragment::Linebreak); } + if !line.is_empty() { + fragments.push(layout_inline_text(line, span, ctx, styles)?.into()); + } + } + let mut frame = MathRun::new(fragments).into_frame(styles); + let axis = scaled!(ctx, styles, axis_height); + frame.set_baseline(frame.height() / 2.0 + axis); + Ok(FrameFragment::new(styles, frame)) +} - if glyph.class == MathClass::Large { - let mut variant = if math_size == MathSize::Display { - let height = scaled!(ctx, styles, display_operator_min_height) - .max(SQRT_2 * glyph.height()); - glyph.stretch_vertical(ctx, height, Abs::zero()) - } else { - glyph.into_variant() - }; - // TeXbook p 155. Large operators are always vertically centered on the axis. - variant.center_on_axis(ctx); - variant.into() - } else { - glyph.into() - } - } else if text.chars().all(|c| c.is_ascii_digit() || c == '.') { - // Numbers aren't that difficult. +/// Layout the given text string into a [`FrameFragment`] after styling all +/// characters for the math font (without auto-italics). +fn layout_inline_text( + text: &str, + span: Span, + ctx: &mut MathContext, + styles: StyleChain, +) -> SourceResult { + if text.chars().all(|c| c.is_ascii_digit() || c == '.') { + // Small optimization for numbers. Note that this lays out slightly + // differently to normal text and is worth re-evaluating in the future. let mut fragments = vec![]; - for c in text.chars() { - let c = styled_char(styles, c, false); - fragments.push(GlyphFragment::new(ctx, styles, c, span).into()); + let is_single = text.chars().count() == 1; + for unstyled_c in text.chars() { + let c = styled_char(styles, unstyled_c, false); + let mut glyph = GlyphFragment::new(ctx, styles, c, span); + if is_single { + // Duplicate what `layout_glyph` does exactly even if it's + // probably incorrect here. + match EquationElem::size_in(styles) { + MathSize::Script => glyph.make_script_size(ctx), + MathSize::ScriptScript => glyph.make_script_script_size(ctx), + _ => {} + } + } + fragments.push(glyph.into()); } let frame = MathRun::new(fragments).into_frame(styles); - FrameFragment::new(styles, frame).with_text_like(true).into() + Ok(FrameFragment::new(styles, frame).with_text_like(true)) } else { let local = [ TextElem::set_top_edge(TopEdge::Metric(TopEdgeMetric::Bounds)), @@ -77,64 +89,97 @@ pub fn layout_text( ] .map(|p| p.wrap()); - // Anything else is handled by Typst's standard text layout. let styles = styles.chain(&local); - let text: EcoString = + let styled_text: EcoString = text.chars().map(|c| styled_char(styles, c, false)).collect(); - if text.contains(is_newline) { - let mut fragments = vec![]; - for (i, piece) in text.split(is_newline).enumerate() { - if i != 0 { - fragments.push(MathFragment::Linebreak); - } - if !piece.is_empty() { - fragments.push(layout_complex_text(piece, ctx, span, styles)?.into()); - } - } - let mut frame = MathRun::new(fragments).into_frame(styles); - let axis = scaled!(ctx, styles, axis_height); - frame.set_baseline(frame.height() / 2.0 + axis); - FrameFragment::new(styles, frame).into() - } else { - layout_complex_text(&text, ctx, span, styles)?.into() + + let spaced = styled_text.graphemes(true).nth(1).is_some(); + let elem = TextElem::packed(styled_text).spanned(span); + + // There isn't a natural width for a paragraph in a math environment; + // because it will be placed somewhere probably not at the left margin + // it will overflow. So emulate an `hbox` instead and allow the + // paragraph to extend as far as needed. + let frame = (ctx.engine.routines.layout_inline)( + ctx.engine, + &StyleVec::wrap(eco_vec![elem]), + ctx.locator.next(&span), + styles, + false, + Size::splat(Abs::inf()), + false, + )? + .into_frame(); + + Ok(FrameFragment::new(styles, frame) + .with_class(MathClass::Alphabetic) + .with_text_like(true) + .with_spaced(spaced)) + } +} + +/// Layout a single character in the math font with the correct styling applied +/// (includes auto-italics). +pub fn layout_symbol( + elem: &Packed, + ctx: &mut MathContext, + styles: StyleChain, +) -> SourceResult<()> { + // Switch dotless char to normal when we have the dtls OpenType feature. + // This should happen before the main styling pass. + let (unstyled_c, dtls) = match try_dotless(elem.text) { + Some(c) if ctx.dtls_table.is_some() => (c, true), + _ => (elem.text, false), + }; + let c = styled_char(styles, unstyled_c, true); + let fragment = match GlyphFragment::try_new(ctx, styles, c, elem.span()) { + Some(glyph) => layout_glyph(glyph, dtls, ctx, styles), + None => { + // Not in the math font, fallback to normal inline text layout. + layout_inline_text(c.encode_utf8(&mut [0; 4]), elem.span(), ctx, styles)? + .into() } }; - ctx.push(fragment); Ok(()) } -/// Layout the given text string into a [`FrameFragment`]. -fn layout_complex_text( - text: &str, +/// Layout a [`GlyphFragment`]. +fn layout_glyph( + mut glyph: GlyphFragment, + dtls: bool, ctx: &mut MathContext, - span: Span, styles: StyleChain, -) -> SourceResult { - // There isn't a natural width for a paragraph in a math environment; - // because it will be placed somewhere probably not at the left margin - // it will overflow. So emulate an `hbox` instead and allow the paragraph - // to extend as far as needed. - let spaced = text.graphemes(true).nth(1).is_some(); - let elem = TextElem::packed(text).spanned(span); - let frame = (ctx.engine.routines.layout_inline)( - ctx.engine, - &StyleVec::wrap(eco_vec![elem]), - ctx.locator.next(&span), - styles, - false, - Size::splat(Abs::inf()), - false, - )? - .into_frame(); +) -> MathFragment { + if dtls { + glyph.make_dotless_form(ctx); + } + let math_size = EquationElem::size_in(styles); + match math_size { + MathSize::Script => glyph.make_script_size(ctx), + MathSize::ScriptScript => glyph.make_script_script_size(ctx), + _ => {} + } - Ok(FrameFragment::new(styles, frame) - .with_class(MathClass::Alphabetic) - .with_text_like(true) - .with_spaced(spaced)) + if glyph.class == MathClass::Large { + let mut variant = if math_size == MathSize::Display { + let height = scaled!(ctx, styles, display_operator_min_height) + .max(SQRT_2 * glyph.height()); + glyph.stretch_vertical(ctx, height, Abs::zero()) + } else { + glyph.into_variant() + }; + // TeXbook p 155. Large operators are always vertically centered on the + // axis. + variant.center_on_axis(ctx); + variant.into() + } else { + glyph.into() + } } -/// Select the correct styled math letter. +/// Style the character by selecting the unicode codepoint for italic, bold, +/// caligraphic, etc. /// /// /// @@ -353,15 +398,12 @@ fn greek_exception( }) } -/// Switch dotless character to non dotless character for use of the dtls -/// OpenType feature. -pub fn dtls_char(c: char, dtls: &mut bool) -> char { - match (c, *dtls) { - ('ı', true) => 'i', - ('ȷ', true) => 'j', - _ => { - *dtls = false; - c - } +/// The non-dotless version of a dotless character that can be used with the +/// `dtls` OpenType feature. +pub fn try_dotless(c: char) -> Option { + match c { + 'ı' => Some('i'), + 'ȷ' => Some('j'), + _ => None, } } diff --git a/tests/ref/math-equation-auto-wrapping.png b/tests/ref/math-equation-auto-wrapping.png index 9c600172e..2476d668c 100644 Binary files a/tests/ref/math-equation-auto-wrapping.png and b/tests/ref/math-equation-auto-wrapping.png differ diff --git a/tests/ref/math-mat-align-explicit-alternating.png b/tests/ref/math-mat-align-explicit-alternating.png index 37e8dc06a..1ebcc7b68 100644 Binary files a/tests/ref/math-mat-align-explicit-alternating.png and b/tests/ref/math-mat-align-explicit-alternating.png differ diff --git a/tests/ref/math-mat-align-explicit-left.png b/tests/ref/math-mat-align-explicit-left.png index 09ce93982..cb9819248 100644 Binary files a/tests/ref/math-mat-align-explicit-left.png and b/tests/ref/math-mat-align-explicit-left.png differ diff --git a/tests/ref/math-mat-align-explicit-right.png b/tests/ref/math-mat-align-explicit-right.png index 3592c0cf5..b537e6571 100644 Binary files a/tests/ref/math-mat-align-explicit-right.png and b/tests/ref/math-mat-align-explicit-right.png differ diff --git a/tests/ref/math-mat-align-implicit.png b/tests/ref/math-mat-align-implicit.png index 0c508efc4..b184d9140 100644 Binary files a/tests/ref/math-mat-align-implicit.png and b/tests/ref/math-mat-align-implicit.png differ diff --git a/tests/ref/math-vec-align-explicit-alternating.png b/tests/ref/math-vec-align-explicit-alternating.png index 37e8dc06a..1ebcc7b68 100644 Binary files a/tests/ref/math-vec-align-explicit-alternating.png and b/tests/ref/math-vec-align-explicit-alternating.png differ diff --git a/tests/suite/foundations/content.typ b/tests/suite/foundations/content.typ index 9ddee5975..c3c119e33 100644 --- a/tests/suite/foundations/content.typ +++ b/tests/suite/foundations/content.typ @@ -85,7 +85,7 @@ [With ] vars .pairs() - .map(((name, value)) => $name = value$) + .map(((name, value)) => $#symbol(name) = value$) .join(", ", last: " and ") [ we have:] $ equation = result $ diff --git a/tests/suite/math/alignment.typ b/tests/suite/math/alignment.typ index 63033ef5c..941c20556 100644 --- a/tests/suite/math/alignment.typ +++ b/tests/suite/math/alignment.typ @@ -4,10 +4,10 @@ // Test alignment step functions. #set page(width: 225pt) $ -"a" &= c \ -&= c + 1 & "By definition" \ -&= d + 100 + 1000 \ -&= x && "Even longer" \ +a &= c \ + &= c + 1 & "By definition" \ + &= d + 100 + 1000 \ + &= x && "Even longer" \ $ --- math-align-post-fix --- diff --git a/tests/suite/math/delimited.typ b/tests/suite/math/delimited.typ index ca82427dd..794ffd8aa 100644 --- a/tests/suite/math/delimited.typ +++ b/tests/suite/math/delimited.typ @@ -41,8 +41,8 @@ $floor(x/2), ceil(x/2), abs(x), norm(x)$ --- math-lr-color --- // Test colored delimiters $ lr( - text("(", fill: #green) a/b - text(")", fill: #blue) + text(\(, fill: #green) a/b + text(\), fill: #blue) ) $ --- math-lr-mid --- diff --git a/tests/suite/math/stretch.typ b/tests/suite/math/stretch.typ index 1377f4d21..d145f72a1 100644 --- a/tests/suite/math/stretch.typ +++ b/tests/suite/math/stretch.typ @@ -63,8 +63,8 @@ $ ext(bar.v) quad ext(bar.v.double) quad // Test stretch when base is given with shorthand. $stretch(||, size: #2em)$ $stretch(\(, size: #2em)$ -$stretch("⟧", size: #2em)$ -$stretch("|", size: #2em)$ +$stretch(⟧, size: #2em)$ +$stretch(|, size: #2em)$ $stretch(->, size: #2em)$ $stretch(↣, size: #2em)$ @@ -87,7 +87,7 @@ $ body^"text" $ #{ let body = $stretch(=)$ for i in range(24) { - body = $body$ + body = $body$ } $body^"long text"$ }