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>() {
self::text::layout_text(elem, ctx, styles)?;
} else if let Some(elem) = elem.to_packed::<SymbolElem>() {
// 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::<BoxElem>() {
layout_box(elem, ctx, styles)?;
} else if elem.is::<AlignPointElem>() {

View File

@ -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<Item = &'a str>,
span: Span,
ctx: &mut MathContext,
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 {
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<FrameFragment> {
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<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);
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<FrameFragment> {
// 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.
///
/// <https://www.w3.org/TR/mathml-core/#new-text-transform-mappings>
/// <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
/// 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<char> {
match c {
'ı' => Some('i'),
'ȷ' => Some('j'),
_ => None,
}
}

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 ]
vars
.pairs()
.map(((name, value)) => $name = value$)
.map(((name, value)) => $#symbol(name) = value$)
.join(", ", last: " and ")
[ we have:]
$ equation = result $

View File

@ -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 ---

View File

@ -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 ---

View File

@ -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"$
}