Update math TextElem layout to separate out SymbolElem
@ -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>() {
|
||||||
|
@ -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();
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
match math_size {
|
|
||||||
MathSize::Script => {
|
|
||||||
glyph.make_script_size(ctx);
|
|
||||||
}
|
|
||||||
MathSize::ScriptScript => {
|
|
||||||
glyph.make_script_script_size(ctx);
|
|
||||||
}
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
} else {
|
||||||
glyph.into_variant()
|
layout_inline_text(text, span, ctx, styles)?
|
||||||
};
|
};
|
||||||
// TeXbook p 155. Large operators are always vertically centered on the axis.
|
ctx.push(fragment);
|
||||||
variant.center_on_axis(ctx);
|
Ok(())
|
||||||
variant.into()
|
}
|
||||||
} else {
|
|
||||||
glyph.into()
|
/// Layout multiple lines of text.
|
||||||
}
|
fn layout_text_lines<'a>(
|
||||||
} else if text.chars().all(|c| c.is_ascii_digit() || c == '.') {
|
lines: impl Iterator<Item = &'a str>,
|
||||||
// Numbers aren't that difficult.
|
span: Span,
|
||||||
|
ctx: &mut MathContext,
|
||||||
|
styles: StyleChain,
|
||||||
|
) -> SourceResult<FrameFragment> {
|
||||||
let mut fragments = vec![];
|
let mut fragments = vec![];
|
||||||
for c in text.chars() {
|
for (i, line) in lines.enumerate() {
|
||||||
let c = styled_char(styles, c, false);
|
if i != 0 {
|
||||||
fragments.push(GlyphFragment::new(ctx, styles, c, span).into());
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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![];
|
||||||
|
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);
|
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,46 +89,17 @@ 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![];
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ctx.push(fragment);
|
let spaced = styled_text.graphemes(true).nth(1).is_some();
|
||||||
Ok(())
|
let elem = TextElem::packed(styled_text).spanned(span);
|
||||||
}
|
|
||||||
|
|
||||||
/// Layout the given text string into a [`FrameFragment`].
|
|
||||||
fn layout_complex_text(
|
|
||||||
text: &str,
|
|
||||||
ctx: &mut MathContext,
|
|
||||||
span: Span,
|
|
||||||
styles: StyleChain,
|
|
||||||
) -> SourceResult<FrameFragment> {
|
|
||||||
// There isn't a natural width for a paragraph in a math environment;
|
// 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
|
// because it will be placed somewhere probably not at the left margin
|
||||||
// it will overflow. So emulate an `hbox` instead and allow the paragraph
|
// it will overflow. So emulate an `hbox` instead and allow the
|
||||||
// to extend as far as needed.
|
// 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)(
|
let frame = (ctx.engine.routines.layout_inline)(
|
||||||
ctx.engine,
|
ctx.engine,
|
||||||
&StyleVec::wrap(eco_vec![elem]),
|
&StyleVec::wrap(eco_vec![elem]),
|
||||||
@ -132,9 +115,71 @@ fn layout_complex_text(
|
|||||||
.with_class(MathClass::Alphabetic)
|
.with_class(MathClass::Alphabetic)
|
||||||
.with_text_like(true)
|
.with_text_like(true)
|
||||||
.with_spaced(spaced))
|
.with_spaced(spaced))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Select the correct styled math letter.
|
/// 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 a [`GlyphFragment`].
|
||||||
|
fn layout_glyph(
|
||||||
|
mut glyph: GlyphFragment,
|
||||||
|
dtls: bool,
|
||||||
|
ctx: &mut MathContext,
|
||||||
|
styles: StyleChain,
|
||||||
|
) -> 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),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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 ]
|
[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 $
|
||||||
|
@ -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 ---
|
||||||
|
@ -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 ---
|
||||||
|
@ -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)$
|
||||||
|
|
||||||
|