Update math TextElem layout to separate out SymbolElem
@ -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>() {
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 160 B After Width: | Height: | Size: 159 B |
Before Width: | Height: | Size: 1009 B After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 992 B After Width: | Height: | Size: 989 B |
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 976 B |
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 1009 B After Width: | Height: | Size: 1.0 KiB |
@ -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 $
|
||||
|
@ -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 ---
|
||||
|
@ -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 ---
|
||||
|
@ -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"$
|
||||
}
|
||||
|