Merge c3fbfba13848239d70f92d014ef4ac133e5d2ba8 into e9f1b5825a9d37ca0c173a7b2830ba36a27ca9e0

This commit is contained in:
Max 2025-07-24 21:29:04 +09:00 committed by GitHub
commit 9b316a1359
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
42 changed files with 493 additions and 266 deletions

View File

@ -1,11 +1,10 @@
use typst_library::diag::SourceResult; use typst_library::diag::SourceResult;
use typst_library::foundations::{Packed, StyleChain}; use typst_library::foundations::{Packed, StyleChain, SymbolElem};
use typst_library::layout::{Em, Frame, Point, Size}; use typst_library::layout::{Em, Frame, Point, Size};
use typst_library::math::AccentElem; use typst_library::math::AccentElem;
use super::{ use super::{
FrameFragment, GlyphFragment, MathContext, MathFragment, style_cramped, style_dtls, FrameFragment, MathContext, MathFragment, style_cramped, style_dtls, style_flac,
style_flac,
}; };
/// How much the accent can be shorter than the base. /// How much the accent can be shorter than the base.
@ -27,14 +26,17 @@ pub fn layout_accent(
if top_accent && elem.dotless.get(styles) { styles.chain(&dtls) } else { styles }; if top_accent && elem.dotless.get(styles) { styles.chain(&dtls) } else { styles };
let cramped = style_cramped(); let cramped = style_cramped();
let base = ctx.layout_into_fragment(&elem.base, base_styles.chain(&cramped))?; let base_styles = base_styles.chain(&cramped);
let base = ctx.layout_into_fragment(&elem.base, base_styles)?;
let (font, size) = base.font(ctx, base_styles, elem.base.span())?;
// Preserve class to preserve automatic spacing. // Preserve class to preserve automatic spacing.
let base_class = base.class(); let base_class = base.class();
let base_attach = base.accent_attach(); let base_attach = base.accent_attach();
// Try to replace the accent glyph with its flattened variant. // Try to replace the accent glyph with its flattened variant.
let flattened_base_height = scaled!(ctx, styles, flattened_accent_base_height); let flattened_base_height = value!(font, flattened_accent_base_height).at(size);
let flac = style_flac(); let flac = style_flac();
let accent_styles = if top_accent && base.ascent() > flattened_base_height { let accent_styles = if top_accent && base.ascent() > flattened_base_height {
styles.chain(&flac) styles.chain(&flac)
@ -42,23 +44,25 @@ pub fn layout_accent(
styles styles
}; };
let mut glyph = let mut accent = ctx.layout_into_fragment(
GlyphFragment::new_char(ctx.font, accent_styles, accent.0, elem.span())?; &SymbolElem::packed(accent.0).spanned(elem.span()),
accent_styles,
)?;
// Forcing the accent to be at least as large as the base makes it too wide // Forcing the accent to be at least as large as the base makes it too wide
// in many cases. // in many cases.
let width = elem.size.resolve(styles).relative_to(base.width()); let width = elem.size.resolve(styles).relative_to(base.width());
let short_fall = ACCENT_SHORT_FALL.at(glyph.item.size); let short_fall = ACCENT_SHORT_FALL.at(size);
glyph.stretch_horizontal(ctx, width - short_fall); accent.stretch_horizontal(ctx, width - short_fall);
let accent_attach = glyph.accent_attach.0; let accent_attach = accent.accent_attach().0;
let accent = glyph.into_frame(); let accent = accent.into_frame();
let (gap, accent_pos, base_pos) = if top_accent { let (gap, accent_pos, base_pos) = if top_accent {
// Descent is negative because the accent's ink bottom is above the // Descent is negative because the accent's ink bottom is above the
// baseline. Therefore, the default gap is the accent's negated descent // baseline. Therefore, the default gap is the accent's negated descent
// minus the accent base height. Only if the base is very small, we // minus the accent base height. Only if the base is very small, we
// need a larger gap so that the accent doesn't move too low. // need a larger gap so that the accent doesn't move too low.
let accent_base_height = scaled!(ctx, styles, accent_base_height); let accent_base_height = value!(font, accent_base_height).at(size);
let gap = -accent.descent() - base.ascent().min(accent_base_height); let gap = -accent.descent() - base.ascent().min(accent_base_height);
let accent_pos = Point::with_x(base_attach.0 - accent_attach); let accent_pos = Point::with_x(base_attach.0 - accent_attach);
let base_pos = Point::with_y(accent.height() + gap); let base_pos = Point::with_y(accent.height() + gap);

View File

@ -4,6 +4,8 @@ use typst_library::layout::{Abs, Axis, Corner, Frame, Point, Rel, Size};
use typst_library::math::{ use typst_library::math::{
AttachElem, EquationElem, LimitsElem, PrimesElem, ScriptsElem, StretchElem, AttachElem, EquationElem, LimitsElem, PrimesElem, ScriptsElem, StretchElem,
}; };
use typst_library::text::Font;
use typst_syntax::Span;
use typst_utils::OptionExt; use typst_utils::OptionExt;
use super::{ use super::{
@ -83,7 +85,7 @@ pub fn layout_attach(
layout!(br, sub_style_chain)?, layout!(br, sub_style_chain)?,
]; ];
layout_attachments(ctx, styles, base, fragments) layout_attachments(ctx, styles, base, elem.base.span(), fragments)
} }
/// Lays out a [`PrimeElem`]. /// Lays out a [`PrimeElem`].
@ -102,13 +104,19 @@ pub fn layout_primes(
4 => '⁗', 4 => '⁗',
_ => unreachable!(), _ => unreachable!(),
}; };
let f = ctx.layout_into_fragment(&SymbolElem::packed(c), styles)?; let f = ctx.layout_into_fragment(
&SymbolElem::packed(c).spanned(elem.span()),
styles,
)?;
ctx.push(f); ctx.push(f);
} }
count => { count => {
// Custom amount of primes // Custom amount of primes
let prime = ctx let prime = ctx
.layout_into_fragment(&SymbolElem::packed(''), styles)? .layout_into_fragment(
&SymbolElem::packed('').spanned(elem.span()),
styles,
)?
.into_frame(); .into_frame();
let width = prime.width() * (count + 1) as f64 / 2.0; let width = prime.width() * (count + 1) as f64 / 2.0;
let mut frame = Frame::soft(Size::new(width, prime.height())); let mut frame = Frame::soft(Size::new(width, prime.height()));
@ -170,22 +178,25 @@ fn layout_attachments(
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain, styles: StyleChain,
base: MathFragment, base: MathFragment,
span: Span,
[tl, t, tr, bl, b, br]: [Option<MathFragment>; 6], [tl, t, tr, bl, b, br]: [Option<MathFragment>; 6],
) -> SourceResult<()> { ) -> SourceResult<()> {
let base_class = base.class(); let class = base.class();
let (font, size) = base.font(ctx, styles, span)?;
let cramped = styles.get(EquationElem::cramped);
// Calculate the distance from the base's baseline to the superscripts' and // Calculate the distance from the base's baseline to the superscripts' and
// subscripts' baseline. // subscripts' baseline.
let (tx_shift, bx_shift) = if [&tl, &tr, &bl, &br].iter().all(|e| e.is_none()) { let (tx_shift, bx_shift) = if [&tl, &tr, &bl, &br].iter().all(|e| e.is_none()) {
(Abs::zero(), Abs::zero()) (Abs::zero(), Abs::zero())
} else { } else {
compute_script_shifts(ctx, styles, &base, [&tl, &tr, &bl, &br]) compute_script_shifts(&font, size, cramped, &base, [&tl, &tr, &bl, &br])
}; };
// Calculate the distance from the base's baseline to the top attachment's // Calculate the distance from the base's baseline to the top attachment's
// and bottom attachment's baseline. // and bottom attachment's baseline.
let (t_shift, b_shift) = let (t_shift, b_shift) =
compute_limit_shifts(ctx, styles, &base, [t.as_ref(), b.as_ref()]); compute_limit_shifts(&font, size, &base, [t.as_ref(), b.as_ref()]);
// Calculate the final frame height. // Calculate the final frame height.
let ascent = base let ascent = base
@ -215,7 +226,7 @@ fn layout_attachments(
// `space_after_script` is extra spacing that is at the start before each // `space_after_script` is extra spacing that is at the start before each
// pre-script, and at the end after each post-script (see the MathConstants // pre-script, and at the end after each post-script (see the MathConstants
// table in the OpenType MATH spec). // table in the OpenType MATH spec).
let space_after_script = scaled!(ctx, styles, space_after_script); let space_after_script = value!(font, space_after_script).at(size);
// Calculate the distance each pre-script extends to the left of the base's // Calculate the distance each pre-script extends to the left of the base's
// width. // width.
@ -272,7 +283,7 @@ fn layout_attachments(
layout!(b, b_x, b_y); // lower-limit layout!(b, b_x, b_y); // lower-limit
// Done! Note that we retain the class of the base. // Done! Note that we retain the class of the base.
ctx.push(FrameFragment::new(styles, frame).with_class(base_class)); ctx.push(FrameFragment::new(styles, frame).with_class(class));
Ok(()) Ok(())
} }
@ -364,8 +375,8 @@ fn compute_limit_widths(
/// Returns two lengths, the first being the distance to the upper-limit's /// Returns two lengths, the first being the distance to the upper-limit's
/// baseline and the second being the distance to the lower-limit's baseline. /// baseline and the second being the distance to the lower-limit's baseline.
fn compute_limit_shifts( fn compute_limit_shifts(
ctx: &MathContext, font: &Font,
styles: StyleChain, font_size: Abs,
base: &MathFragment, base: &MathFragment,
[t, b]: [Option<&MathFragment>; 2], [t, b]: [Option<&MathFragment>; 2],
) -> (Abs, Abs) { ) -> (Abs, Abs) {
@ -373,16 +384,15 @@ fn compute_limit_shifts(
// ascender of the limits respectively, whereas `upper_rise_min` and // ascender of the limits respectively, whereas `upper_rise_min` and
// `lower_drop_min` give gaps to each limit's baseline (see the // `lower_drop_min` give gaps to each limit's baseline (see the
// MathConstants table in the OpenType MATH spec). // MathConstants table in the OpenType MATH spec).
let t_shift = t.map_or_default(|t| { let t_shift = t.map_or_default(|t| {
let upper_gap_min = scaled!(ctx, styles, upper_limit_gap_min); let upper_gap_min = value!(font, upper_limit_gap_min).at(font_size);
let upper_rise_min = scaled!(ctx, styles, upper_limit_baseline_rise_min); let upper_rise_min = value!(font, upper_limit_baseline_rise_min).at(font_size);
base.ascent() + upper_rise_min.max(upper_gap_min + t.descent()) base.ascent() + upper_rise_min.max(upper_gap_min + t.descent())
}); });
let b_shift = b.map_or_default(|b| { let b_shift = b.map_or_default(|b| {
let lower_gap_min = scaled!(ctx, styles, lower_limit_gap_min); let lower_gap_min = value!(font, lower_limit_gap_min).at(font_size);
let lower_drop_min = scaled!(ctx, styles, lower_limit_baseline_drop_min); let lower_drop_min = value!(font, lower_limit_baseline_drop_min).at(font_size);
base.descent() + lower_drop_min.max(lower_gap_min + b.ascent()) base.descent() + lower_drop_min.max(lower_gap_min + b.ascent())
}); });
@ -393,25 +403,27 @@ fn compute_limit_shifts(
/// Returns two lengths, the first being the distance to the superscripts' /// Returns two lengths, the first being the distance to the superscripts'
/// baseline and the second being the distance to the subscripts' baseline. /// baseline and the second being the distance to the subscripts' baseline.
fn compute_script_shifts( fn compute_script_shifts(
ctx: &MathContext, font: &Font,
styles: StyleChain, font_size: Abs,
cramped: bool,
base: &MathFragment, base: &MathFragment,
[tl, tr, bl, br]: [&Option<MathFragment>; 4], [tl, tr, bl, br]: [&Option<MathFragment>; 4],
) -> (Abs, Abs) { ) -> (Abs, Abs) {
let sup_shift_up = if styles.get(EquationElem::cramped) { let sup_shift_up = (if cramped {
scaled!(ctx, styles, superscript_shift_up_cramped) value!(font, superscript_shift_up_cramped)
} else { } else {
scaled!(ctx, styles, superscript_shift_up) value!(font, superscript_shift_up)
}; })
.at(font_size);
let sup_bottom_min = scaled!(ctx, styles, superscript_bottom_min); let sup_bottom_min = value!(font, superscript_bottom_min).at(font_size);
let sup_bottom_max_with_sub = let sup_bottom_max_with_sub =
scaled!(ctx, styles, superscript_bottom_max_with_subscript); value!(font, superscript_bottom_max_with_subscript).at(font_size);
let sup_drop_max = scaled!(ctx, styles, superscript_baseline_drop_max); let sup_drop_max = value!(font, superscript_baseline_drop_max).at(font_size);
let gap_min = scaled!(ctx, styles, sub_superscript_gap_min); let gap_min = value!(font, sub_superscript_gap_min).at(font_size);
let sub_shift_down = scaled!(ctx, styles, subscript_shift_down); let sub_shift_down = value!(font, subscript_shift_down).at(font_size);
let sub_top_max = scaled!(ctx, styles, subscript_top_max); let sub_top_max = value!(font, subscript_top_max).at(font_size);
let sub_drop_min = scaled!(ctx, styles, subscript_baseline_drop_min); let sub_drop_min = value!(font, subscript_baseline_drop_min).at(font_size);
let mut shift_up = Abs::zero(); let mut shift_up = Abs::zero();
let mut shift_down = Abs::zero(); let mut shift_down = Abs::zero();

View File

@ -7,7 +7,7 @@ use typst_library::visualize::{FixedStroke, Geometry};
use typst_syntax::Span; use typst_syntax::Span;
use super::{ use super::{
DELIM_SHORT_FALL, FrameFragment, GlyphFragment, MathContext, style_for_denominator, DELIM_SHORT_FALL, FrameFragment, MathContext, find_math_font, style_for_denominator,
style_for_numerator, style_for_numerator,
}; };
@ -49,29 +49,33 @@ fn layout_frac_like(
binom: bool, binom: bool,
span: Span, span: Span,
) -> SourceResult<()> { ) -> SourceResult<()> {
let short_fall = DELIM_SHORT_FALL.resolve(styles); let font = find_math_font(ctx.engine.world, styles, span)?;
let axis = scaled!(ctx, styles, axis_height); let axis = value!(font, axis_height).resolve(styles);
let thickness = scaled!(ctx, styles, fraction_rule_thickness); let thickness = value!(font, fraction_rule_thickness).resolve(styles);
let shift_up = scaled!( let shift_up = value!(
ctx, styles, font, styles,
text: fraction_numerator_shift_up, text: fraction_numerator_shift_up,
display: fraction_numerator_display_style_shift_up, display: fraction_numerator_display_style_shift_up,
); )
let shift_down = scaled!( .resolve(styles);
ctx, styles, let shift_down = value!(
font, styles,
text: fraction_denominator_shift_down, text: fraction_denominator_shift_down,
display: fraction_denominator_display_style_shift_down, display: fraction_denominator_display_style_shift_down,
); )
let num_min = scaled!( .resolve(styles);
ctx, styles, let num_min = value!(
font, styles,
text: fraction_numerator_gap_min, text: fraction_numerator_gap_min,
display: fraction_num_display_style_gap_min, display: fraction_num_display_style_gap_min,
); )
let denom_min = scaled!( .resolve(styles);
ctx, styles, let denom_min = value!(
font, styles,
text: fraction_denominator_gap_min, text: fraction_denominator_gap_min,
display: fraction_denom_display_style_gap_min, display: fraction_denom_display_style_gap_min,
); )
.resolve(styles);
let num_style = style_for_numerator(styles); let num_style = style_for_numerator(styles);
let num = ctx.layout_into_frame(num, styles.chain(&num_style))?; let num = ctx.layout_into_frame(num, styles.chain(&num_style))?;
@ -82,7 +86,7 @@ fn layout_frac_like(
// Add a comma between each element. // Add a comma between each element.
denom denom
.iter() .iter()
.flat_map(|a| [SymbolElem::packed(','), a.clone()]) .flat_map(|a| [SymbolElem::packed(',').spanned(span), a.clone()])
.skip(1), .skip(1),
), ),
styles.chain(&denom_style), styles.chain(&denom_style),
@ -109,12 +113,18 @@ fn layout_frac_like(
frame.push_frame(denom_pos, denom); frame.push_frame(denom_pos, denom);
if binom { if binom {
let mut left = GlyphFragment::new_char(ctx.font, styles, '(', span)?; let short_fall = DELIM_SHORT_FALL.resolve(styles);
let mut left =
ctx.layout_into_fragment(&SymbolElem::packed('(').spanned(span), styles)?;
left.stretch_vertical(ctx, height - short_fall); left.stretch_vertical(ctx, height - short_fall);
left.center_on_axis(); left.center_on_axis();
ctx.push(left); ctx.push(left);
ctx.push(FrameFragment::new(styles, frame)); ctx.push(FrameFragment::new(styles, frame));
let mut right = GlyphFragment::new_char(ctx.font, styles, ')', span)?;
let mut right =
ctx.layout_into_fragment(&SymbolElem::packed(')').spanned(span), styles)?;
right.stretch_vertical(ctx, height - short_fall); right.stretch_vertical(ctx, height - short_fall);
right.center_on_axis(); right.center_on_axis();
ctx.push(right); ctx.push(right);

View File

@ -4,6 +4,7 @@ use az::SaturatingAs;
use rustybuzz::{BufferFlags, UnicodeBuffer}; use rustybuzz::{BufferFlags, UnicodeBuffer};
use ttf_parser::GlyphId; use ttf_parser::GlyphId;
use ttf_parser::math::{GlyphAssembly, GlyphConstruction, GlyphPart}; use ttf_parser::math::{GlyphAssembly, GlyphConstruction, GlyphPart};
use typst_library::World;
use typst_library::diag::{SourceResult, bail, warning}; use typst_library::diag::{SourceResult, bail, warning};
use typst_library::foundations::StyleChain; use typst_library::foundations::StyleChain;
use typst_library::introspection::Tag; use typst_library::introspection::Tag;
@ -11,12 +12,15 @@ use typst_library::layout::{
Abs, Axes, Axis, Corner, Em, Frame, FrameItem, Point, Size, VAlignment, Abs, Axes, Axis, Corner, Em, Frame, FrameItem, Point, Size, VAlignment,
}; };
use typst_library::math::{EquationElem, MathSize}; use typst_library::math::{EquationElem, MathSize};
use typst_library::text::{Font, Glyph, TextElem, TextItem, features, language}; use typst_library::text::{
Font, Glyph, TextElem, TextItem, families, features, language, variant,
};
use typst_library::visualize::Paint;
use typst_syntax::Span; use typst_syntax::Span;
use typst_utils::{Get, default_math_class}; use typst_utils::{Get, default_math_class};
use unicode_math_class::MathClass; use unicode_math_class::MathClass;
use super::MathContext; use super::{MathContext, find_math_font};
use crate::inline::create_shape_plan; use crate::inline::create_shape_plan;
use crate::modifiers::{FrameModifiers, FrameModify}; use crate::modifiers::{FrameModifiers, FrameModify};
@ -108,6 +112,21 @@ impl MathFragment {
} }
} }
pub fn font(
&self,
ctx: &MathContext,
styles: StyleChain,
span: Span,
) -> SourceResult<(Font, Abs)> {
Ok((
match self {
Self::Glyph(glyph) => glyph.item.font.clone(),
_ => find_math_font(ctx.engine.world, styles, span)?,
},
self.font_size().unwrap_or_else(|| styles.resolve(TextElem::size)),
))
}
pub fn font_size(&self) -> Option<Abs> { pub fn font_size(&self) -> Option<Abs> {
match self { match self {
Self::Glyph(glyph) => Some(glyph.item.size), Self::Glyph(glyph) => Some(glyph.item.size),
@ -192,6 +211,31 @@ impl MathFragment {
} }
} }
pub fn fill(&self) -> Option<Paint> {
match self {
Self::Glyph(glyph) => Some(glyph.item.fill.clone()),
_ => None,
}
}
pub fn stretch_vertical(&mut self, ctx: &mut MathContext, height: Abs) {
if let Self::Glyph(glyph) = self {
glyph.stretch_vertical(ctx, height)
}
}
pub fn stretch_horizontal(&mut self, ctx: &mut MathContext, width: Abs) {
if let Self::Glyph(glyph) = self {
glyph.stretch_horizontal(ctx, width)
}
}
pub fn center_on_axis(&mut self) {
if let Self::Glyph(glyph) = self {
glyph.center_on_axis()
}
}
/// If no kern table is provided for a corner, a kerning amount of zero is /// If no kern table is provided for a corner, a kerning amount of zero is
/// assumed. /// assumed.
pub fn kern_at_height(&self, corner: Corner, height: Abs) -> Abs { pub fn kern_at_height(&self, corner: Corner, height: Abs) -> Abs {
@ -261,23 +305,70 @@ pub struct GlyphFragment {
impl GlyphFragment { impl GlyphFragment {
/// Calls `new` with the given character. /// Calls `new` with the given character.
pub fn new_char( pub fn new_char(
font: &Font, ctx: &MathContext,
styles: StyleChain, styles: StyleChain,
c: char, c: char,
span: Span, span: Span,
) -> SourceResult<Self> { ) -> SourceResult<Option<Self>> {
Self::new(font, styles, c.encode_utf8(&mut [0; 4]), span) Self::new(ctx, styles, c.encode_utf8(&mut [0; 4]), span)
}
/// Selects a font to use and then shapes text.
pub fn new(
ctx: &MathContext,
styles: StyleChain,
text: &str,
span: Span,
) -> SourceResult<Option<Self>> {
let families = families(styles);
let variant = variant(styles);
let fallback = styles.get(TextElem::fallback);
let end = text.char_indices().nth(1).map(|(i, _)| i).unwrap_or(text.len());
// Find the next available family.
let world = ctx.engine.world;
let book = world.book();
let mut selection = None;
for family in families {
selection = book
.select(family.as_str(), variant)
.and_then(|id| world.font(id))
.filter(|font| {
font.ttf().tables().math.and_then(|math| math.constants).is_some()
})
.filter(|_| family.covers().is_none_or(|cov| cov.is_match(&text[..end])));
if selection.is_some() {
break;
}
}
// Do font fallback if the families are exhausted and fallback is enabled.
if selection.is_none() && fallback {
selection = book
.select_fallback(None, variant, text)
.and_then(|id| world.font(id))
.filter(|font| {
font.ttf().tables().math.and_then(|math| math.constants).is_some()
});
}
// Error out if no math font could be found at all.
let Some(font) = selection else {
bail!(span, "current font does not support math");
};
Self::shape(&font, styles, text, span)
} }
/// Try to create a new glyph out of the given string. Will bail if the /// Try to create a new glyph out of the given string. Will bail if the
/// result from shaping the string is not a single glyph or is a tofu. /// result from shaping the string is more than a single glyph.
#[comemo::memoize] #[comemo::memoize]
pub fn new( pub fn shape(
font: &Font, font: &Font,
styles: StyleChain, styles: StyleChain,
text: &str, text: &str,
span: Span, span: Span,
) -> SourceResult<GlyphFragment> { ) -> SourceResult<Option<GlyphFragment>> {
let mut buffer = UnicodeBuffer::new(); let mut buffer = UnicodeBuffer::new();
buffer.push_str(text); buffer.push_str(text);
buffer.set_language(language(styles)); buffer.set_language(language(styles));
@ -300,18 +391,15 @@ impl GlyphFragment {
); );
let buffer = rustybuzz::shape_with_plan(font.rusty(), &plan, buffer); let buffer = rustybuzz::shape_with_plan(font.rusty(), &plan, buffer);
if buffer.len() != 1 { match buffer.len() {
bail!(span, "did not get a single glyph after shaping {}", text); 0 => return Ok(None),
1 => {}
_ => bail!(span, "did not get a single glyph after shaping {}", text),
} }
let info = buffer.glyph_infos()[0]; let info = buffer.glyph_infos()[0];
let pos = buffer.glyph_positions()[0]; let pos = buffer.glyph_positions()[0];
// TODO: add support for coverage and fallback, like in normal text shaping.
if info.glyph_id == 0 {
bail!(span, "current font is missing a glyph for {}", text);
}
let cluster = info.cluster as usize; let cluster = info.cluster as usize;
let c = text[cluster..].chars().next().unwrap(); let c = text[cluster..].chars().next().unwrap();
let limits = Limits::for_char(c); let limits = Limits::for_char(c);
@ -361,7 +449,7 @@ impl GlyphFragment {
modifiers: FrameModifiers::get_in(styles), modifiers: FrameModifiers::get_in(styles),
}; };
fragment.update_glyph(); fragment.update_glyph();
Ok(fragment) Ok(Some(fragment))
} }
/// Sets element id and boxes in appropriate way without changing other /// Sets element id and boxes in appropriate way without changing other

View File

@ -33,12 +33,13 @@ pub fn layout_lr(
let (start_idx, end_idx) = fragments.split_prefix_suffix(|f| f.is_ignorant()); let (start_idx, end_idx) = fragments.split_prefix_suffix(|f| f.is_ignorant());
let inner_fragments = &mut fragments[start_idx..end_idx]; let inner_fragments = &mut fragments[start_idx..end_idx];
let axis = scaled!(ctx, styles, axis_height); let mut max_extent = Abs::zero();
let max_extent = inner_fragments for fragment in inner_fragments.iter() {
.iter() let (font, size) = fragment.font(ctx, styles, elem.span())?;
.map(|fragment| (fragment.ascent() - axis).max(fragment.descent() + axis)) let axis = value!(font, axis_height).at(size);
.max() let extent = (fragment.ascent() - axis).max(fragment.descent() + axis);
.unwrap_or_default(); max_extent = max_extent.max(extent);
}
let relative_to = 2.0 * max_extent; let relative_to = 2.0 * max_extent;
let height = elem.size.resolve(styles); let height = elem.size.resolve(styles);

View File

@ -1,5 +1,5 @@
use typst_library::diag::{SourceResult, bail, warning}; use typst_library::diag::{SourceResult, bail, warning};
use typst_library::foundations::{Content, Packed, Resolve, StyleChain}; use typst_library::foundations::{Content, Packed, Resolve, StyleChain, SymbolElem};
use typst_library::layout::{ use typst_library::layout::{
Abs, Axes, Em, FixedAlignment, Frame, FrameItem, Point, Ratio, Rel, Size, Abs, Axes, Em, FixedAlignment, Frame, FrameItem, Point, Ratio, Rel, Size,
}; };
@ -10,7 +10,7 @@ use typst_syntax::Span;
use super::{ use super::{
AlignmentResult, DELIM_SHORT_FALL, FrameFragment, GlyphFragment, LeftRightAlternator, AlignmentResult, DELIM_SHORT_FALL, FrameFragment, GlyphFragment, LeftRightAlternator,
MathContext, alignments, style_for_denominator, MathContext, alignments, find_math_font, style_for_denominator,
}; };
const VERTICAL_PADDING: Ratio = Ratio::new(0.1); const VERTICAL_PADDING: Ratio = Ratio::new(0.1);
@ -186,12 +186,10 @@ fn layout_body(
// We pad ascent and descent with the ascent and descent of the paren // We pad ascent and descent with the ascent and descent of the paren
// to ensure that normal matrices are aligned with others unless they are // to ensure that normal matrices are aligned with others unless they are
// way too big. // way too big.
let paren = GlyphFragment::new_char( // This will never panic as a paren will never shape into nothing.
ctx.font, let paren =
styles.chain(&denom_style), GlyphFragment::new_char(ctx, styles.chain(&denom_style), '(', Span::detached())?
'(', .unwrap();
Span::detached(),
)?;
for (column, col) in columns.iter().zip(&mut cols) { for (column, col) in columns.iter().zip(&mut cols) {
for (cell, (ascent, descent)) in column.iter().zip(&mut heights) { for (cell, (ascent, descent)) in column.iter().zip(&mut heights) {
@ -314,13 +312,15 @@ fn layout_delimiters(
span: Span, span: Span,
) -> SourceResult<()> { ) -> SourceResult<()> {
let short_fall = DELIM_SHORT_FALL.resolve(styles); let short_fall = DELIM_SHORT_FALL.resolve(styles);
let axis = scaled!(ctx, styles, axis_height); let font = find_math_font(ctx.engine.world, styles, span)?;
let axis = value!(font, axis_height).resolve(styles);
let height = frame.height(); let height = frame.height();
let target = height + VERTICAL_PADDING.of(height); let target = height + VERTICAL_PADDING.of(height);
frame.set_baseline(height / 2.0 + axis); frame.set_baseline(height / 2.0 + axis);
if let Some(left_c) = left { if let Some(left_c) = left {
let mut left = GlyphFragment::new_char(ctx.font, styles, left_c, span)?; let mut left =
ctx.layout_into_fragment(&SymbolElem::packed(left_c).spanned(span), styles)?;
left.stretch_vertical(ctx, target - short_fall); left.stretch_vertical(ctx, target - short_fall);
left.center_on_axis(); left.center_on_axis();
ctx.push(left); ctx.push(left);
@ -329,7 +329,8 @@ fn layout_delimiters(
ctx.push(FrameFragment::new(styles, frame)); ctx.push(FrameFragment::new(styles, frame));
if let Some(right_c) = right { if let Some(right_c) = right {
let mut right = GlyphFragment::new_char(ctx.font, styles, right_c, span)?; let mut right =
ctx.layout_into_fragment(&SymbolElem::packed(right_c).spanned(span), styles)?;
right.stretch_vertical(ctx, target - short_fall); right.stretch_vertical(ctx, target - short_fall);
right.center_on_axis(); right.center_on_axis();
ctx.push(right); ctx.push(right);

View File

@ -13,8 +13,7 @@ mod stretch;
mod text; mod text;
mod underover; mod underover;
use typst_library::World; use typst_library::diag::SourceResult;
use typst_library::diag::{SourceResult, bail};
use typst_library::engine::Engine; use typst_library::engine::Engine;
use typst_library::foundations::{ use typst_library::foundations::{
Content, NativeElement, Packed, Resolve, StyleChain, SymbolElem, Content, NativeElement, Packed, Resolve, StyleChain, SymbolElem,
@ -28,10 +27,7 @@ use typst_library::layout::{
use typst_library::math::*; use typst_library::math::*;
use typst_library::model::ParElem; use typst_library::model::ParElem;
use typst_library::routines::{Arenas, RealizationKind}; use typst_library::routines::{Arenas, RealizationKind};
use typst_library::text::{ use typst_library::text::{LinebreakElem, RawElem, SpaceElem, TextEdgeBounds, TextElem};
Font, LinebreakElem, SpaceElem, TextEdgeBounds, TextElem, families, variant,
};
use typst_syntax::Span;
use typst_utils::Numeric; use typst_utils::Numeric;
use unicode_math_class::MathClass; use unicode_math_class::MathClass;
@ -53,12 +49,11 @@ pub fn layout_equation_inline(
) -> SourceResult<Vec<InlineItem>> { ) -> SourceResult<Vec<InlineItem>> {
assert!(!elem.block.get(styles)); assert!(!elem.block.get(styles));
let font = find_math_font(engine, styles, elem.span())?;
let mut locator = locator.split(); let mut locator = locator.split();
let mut ctx = MathContext::new(engine, &mut locator, region, &font); let mut ctx = MathContext::new(engine, &mut locator, region);
let scale_style = style_for_script_scale(&ctx); let font = find_math_font(ctx.engine.world, styles, elem.span())?;
let scale_style = style_for_script_scale(&font);
let styles = styles.chain(&scale_style); let styles = styles.chain(&scale_style);
let run = ctx.layout_into_run(&elem.body, styles)?; let run = ctx.layout_into_run(&elem.body, styles)?;
@ -108,12 +103,12 @@ pub fn layout_equation_block(
assert!(elem.block.get(styles)); assert!(elem.block.get(styles));
let span = elem.span(); let span = elem.span();
let font = find_math_font(engine, styles, span)?;
let mut locator = locator.split(); let mut locator = locator.split();
let mut ctx = MathContext::new(engine, &mut locator, regions.base(), &font); let mut ctx = MathContext::new(engine, &mut locator, regions.base());
let scale_style = style_for_script_scale(&ctx); let font = find_math_font(ctx.engine.world, styles, elem.span())?;
let scale_style = style_for_script_scale(&font);
let styles = styles.chain(&scale_style); let styles = styles.chain(&scale_style);
let full_equation_builder = ctx let full_equation_builder = ctx
@ -234,24 +229,6 @@ pub fn layout_equation_block(
Ok(Fragment::frames(frames)) Ok(Fragment::frames(frames))
} }
fn find_math_font(
engine: &mut Engine<'_>,
styles: StyleChain,
span: Span,
) -> SourceResult<Font> {
let variant = variant(styles);
let world = engine.world;
let Some(font) = families(styles).find_map(|family| {
let id = world.book().select(family.as_str(), variant)?;
let font = world.font(id)?;
let _ = font.ttf().tables().math?.constants?;
Some(font)
}) else {
bail!(span, "current font does not support math");
};
Ok(font)
}
fn add_equation_number( fn add_equation_number(
equation_builder: MathRunFrameBuilder, equation_builder: MathRunFrameBuilder,
number: Frame, number: Frame,
@ -370,9 +347,6 @@ struct MathContext<'a, 'v, 'e> {
engine: &'v mut Engine<'e>, engine: &'v mut Engine<'e>,
locator: &'v mut SplitLocator<'a>, locator: &'v mut SplitLocator<'a>,
region: Region, region: Region,
// Font-related.
font: &'a Font,
constants: ttf_parser::math::Constants<'a>,
// Mutable. // Mutable.
fragments: Vec<MathFragment>, fragments: Vec<MathFragment>,
} }
@ -383,19 +357,11 @@ impl<'a, 'v, 'e> MathContext<'a, 'v, 'e> {
engine: &'v mut Engine<'e>, engine: &'v mut Engine<'e>,
locator: &'v mut SplitLocator<'a>, locator: &'v mut SplitLocator<'a>,
base: Size, base: Size,
font: &'a Font,
) -> Self { ) -> Self {
// These unwraps are safe as the font given is one returned by the
// find_math_font function, which only returns fonts that have a math
// constants table.
let constants = font.ttf().tables().math.unwrap().constants.unwrap();
Self { Self {
engine, engine,
locator, locator,
region: Region::new(base, Axes::splat(false)), region: Region::new(base, Axes::splat(false)),
font,
constants,
fragments: vec![], fragments: vec![],
} }
} }
@ -469,17 +435,7 @@ impl<'a, 'v, 'e> MathContext<'a, 'v, 'e> {
styles, styles,
)?; )?;
let outer = styles;
for (elem, styles) in pairs { for (elem, styles) in pairs {
// Hack because the font is fixed in math.
if styles != outer
&& styles.get_ref(TextElem::font) != outer.get_ref(TextElem::font)
{
let frame = layout_external(elem, self, styles)?;
self.push(FrameFragment::new(styles, frame).with_spaced(true));
continue;
}
layout_realized(elem, self, styles)?; layout_realized(elem, self, styles)?;
} }
@ -496,7 +452,10 @@ fn layout_realized(
if let Some(elem) = elem.to_packed::<TagElem>() { if let Some(elem) = elem.to_packed::<TagElem>() {
ctx.push(MathFragment::Tag(elem.tag.clone())); ctx.push(MathFragment::Tag(elem.tag.clone()));
} else if elem.is::<SpaceElem>() { } else if elem.is::<SpaceElem>() {
let space_width = ctx.font.space_width().unwrap_or(THICK); let space_width = find_math_font(ctx.engine.world, styles, elem.span())
.ok()
.and_then(|font| font.space_width())
.unwrap_or(THICK);
ctx.push(MathFragment::Space(space_width.resolve(styles))); ctx.push(MathFragment::Space(space_width.resolve(styles)));
} else if elem.is::<LinebreakElem>() { } else if elem.is::<LinebreakElem>() {
ctx.push(MathFragment::Linebreak); ctx.push(MathFragment::Linebreak);
@ -566,9 +525,11 @@ fn layout_realized(
self::underover::layout_overshell(elem, ctx, styles)? self::underover::layout_overshell(elem, ctx, styles)?
} else { } else {
let mut frame = layout_external(elem, ctx, styles)?; let mut frame = layout_external(elem, ctx, styles)?;
if !frame.has_baseline() { if !frame.has_baseline() && !elem.is::<RawElem>() {
let axis = scaled!(ctx, styles, axis_height); if let Ok(font) = find_math_font(ctx.engine.world, styles, elem.span()) {
frame.set_baseline(frame.height() / 2.0 + axis); let axis = value!(font, axis_height).resolve(styles);
frame.set_baseline(frame.height() / 2.0 + axis);
}
} }
ctx.push( ctx.push(
FrameFragment::new(styles, frame) FrameFragment::new(styles, frame)

View File

@ -1,11 +1,11 @@
use typst_library::diag::SourceResult; use typst_library::diag::SourceResult;
use typst_library::foundations::{Packed, StyleChain}; use typst_library::foundations::{Packed, StyleChain, SymbolElem};
use typst_library::layout::{Abs, Frame, FrameItem, Point, Size}; use typst_library::layout::{Abs, Frame, FrameItem, Point, Size};
use typst_library::math::{EquationElem, MathSize, RootElem}; use typst_library::math::{EquationElem, MathSize, RootElem};
use typst_library::text::TextElem; use typst_library::text::TextElem;
use typst_library::visualize::{FixedStroke, Geometry}; use typst_library::visualize::{FixedStroke, Geometry};
use super::{FrameFragment, GlyphFragment, MathContext, style_cramped}; use super::{FrameFragment, MathContext, style_cramped};
/// Lays out a [`RootElem`]. /// Lays out a [`RootElem`].
/// ///
@ -17,45 +17,62 @@ pub fn layout_root(
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
let index = elem.index.get_ref(styles);
let span = elem.span(); let span = elem.span();
let gap = scaled!(
ctx, styles,
text: radical_vertical_gap,
display: radical_display_style_vertical_gap,
);
let thickness = scaled!(ctx, styles, radical_rule_thickness);
let extra_ascender = scaled!(ctx, styles, radical_extra_ascender);
let kern_before = scaled!(ctx, styles, radical_kern_before_degree);
let kern_after = scaled!(ctx, styles, radical_kern_after_degree);
let raise_factor = percent!(ctx, radical_degree_bottom_raise_percent);
// Layout radicand. // Layout radicand.
let radicand = { let radicand = {
let cramped = style_cramped(); let cramped = style_cramped();
let styles = styles.chain(&cramped); let styles = styles.chain(&cramped);
let run = ctx.layout_into_run(&elem.radicand, styles)?; let run = ctx.layout_into_run(&elem.radicand, styles)?;
let multiline = run.is_multiline(); let multiline = run.is_multiline();
let mut radicand = run.into_fragment(styles).into_frame(); let radicand = run.into_fragment(styles);
if multiline { if multiline {
// Align the frame center line with the math axis. // Align the frame center line with the math axis.
radicand.set_baseline( let (font, size) = radicand.font(ctx, styles, elem.radicand.span())?;
radicand.height() / 2.0 + scaled!(ctx, styles, axis_height), let axis = value!(font, axis_height).at(size);
); let mut radicand = radicand.into_frame();
radicand.set_baseline(radicand.height() / 2.0 + axis);
radicand
} else {
radicand.into_frame()
} }
radicand
}; };
// Layout root symbol. // Layout root symbol.
let mut sqrt =
ctx.layout_into_fragment(&SymbolElem::packed('√').spanned(span), styles)?;
let (font, size) = sqrt.font(ctx, styles, span)?;
let thickness = value!(font, radical_rule_thickness).at(size);
let extra_ascender = value!(font, radical_extra_ascender).at(size);
let kern_before = value!(font, radical_kern_before_degree).at(size);
let kern_after = value!(font, radical_kern_after_degree).at(size);
let raise_factor = percent!(font, radical_degree_bottom_raise_percent);
let gap = value!(
font, styles,
text: radical_vertical_gap,
display: radical_display_style_vertical_gap,
)
.at(size);
let line = FrameItem::Shape(
Geometry::Line(Point::with_x(radicand.width())).stroked(FixedStroke::from_pair(
sqrt.fill()
.unwrap_or_else(|| styles.get_ref(TextElem::fill).as_decoration()),
thickness,
)),
span,
);
let target = radicand.height() + thickness + gap; let target = radicand.height() + thickness + gap;
let mut sqrt = GlyphFragment::new_char(ctx.font, styles, '√', span)?;
sqrt.stretch_vertical(ctx, target); sqrt.stretch_vertical(ctx, target);
let sqrt = sqrt.into_frame(); let sqrt = sqrt.into_frame();
// Layout the index. // Layout the index.
let sscript = EquationElem::size.set(MathSize::ScriptScript).wrap(); let sscript = EquationElem::size.set(MathSize::ScriptScript).wrap();
let index = index let index = elem
.index
.get_ref(styles)
.as_ref() .as_ref()
.map(|elem| ctx.layout_into_frame(elem, styles.chain(&sscript))) .map(|elem| ctx.layout_into_frame(elem, styles.chain(&sscript)))
.transpose()?; .transpose()?;
@ -107,19 +124,7 @@ pub fn layout_root(
} }
frame.push_frame(sqrt_pos, sqrt); frame.push_frame(sqrt_pos, sqrt);
frame.push( frame.push(line_pos, line);
line_pos,
FrameItem::Shape(
Geometry::Line(Point::with_x(radicand.width())).stroked(
FixedStroke::from_pair(
styles.get_ref(TextElem::fill).as_decoration(),
thickness,
),
),
span,
),
);
frame.push_frame(radicand_pos, radicand); frame.push_frame(radicand_pos, radicand);
ctx.push(FrameFragment::new(styles, frame)); ctx.push(FrameFragment::new(styles, frame));

View File

@ -1,32 +1,47 @@
use comemo::Tracked;
use ttf_parser::Tag; use ttf_parser::Tag;
use ttf_parser::math::MathValue; use ttf_parser::math::MathValue;
use typst_library::World;
use typst_library::diag::{SourceResult, bail};
use typst_library::foundations::{Style, StyleChain}; use typst_library::foundations::{Style, StyleChain};
use typst_library::layout::{Abs, Em, FixedAlignment, Frame, Point, Size}; use typst_library::layout::{Abs, Em, FixedAlignment, Frame, Point, Size};
use typst_library::math::{EquationElem, MathSize}; use typst_library::math::{EquationElem, MathSize};
use typst_library::text::{FontFeatures, TextElem}; use typst_library::text::{Font, FontFeatures, TextElem, families, variant};
use typst_syntax::Span;
use typst_utils::LazyHash; use typst_utils::LazyHash;
use super::{LeftRightAlternator, MathContext, MathFragment, MathRun}; use super::{LeftRightAlternator, MathFragment, MathRun};
macro_rules! scaled { macro_rules! value {
($ctx:expr, $styles:expr, text: $text:ident, display: $display:ident $(,)?) => { ($font:expr, $styles:expr, text: $text:ident, display: $display:ident $(,)?) => {
match $styles.get(typst_library::math::EquationElem::size) { match $styles.get(typst_library::math::EquationElem::size) {
typst_library::math::MathSize::Display => scaled!($ctx, $styles, $display), typst_library::math::MathSize::Display => value!($font, $display),
_ => scaled!($ctx, $styles, $text), _ => value!($font, $text),
} }
}; };
($ctx:expr, $styles:expr, $name:ident) => { ($font:expr, $name:ident) => {
$crate::math::Scaled::scaled( $font
$ctx.constants.$name(), .ttf()
$ctx, .tables()
$styles.resolve(typst_library::text::TextElem::size), .math
) .and_then(|math| math.constants)
.map(|constants| {
crate::math::shared::Scaled::scaled(constants.$name(), &$font)
})
.unwrap()
}; };
} }
macro_rules! percent { macro_rules! percent {
($ctx:expr, $name:ident) => { ($font:expr, $name:ident) => {
$ctx.constants.$name() as f64 / 100.0 $font
.ttf()
.tables()
.math
.and_then(|math| math.constants)
.map(|constants| constants.$name())
.unwrap() as f64
/ 100.0
}; };
} }
@ -35,27 +50,47 @@ pub const DELIM_SHORT_FALL: Em = Em::new(0.1);
/// Converts some unit to an absolute length with the current font & font size. /// Converts some unit to an absolute length with the current font & font size.
pub trait Scaled { pub trait Scaled {
fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs; fn scaled(self, font: &Font) -> Em;
} }
impl Scaled for i16 { impl Scaled for i16 {
fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs { fn scaled(self, font: &Font) -> Em {
ctx.font.to_em(self).at(font_size) font.to_em(self)
} }
} }
impl Scaled for u16 { impl Scaled for u16 {
fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs { fn scaled(self, font: &Font) -> Em {
ctx.font.to_em(self).at(font_size) font.to_em(self)
} }
} }
impl Scaled for MathValue<'_> { impl Scaled for MathValue<'_> {
fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs { fn scaled(self, font: &Font) -> Em {
self.value.scaled(ctx, font_size) self.value.scaled(font)
} }
} }
/// Get the current math font.
#[comemo::memoize]
pub fn find_math_font(
world: Tracked<dyn World + '_>,
styles: StyleChain,
span: Span,
) -> SourceResult<Font> {
let variant = variant(styles);
let Some(font) = families(styles).find_map(|family| {
let id = world.book().select(family.as_str(), variant)?;
let font = world.font(id)?;
let _ = font.ttf().tables().math?.constants?;
// Take the base font as the "main" math font.
family.covers().map_or(Some(font), |_| None)
}) else {
bail!(span, "current font does not support math");
};
Ok(font)
}
/// Styles something as cramped. /// Styles something as cramped.
pub fn style_cramped() -> LazyHash<Style> { pub fn style_cramped() -> LazyHash<Style> {
EquationElem::cramped.set(true).wrap() EquationElem::cramped.set(true).wrap()
@ -107,11 +142,12 @@ pub fn style_for_denominator(styles: StyleChain) -> [LazyHash<Style>; 2] {
} }
/// Styles to add font constants to the style chain. /// Styles to add font constants to the style chain.
pub fn style_for_script_scale(ctx: &MathContext) -> LazyHash<Style> { pub fn style_for_script_scale(font: &Font) -> LazyHash<Style> {
let constants = font.ttf().tables().math.and_then(|math| math.constants).unwrap();
EquationElem::script_scale EquationElem::script_scale
.set(( .set((
ctx.constants.script_percent_scale_down(), constants.script_percent_scale_down(),
ctx.constants.script_script_percent_scale_down(), constants.script_script_percent_scale_down(),
)) ))
.wrap() .wrap()
} }

View File

@ -3,7 +3,7 @@ use std::f64::consts::SQRT_2;
use codex::styling::{MathStyle, to_style}; use codex::styling::{MathStyle, to_style};
use ecow::EcoString; use ecow::EcoString;
use typst_library::diag::SourceResult; use typst_library::diag::SourceResult;
use typst_library::foundations::{Packed, StyleChain, SymbolElem}; use typst_library::foundations::{Packed, Resolve, StyleChain, SymbolElem};
use typst_library::layout::{Abs, Size}; use typst_library::layout::{Abs, Size};
use typst_library::math::{EquationElem, MathSize}; use typst_library::math::{EquationElem, MathSize};
use typst_library::text::{ use typst_library::text::{
@ -14,8 +14,8 @@ use unicode_math_class::MathClass;
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
use super::{ use super::{
FrameFragment, GlyphFragment, MathContext, MathFragment, MathRun, has_dtls_feat, FrameFragment, GlyphFragment, MathContext, MathFragment, MathRun, find_math_font,
style_dtls, has_dtls_feat, style_dtls,
}; };
/// Lays out a [`TextElem`]. /// Lays out a [`TextElem`].
@ -52,7 +52,8 @@ fn layout_text_lines<'a>(
} }
} }
let mut frame = MathRun::new(fragments).into_frame(styles); let mut frame = MathRun::new(fragments).into_frame(styles);
let axis = scaled!(ctx, styles, axis_height); let font = find_math_font(ctx.engine.world, styles, span)?;
let axis = value!(font, axis_height).resolve(styles);
frame.set_baseline(frame.height() / 2.0 + axis); frame.set_baseline(frame.height() / 2.0 + axis);
Ok(FrameFragment::new(styles, frame)) Ok(FrameFragment::new(styles, frame))
} }
@ -80,7 +81,9 @@ fn layout_inline_text(
let style = MathStyle::select(unstyled_c, variant, bold, italic); let style = MathStyle::select(unstyled_c, variant, bold, italic);
let c = to_style(unstyled_c, style).next().unwrap(); let c = to_style(unstyled_c, style).next().unwrap();
let glyph = GlyphFragment::new_char(ctx.font, styles, c, span)?; // This won't panic as ASCII digits and '.' will never end up as
// nothing after shaping.
let glyph = GlyphFragment::new_char(ctx, styles, c, span)?.unwrap();
fragments.push(glyph.into()); fragments.push(glyph.into());
} }
let frame = MathRun::new(fragments).into_frame(styles); let frame = MathRun::new(fragments).into_frame(styles);
@ -132,8 +135,11 @@ pub fn layout_symbol(
// Switch dotless char to normal when we have the dtls OpenType feature. // Switch dotless char to normal when we have the dtls OpenType feature.
// This should happen before the main styling pass. // This should happen before the main styling pass.
let dtls = style_dtls(); let dtls = style_dtls();
let (unstyled_c, symbol_styles) = match try_dotless(elem.text) { let (unstyled_c, symbol_styles) = match (
Some(c) if has_dtls_feat(ctx.font) => (c, styles.chain(&dtls)), try_dotless(elem.text),
find_math_font(ctx.engine.world, styles, elem.span()),
) {
(Some(c), Ok(font)) if has_dtls_feat(&font) => (c, styles.chain(&dtls)),
_ => (elem.text, styles), _ => (elem.text, styles),
}; };
@ -144,39 +150,22 @@ pub fn layout_symbol(
let style = MathStyle::select(unstyled_c, variant, bold, italic); let style = MathStyle::select(unstyled_c, variant, bold, italic);
let text: EcoString = to_style(unstyled_c, style).collect(); let text: EcoString = to_style(unstyled_c, style).collect();
let fragment: MathFragment = if let Some(mut glyph) = GlyphFragment::new(ctx, symbol_styles, &text, elem.span())? {
match GlyphFragment::new(ctx.font, symbol_styles, &text, elem.span()) { if glyph.class == MathClass::Large {
Ok(mut glyph) => { if styles.get(EquationElem::size) == MathSize::Display {
adjust_glyph_layout(&mut glyph, ctx, styles); let height = value!(glyph.item.font, display_operator_min_height)
glyph.into() .at(glyph.item.size)
} .max(SQRT_2 * glyph.size.y);
Err(_) => { glyph.stretch_vertical(ctx, height);
// Not in the math font, fallback to normal inline text layout. };
// TODO: Should replace this with proper fallback in [`GlyphFragment::new`]. // TeXbook p 155. Large operators are always vertically centered on
layout_inline_text(&text, elem.span(), ctx, styles)?.into() // the axis.
} glyph.center_on_axis();
}; }
ctx.push(fragment); ctx.push(glyph);
Ok(())
}
/// Centers large glyphs vertically on the axis, scaling them if in display
/// style.
fn adjust_glyph_layout(
glyph: &mut GlyphFragment,
ctx: &mut MathContext,
styles: StyleChain,
) {
if glyph.class == MathClass::Large {
if styles.get(EquationElem::size) == MathSize::Display {
let height = scaled!(ctx, styles, display_operator_min_height)
.max(SQRT_2 * glyph.size.y);
glyph.stretch_vertical(ctx, height);
};
// TeXbook p 155. Large operators are always vertically centered on the
// axis.
glyph.center_on_axis();
} }
Ok(())
} }
/// The non-dotless version of a dotless character that can be used with the /// The non-dotless version of a dotless character that can be used with the

View File

@ -1,5 +1,5 @@
use typst_library::diag::SourceResult; use typst_library::diag::SourceResult;
use typst_library::foundations::{Content, Packed, Resolve, StyleChain}; use typst_library::foundations::{Content, Packed, Resolve, StyleChain, SymbolElem};
use typst_library::layout::{Abs, Em, FixedAlignment, Frame, FrameItem, Point, Size}; use typst_library::layout::{Abs, Em, FixedAlignment, Frame, FrameItem, Point, Size};
use typst_library::math::{ use typst_library::math::{
OverbraceElem, OverbracketElem, OverlineElem, OverparenElem, OvershellElem, OverbraceElem, OverbracketElem, OverlineElem, OverparenElem, OvershellElem,
@ -10,8 +10,8 @@ use typst_library::visualize::{FixedStroke, Geometry};
use typst_syntax::Span; use typst_syntax::Span;
use super::{ use super::{
FrameFragment, GlyphFragment, LeftRightAlternator, MathContext, MathRun, stack, FrameFragment, LeftRightAlternator, MathContext, MathRun, stack, style_cramped,
style_cramped, style_for_subscript, style_for_superscript, style_for_subscript, style_for_superscript,
}; };
const BRACE_GAP: Em = Em::new(0.25); const BRACE_GAP: Em = Em::new(0.25);
@ -208,26 +208,29 @@ fn layout_underoverline(
let (extra_height, content, line_pos, content_pos, baseline, bar_height, line_adjust); let (extra_height, content, line_pos, content_pos, baseline, bar_height, line_adjust);
match position { match position {
Position::Under => { Position::Under => {
let sep = scaled!(ctx, styles, underbar_extra_descender);
bar_height = scaled!(ctx, styles, underbar_rule_thickness);
let gap = scaled!(ctx, styles, underbar_vertical_gap);
extra_height = sep + bar_height + gap;
content = ctx.layout_into_fragment(body, styles)?; content = ctx.layout_into_fragment(body, styles)?;
let (font, size) = content.font(ctx, styles, span)?;
let sep = value!(font, underbar_extra_descender).at(size);
bar_height = value!(font, underbar_rule_thickness).at(size);
let gap = value!(font, underbar_vertical_gap).at(size);
extra_height = sep + bar_height + gap;
line_pos = Point::with_y(content.height() + gap + bar_height / 2.0); line_pos = Point::with_y(content.height() + gap + bar_height / 2.0);
content_pos = Point::zero(); content_pos = Point::zero();
baseline = content.ascent(); baseline = content.ascent();
line_adjust = -content.italics_correction(); line_adjust = -content.italics_correction();
} }
Position::Over => { Position::Over => {
let sep = scaled!(ctx, styles, overbar_extra_ascender);
bar_height = scaled!(ctx, styles, overbar_rule_thickness);
let gap = scaled!(ctx, styles, overbar_vertical_gap);
extra_height = sep + bar_height + gap;
let cramped = style_cramped(); let cramped = style_cramped();
content = ctx.layout_into_fragment(body, styles.chain(&cramped))?; let styles = styles.chain(&cramped);
content = ctx.layout_into_fragment(body, styles)?;
let (font, size) = content.font(ctx, styles, span)?;
let sep = value!(font, overbar_extra_ascender).at(size);
bar_height = value!(font, overbar_rule_thickness).at(size);
let gap = value!(font, overbar_vertical_gap).at(size);
extra_height = sep + bar_height + gap;
line_pos = Point::with_y(sep + bar_height / 2.0); line_pos = Point::with_y(sep + bar_height / 2.0);
content_pos = Point::with_y(extra_height); content_pos = Point::with_y(extra_height);
@ -285,7 +288,8 @@ fn layout_underoverspreader(
let body = ctx.layout_into_run(body, styles)?; let body = ctx.layout_into_run(body, styles)?;
let body_class = body.class(); let body_class = body.class();
let body = body.into_fragment(styles); let body = body.into_fragment(styles);
let mut glyph = GlyphFragment::new_char(ctx.font, styles, c, span)?; let mut glyph =
ctx.layout_into_fragment(&SymbolElem::packed(c).spanned(span), styles)?;
glyph.stretch_horizontal(ctx, body.width()); glyph.stretch_horizontal(ctx, body.width());
let mut rows = vec![]; let mut rows = vec![];

View File

@ -98,14 +98,11 @@ pub fn cal(
/// ```example /// ```example
/// #let scr(it) = text( /// #let scr(it) = text(
/// features: ("ss01",), /// features: ("ss01",),
/// box($cal(it)$), /// $cal(it)$,
/// ) /// )
/// ///
/// We establish $cal(P) != scr(P)$. /// We establish $cal(P) != scr(P)$.
/// ``` /// ```
///
/// (The box is not conceptually necessary, but unfortunately currently needed
/// due to limitations in Typst's text style handling in math.)
#[func(title = "Script Style", keywords = ["mathscr", "roundhand"])] #[func(title = "Script Style", keywords = ["mathscr", "roundhand"])]
pub fn scr( pub fn scr(
/// The content to style. /// The content to style.

View File

@ -936,6 +936,7 @@ pub fn families(styles: StyleChain<'_>) -> impl Iterator<Item = &'_ FontFamily>
"noto color emoji", "noto color emoji",
"apple color emoji", "apple color emoji",
"segoe ui emoji", "segoe ui emoji",
"new computer modern math",
] ]
.into_iter() .into_iter()
.map(FontFamily::new) .map(FontFamily::new)

View File

@ -28,7 +28,7 @@ use typst_library::model::{
ParElem, ParbreakElem, TermsElem, ParElem, ParbreakElem, TermsElem,
}; };
use typst_library::routines::{Arenas, FragmentKind, Pair, RealizationKind}; use typst_library::routines::{Arenas, FragmentKind, Pair, RealizationKind};
use typst_library::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem}; use typst_library::text::{LinebreakElem, RawElem, SmartQuoteElem, SpaceElem, TextElem};
use typst_syntax::Span; use typst_syntax::Span;
use typst_utils::{SliceExt, SmallBitSet}; use typst_utils::{SliceExt, SmallBitSet};
@ -286,6 +286,13 @@ fn visit_kind_rules<'a>(
styles: StyleChain<'a>, styles: StyleChain<'a>,
) -> SourceResult<bool> { ) -> SourceResult<bool> {
if let RealizationKind::Math = s.kind { if let RealizationKind::Math = s.kind {
// Deal with Raw later when it gets laid out externally, so that it
// renders correctly in math.
if content.is::<RawElem>() {
s.sink.push((content, styles));
return Ok(true);
}
// Transparently recurse into equations nested in math, so that things // Transparently recurse into equations nested in math, so that things
// like this work: // like this work:
// ``` // ```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 868 B

After

Width:  |  Height:  |  Size: 884 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 507 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 730 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 355 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 858 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 674 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 387 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 402 B

After

Width:  |  Height:  |  Size: 492 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 968 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 611 B

After

Width:  |  Height:  |  Size: 607 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
tests/ref/math-op-font.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 440 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 856 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 791 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 607 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 946 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 489 B

After

Width:  |  Height:  |  Size: 450 B

View File

@ -21,6 +21,85 @@ $ x := #table(columns: 2)[x][y]/mat(1, 2, 3)
#let here = text.with(font: "Noto Sans") #let here = text.with(font: "Noto Sans")
$#here[f] := #here[Hi there]$. $#here[f] := #here[Hi there]$.
--- math-root-show-rule-1 ---
#show "√": set text(red, font: "Noto Sans Math")
$ root(2, (a + b) / c) $
--- math-root-show-rule-2 ---
#show "√": set text(2em)
$ sqrt(2) $
--- math-root-show-rule-3 ---
// Test cursed show rule.
#show "√": "!"
$ sqrt(2) root(2, 2) $
--- math-root-show-rule-4 ---
#show math.root: set text(red)
$ sqrt(x + y) root(4, 2) $
--- math-root-show-rule-5 ---
#show math.root: it => {
show "√": set text(purple) if it.index == none
it
}
$ sqrt(1/2) root(3, 1/2) $
--- math-delim-show-rule-1 ---
#show regex("\[|\]"): set text(green, font: "Noto Sans Math")
$ mat(delim: \[, a, b, c; d, e, f; g, h, i) quad [x + y] $
--- math-delim-show-rule-2 ---
#show math.vec: it => {
show regex("\(|\)"): set text(blue)
it
}
$ vec(1, 0, 0), mat(1; 0; 0), (1), binom(n, k) $
--- math-delim-show-rule-3 ---
#show "⏟": set text(fuchsia)
$ underbrace(1 + 1 = 2, "obviously") $
--- math-delim-show-rule-4 ---
#show "{": set text(navy)
$ cases(x + y + z = 0, 2x - y = 0, -5y + 2z = 0) $
--- math-delim-show-rule-5 ---
#show regex("\(|\)"): set text(1.5em)
$ 10 dot (9 - 5) dot (1/2 - 1) $
--- math-primes-show-rule ---
#show math.primes: set text(maroon)
$f'(x), f''''''(x)$
--- math-glyph-show-rule ---
#show "+": set text(orange, font: "Noto Sans Math")
$ 1 + 1 = +2 $
#show "+": text(2em)[#sym.plus.circle]
$ 1 + 1 = +2 $
--- math-accent-show-rule-1 ---
#show "\u{0302}": set text(blue, font: "XITS Math")
$hat(x)$, $hat(hat(x))$, x\u{0302}
--- math-accent-show-rule-2 ---
#let rhat(x) = {
show "\u{0302}": set text(red)
math.hat(x)
}
$hat(x)$, $rhat(x)$, $hat(rhat(x))$, $rhat(hat(x))$, x\u{0302}
--- math-accent-show-rule-3 ---
#show math.accent: it => {
show "\u{0300}": set text(green)
it
}
$grave(x)$, x\u{0300}
--- math-accent-show-rule-4 ---
#show "\u{0302}": box(inset: (bottom: 5pt), text(0.5em, sym.diamond.small))
$hat(X)$, $hat(x)$
--- math-box-without-baseline --- --- math-box-without-baseline ---
// Test boxes without a baseline act as if the baseline is at the base // Test boxes without a baseline act as if the baseline is at the base
#{ #{

View File

@ -28,3 +28,20 @@ $ bold(op("bold", limits: #true))_x y $
--- math-non-math-content --- --- math-non-math-content ---
// With non-text content // With non-text content
$ op(#underline[ul]) a $ $ op(#underline[ul]) a $
--- math-op-font ---
// Test with different font.
#let colim = math.op(
text(font: "IBM Plex Sans", weight: "regular", size: 0.8em)[colim],
limits: true,
)
$ colim_(x -> 0) inline(colim_(x -> 0)) $
--- math-op-set-font ---
// Test setting font.
#show math.equation: set text(weight: "regular")
#let lig = math.op("fi")
#let test = $sech(x) mod_(x -> oo) lig_1(X)$
#test
#show math.op: set text(font: "New Computer Modern")
#test

View File

@ -2,7 +2,7 @@
--- math-font-fallback --- --- math-font-fallback ---
// Test font fallback. // Test font fallback.
$ and 🏳🌈 $ $ and "よ" and 🏳🌈 $
--- math-text-color --- --- math-text-color ---
// Test text properties. // Test text properties.
@ -17,6 +17,21 @@ $ nothing $
$ "hi ∅ hey" $ $ "hi ∅ hey" $
$ sum_(i in NN) 1 + i $ $ sum_(i in NN) 1 + i $
--- math-font-features-switch ---
#let scr(it) = text(features: ("ss01",), $cal(it)$)
$cal(P)_i != scr(P)_i$, $cal(bold(I))_l != bold(scr(I))_l$
$ product.co_(B in scr(B))^(B in scr(bold(B))) cal(B)(X) $
--- math-font-covers ---
#show math.equation: set text(
font: (
// Ignore that this regex actually misses some of the script glyphs...
(name: "XITS Math", covers: regex("[\u{1D49C}-\u{1D503}]")),
),
features: ("ss01",),
)
$ cal(P)_i (X) * cal(C)_1 $
--- math-optical-size-nested-scripts --- --- math-optical-size-nested-scripts ---
// Test transition from script to scriptscript. // Test transition from script to scriptscript.
#[ #[