Update math TextElem layout to separate out SymbolElem

This commit is contained in:
Ian Wrzesinski 2025-01-20 14:39:26 -05:00
parent 7838da02ec
commit 6fe1e20afb
12 changed files with 151 additions and 113 deletions

View File

@ -538,11 +538,7 @@ fn layout_realized(
} else if let Some(elem) = elem.to_packed::<TextElem>() { } else if let Some(elem) = elem.to_packed::<TextElem>() {
self::text::layout_text(elem, ctx, styles)?; self::text::layout_text(elem, ctx, styles)?;
} else if let Some(elem) = elem.to_packed::<SymbolElem>() { } else if let Some(elem) = elem.to_packed::<SymbolElem>() {
// This is a hack to avoid affecting layout that will be replaced in a self::text::layout_symbol(elem, ctx, styles)?;
// 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)?;
} else if let Some(elem) = elem.to_packed::<BoxElem>() { } else if let Some(elem) = elem.to_packed::<BoxElem>() {
layout_box(elem, ctx, styles)?; layout_box(elem, ctx, styles)?;
} else if elem.is::<AlignPointElem>() { } else if elem.is::<AlignPointElem>() {

View File

@ -2,7 +2,7 @@ use std::f64::consts::SQRT_2;
use ecow::{eco_vec, EcoString}; use ecow::{eco_vec, EcoString};
use typst_library::diag::SourceResult; 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::layout::{Abs, Size};
use typst_library::math::{EquationElem, MathSize, MathVariant}; use typst_library::math::{EquationElem, MathSize, MathVariant};
use typst_library::text::{ use typst_library::text::{
@ -22,54 +22,66 @@ pub fn layout_text(
) -> SourceResult<()> { ) -> SourceResult<()> {
let text = &elem.text; let text = &elem.text;
let span = elem.span(); let span = elem.span();
let mut chars = text.chars(); let fragment = if text.contains(is_newline) {
let math_size = EquationElem::size_in(styles); layout_text_lines(text.split(is_newline), span, ctx, styles)?
let mut dtls = ctx.dtls_table.is_some(); } else {
let fragment: MathFragment = if let Some(mut glyph) = chars layout_inline_text(text, span, ctx, styles)?
.next() };
.filter(|_| chars.next().is_none()) ctx.push(fragment);
.map(|c| dtls_char(c, &mut dtls)) Ok(())
.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);
}
match math_size { /// Layout multiple lines of text.
MathSize::Script => { fn layout_text_lines<'a>(
glyph.make_script_size(ctx); lines: impl Iterator<Item = &'a str>,
} span: Span,
MathSize::ScriptScript => { ctx: &mut MathContext,
glyph.make_script_script_size(ctx); styles: StyleChain,
} ) -> SourceResult<FrameFragment> {
_ => (), 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 { /// Layout the given text string into a [`FrameFragment`] after styling all
let mut variant = if math_size == MathSize::Display { /// characters for the math font (without auto-italics).
let height = scaled!(ctx, styles, display_operator_min_height) fn layout_inline_text(
.max(SQRT_2 * glyph.height()); text: &str,
glyph.stretch_vertical(ctx, height, Abs::zero()) span: Span,
} else { ctx: &mut MathContext,
glyph.into_variant() styles: StyleChain,
}; ) -> SourceResult<FrameFragment> {
// TeXbook p 155. Large operators are always vertically centered on the axis. if text.chars().all(|c| c.is_ascii_digit() || c == '.') {
variant.center_on_axis(ctx); // Small optimization for numbers. Note that this lays out slightly
variant.into() // differently to normal text and is worth re-evaluating in the future.
} else {
glyph.into()
}
} else if text.chars().all(|c| c.is_ascii_digit() || c == '.') {
// Numbers aren't that difficult.
let mut fragments = vec![]; let mut fragments = vec![];
for c in text.chars() { let is_single = text.chars().count() == 1;
let c = styled_char(styles, c, false); for unstyled_c in text.chars() {
fragments.push(GlyphFragment::new(ctx, styles, c, span).into()); 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); 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 { } else {
let local = [ let local = [
TextElem::set_top_edge(TopEdge::Metric(TopEdgeMetric::Bounds)), TextElem::set_top_edge(TopEdge::Metric(TopEdgeMetric::Bounds)),
@ -77,64 +89,97 @@ pub fn layout_text(
] ]
.map(|p| p.wrap()); .map(|p| p.wrap());
// Anything else is handled by Typst's standard text layout.
let styles = styles.chain(&local); let styles = styles.chain(&local);
let text: EcoString = let styled_text: EcoString =
text.chars().map(|c| styled_char(styles, c, false)).collect(); text.chars().map(|c| styled_char(styles, c, false)).collect();
if text.contains(is_newline) {
let mut fragments = vec![]; let spaced = styled_text.graphemes(true).nth(1).is_some();
for (i, piece) in text.split(is_newline).enumerate() { let elem = TextElem::packed(styled_text).spanned(span);
if i != 0 {
fragments.push(MathFragment::Linebreak); // 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
if !piece.is_empty() { // it will overflow. So emulate an `hbox` instead and allow the
fragments.push(layout_complex_text(piece, ctx, span, styles)?.into()); // paragraph to extend as far as needed.
} let frame = (ctx.engine.routines.layout_inline)(
} ctx.engine,
let mut frame = MathRun::new(fragments).into_frame(styles); &StyleVec::wrap(eco_vec![elem]),
let axis = scaled!(ctx, styles, axis_height); ctx.locator.next(&span),
frame.set_baseline(frame.height() / 2.0 + axis); styles,
FrameFragment::new(styles, frame).into() false,
} else { Size::splat(Abs::inf()),
layout_complex_text(&text, ctx, span, styles)?.into() 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<SymbolElem>,
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); ctx.push(fragment);
Ok(()) Ok(())
} }
/// Layout the given text string into a [`FrameFragment`]. /// Layout a [`GlyphFragment`].
fn layout_complex_text( fn layout_glyph(
text: &str, mut glyph: GlyphFragment,
dtls: bool,
ctx: &mut MathContext, ctx: &mut MathContext,
span: Span,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<FrameFragment> { ) -> MathFragment {
// There isn't a natural width for a paragraph in a math environment; if dtls {
// because it will be placed somewhere probably not at the left margin glyph.make_dotless_form(ctx);
// it will overflow. So emulate an `hbox` instead and allow the paragraph }
// to extend as far as needed. let math_size = EquationElem::size_in(styles);
let spaced = text.graphemes(true).nth(1).is_some(); match math_size {
let elem = TextElem::packed(text).spanned(span); MathSize::Script => glyph.make_script_size(ctx),
let frame = (ctx.engine.routines.layout_inline)( MathSize::ScriptScript => glyph.make_script_script_size(ctx),
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) if glyph.class == MathClass::Large {
.with_class(MathClass::Alphabetic) let mut variant = if math_size == MathSize::Display {
.with_text_like(true) let height = scaled!(ctx, styles, display_operator_min_height)
.with_spaced(spaced)) .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.
/// ///
/// <https://www.w3.org/TR/mathml-core/#new-text-transform-mappings> /// <https://www.w3.org/TR/mathml-core/#new-text-transform-mappings>
/// <https://en.wikipedia.org/wiki/Mathematical_Alphanumeric_Symbols> /// <https://en.wikipedia.org/wiki/Mathematical_Alphanumeric_Symbols>
@ -353,15 +398,12 @@ fn greek_exception(
}) })
} }
/// Switch dotless character to non dotless character for use of the dtls /// The non-dotless version of a dotless character that can be used with the
/// OpenType feature. /// `dtls` OpenType feature.
pub fn dtls_char(c: char, dtls: &mut bool) -> char { pub fn try_dotless(c: char) -> Option<char> {
match (c, *dtls) { match c {
('ı', true) => 'i', 'ı' => Some('i'),
('ȷ', true) => 'j', 'ȷ' => Some('j'),
_ => { _ => None,
*dtls = false;
c
}
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 B

After

Width:  |  Height:  |  Size: 159 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1009 B

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 992 B

After

Width:  |  Height:  |  Size: 989 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 976 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1009 B

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -85,7 +85,7 @@
[With ] [With ]
vars vars
.pairs() .pairs()
.map(((name, value)) => $name = value$) .map(((name, value)) => $#symbol(name) = value$)
.join(", ", last: " and ") .join(", ", last: " and ")
[ we have:] [ we have:]
$ equation = result $ $ equation = result $

View File

@ -4,10 +4,10 @@
// Test alignment step functions. // Test alignment step functions.
#set page(width: 225pt) #set page(width: 225pt)
$ $
"a" &= c \ a &= c \
&= c + 1 & "By definition" \ &= c + 1 & "By definition" \
&= d + 100 + 1000 \ &= d + 100 + 1000 \
&= x && "Even longer" \ &= x && "Even longer" \
$ $
--- math-align-post-fix --- --- math-align-post-fix ---

View File

@ -41,8 +41,8 @@ $floor(x/2), ceil(x/2), abs(x), norm(x)$
--- math-lr-color --- --- math-lr-color ---
// Test colored delimiters // Test colored delimiters
$ lr( $ lr(
text("(", fill: #green) a/b text(\(, fill: #green) a/b
text(")", fill: #blue) text(\), fill: #blue)
) $ ) $
--- math-lr-mid --- --- math-lr-mid ---

View File

@ -63,8 +63,8 @@ $ ext(bar.v) quad ext(bar.v.double) quad
// Test stretch when base is given with shorthand. // 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)$
$stretch(->, size: #2em)$ $stretch(->, size: #2em)$
$stretch(, size: #2em)$ $stretch(, size: #2em)$