Support multiple fonts in math (#6365)
@ -1137,8 +1137,9 @@ fn calculate_adjustability(ctx: &mut ShapingContext, lang: Lang, region: Option<
|
||||
|
||||
/// Difference between non-breaking and normal space.
|
||||
fn nbsp_delta(font: &Font) -> Option<Em> {
|
||||
let space = font.ttf().glyph_index(' ')?.0;
|
||||
let nbsp = font.ttf().glyph_index('\u{00A0}')?.0;
|
||||
Some(font.x_advance(nbsp)? - font.space_width()?)
|
||||
Some(font.x_advance(nbsp)? - font.x_advance(space)?)
|
||||
}
|
||||
|
||||
/// Returns true if all glyphs in `glyphs` have ranges within the range `range`.
|
||||
|
@ -1,11 +1,10 @@
|
||||
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::math::AccentElem;
|
||||
|
||||
use super::{
|
||||
FrameFragment, GlyphFragment, MathContext, MathFragment, style_cramped, style_dtls,
|
||||
style_flac,
|
||||
FrameFragment, MathContext, MathFragment, style_cramped, style_dtls, style_flac,
|
||||
};
|
||||
|
||||
/// 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 };
|
||||
|
||||
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);
|
||||
|
||||
// Preserve class to preserve automatic spacing.
|
||||
let base_class = base.class();
|
||||
let base_attach = base.accent_attach();
|
||||
|
||||
// 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 = font.math().flattened_accent_base_height.at(size);
|
||||
let flac = style_flac();
|
||||
let accent_styles = if top_accent && base.ascent() > flattened_base_height {
|
||||
styles.chain(&flac)
|
||||
@ -42,23 +44,25 @@ pub fn layout_accent(
|
||||
styles
|
||||
};
|
||||
|
||||
let mut glyph =
|
||||
GlyphFragment::new_char(ctx.font, accent_styles, accent.0, elem.span())?;
|
||||
let mut accent = ctx.layout_into_fragment(
|
||||
&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
|
||||
// in many cases.
|
||||
let width = elem.size.resolve(styles).relative_to(base.width());
|
||||
let short_fall = ACCENT_SHORT_FALL.at(glyph.item.size);
|
||||
glyph.stretch_horizontal(ctx, width - short_fall);
|
||||
let accent_attach = glyph.accent_attach.0;
|
||||
let accent = glyph.into_frame();
|
||||
let short_fall = ACCENT_SHORT_FALL.at(size);
|
||||
accent.stretch_horizontal(ctx, width - short_fall);
|
||||
let accent_attach = accent.accent_attach().0;
|
||||
let accent = accent.into_frame();
|
||||
|
||||
let (gap, accent_pos, base_pos) = if top_accent {
|
||||
// Descent is negative because the accent's ink bottom is above the
|
||||
// baseline. Therefore, the default gap is the accent's negated descent
|
||||
// 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.
|
||||
let accent_base_height = scaled!(ctx, styles, accent_base_height);
|
||||
let accent_base_height = font.math().accent_base_height.at(size);
|
||||
let gap = -accent.descent() - base.ascent().min(accent_base_height);
|
||||
let accent_pos = Point::with_x(base_attach.0 - accent_attach);
|
||||
let base_pos = Point::with_y(accent.height() + gap);
|
||||
|
@ -4,6 +4,7 @@ use typst_library::layout::{Abs, Axis, Corner, Frame, Point, Rel, Size};
|
||||
use typst_library::math::{
|
||||
AttachElem, EquationElem, LimitsElem, PrimesElem, ScriptsElem, StretchElem,
|
||||
};
|
||||
use typst_library::text::Font;
|
||||
use typst_utils::OptionExt;
|
||||
|
||||
use super::{
|
||||
@ -102,13 +103,19 @@ pub fn layout_primes(
|
||||
4 => '⁗',
|
||||
_ => 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);
|
||||
}
|
||||
count => {
|
||||
// Custom amount of primes
|
||||
let prime = ctx
|
||||
.layout_into_fragment(&SymbolElem::packed('′'), styles)?
|
||||
.layout_into_fragment(
|
||||
&SymbolElem::packed('′').spanned(elem.span()),
|
||||
styles,
|
||||
)?
|
||||
.into_frame();
|
||||
let width = prime.width() * (count + 1) as f64 / 2.0;
|
||||
let mut frame = Frame::soft(Size::new(width, prime.height()));
|
||||
@ -172,20 +179,22 @@ fn layout_attachments(
|
||||
base: MathFragment,
|
||||
[tl, t, tr, bl, b, br]: [Option<MathFragment>; 6],
|
||||
) -> SourceResult<()> {
|
||||
let base_class = base.class();
|
||||
let class = base.class();
|
||||
let (font, size) = base.font(ctx, styles);
|
||||
let cramped = styles.get(EquationElem::cramped);
|
||||
|
||||
// Calculate the distance from the base's baseline to the superscripts' and
|
||||
// subscripts' baseline.
|
||||
let (tx_shift, bx_shift) = if [&tl, &tr, &bl, &br].iter().all(|e| e.is_none()) {
|
||||
(Abs::zero(), Abs::zero())
|
||||
} 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
|
||||
// and bottom attachment's baseline.
|
||||
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.
|
||||
let ascent = base
|
||||
@ -215,7 +224,7 @@ fn layout_attachments(
|
||||
// `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
|
||||
// table in the OpenType MATH spec).
|
||||
let space_after_script = scaled!(ctx, styles, space_after_script);
|
||||
let space_after_script = font.math().space_after_script.at(size);
|
||||
|
||||
// Calculate the distance each pre-script extends to the left of the base's
|
||||
// width.
|
||||
@ -272,7 +281,7 @@ fn layout_attachments(
|
||||
layout!(b, b_x, b_y); // lower-limit
|
||||
|
||||
// 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(())
|
||||
}
|
||||
@ -364,8 +373,8 @@ fn compute_limit_widths(
|
||||
/// 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.
|
||||
fn compute_limit_shifts(
|
||||
ctx: &MathContext,
|
||||
styles: StyleChain,
|
||||
font: &Font,
|
||||
font_size: Abs,
|
||||
base: &MathFragment,
|
||||
[t, b]: [Option<&MathFragment>; 2],
|
||||
) -> (Abs, Abs) {
|
||||
@ -373,16 +382,15 @@ fn compute_limit_shifts(
|
||||
// ascender of the limits respectively, whereas `upper_rise_min` and
|
||||
// `lower_drop_min` give gaps to each limit's baseline (see the
|
||||
// MathConstants table in the OpenType MATH spec).
|
||||
|
||||
let t_shift = t.map_or_default(|t| {
|
||||
let upper_gap_min = scaled!(ctx, styles, upper_limit_gap_min);
|
||||
let upper_rise_min = scaled!(ctx, styles, upper_limit_baseline_rise_min);
|
||||
let upper_gap_min = font.math().upper_limit_gap_min.at(font_size);
|
||||
let upper_rise_min = font.math().upper_limit_baseline_rise_min.at(font_size);
|
||||
base.ascent() + upper_rise_min.max(upper_gap_min + t.descent())
|
||||
});
|
||||
|
||||
let b_shift = b.map_or_default(|b| {
|
||||
let lower_gap_min = scaled!(ctx, styles, lower_limit_gap_min);
|
||||
let lower_drop_min = scaled!(ctx, styles, lower_limit_baseline_drop_min);
|
||||
let lower_gap_min = font.math().lower_limit_gap_min.at(font_size);
|
||||
let lower_drop_min = font.math().lower_limit_baseline_drop_min.at(font_size);
|
||||
base.descent() + lower_drop_min.max(lower_gap_min + b.ascent())
|
||||
});
|
||||
|
||||
@ -393,25 +401,27 @@ fn compute_limit_shifts(
|
||||
/// Returns two lengths, the first being the distance to the superscripts'
|
||||
/// baseline and the second being the distance to the subscripts' baseline.
|
||||
fn compute_script_shifts(
|
||||
ctx: &MathContext,
|
||||
styles: StyleChain,
|
||||
font: &Font,
|
||||
font_size: Abs,
|
||||
cramped: bool,
|
||||
base: &MathFragment,
|
||||
[tl, tr, bl, br]: [&Option<MathFragment>; 4],
|
||||
) -> (Abs, Abs) {
|
||||
let sup_shift_up = if styles.get(EquationElem::cramped) {
|
||||
scaled!(ctx, styles, superscript_shift_up_cramped)
|
||||
let sup_shift_up = (if cramped {
|
||||
font.math().superscript_shift_up_cramped
|
||||
} else {
|
||||
scaled!(ctx, styles, superscript_shift_up)
|
||||
};
|
||||
font.math().superscript_shift_up
|
||||
})
|
||||
.at(font_size);
|
||||
|
||||
let sup_bottom_min = scaled!(ctx, styles, superscript_bottom_min);
|
||||
let sup_bottom_min = font.math().superscript_bottom_min.at(font_size);
|
||||
let sup_bottom_max_with_sub =
|
||||
scaled!(ctx, styles, superscript_bottom_max_with_subscript);
|
||||
let sup_drop_max = scaled!(ctx, styles, superscript_baseline_drop_max);
|
||||
let gap_min = scaled!(ctx, styles, sub_superscript_gap_min);
|
||||
let sub_shift_down = scaled!(ctx, styles, subscript_shift_down);
|
||||
let sub_top_max = scaled!(ctx, styles, subscript_top_max);
|
||||
let sub_drop_min = scaled!(ctx, styles, subscript_baseline_drop_min);
|
||||
font.math().superscript_bottom_max_with_subscript.at(font_size);
|
||||
let sup_drop_max = font.math().superscript_baseline_drop_max.at(font_size);
|
||||
let gap_min = font.math().sub_superscript_gap_min.at(font_size);
|
||||
let sub_shift_down = font.math().subscript_shift_down.at(font_size);
|
||||
let sub_top_max = font.math().subscript_top_max.at(font_size);
|
||||
let sub_drop_min = font.math().subscript_baseline_drop_min.at(font_size);
|
||||
|
||||
let mut shift_up = Abs::zero();
|
||||
let mut shift_down = Abs::zero();
|
||||
|
@ -1,13 +1,13 @@
|
||||
use typst_library::diag::SourceResult;
|
||||
use typst_library::foundations::{Content, Packed, Resolve, StyleChain, SymbolElem};
|
||||
use typst_library::layout::{Em, Frame, FrameItem, Point, Size};
|
||||
use typst_library::math::{BinomElem, FracElem};
|
||||
use typst_library::math::{BinomElem, EquationElem, FracElem, MathSize};
|
||||
use typst_library::text::TextElem;
|
||||
use typst_library::visualize::{FixedStroke, Geometry};
|
||||
use typst_syntax::Span;
|
||||
|
||||
use super::{
|
||||
DELIM_SHORT_FALL, FrameFragment, GlyphFragment, MathContext, style_for_denominator,
|
||||
DELIM_SHORT_FALL, FrameFragment, MathContext, style_for_denominator,
|
||||
style_for_numerator,
|
||||
};
|
||||
|
||||
@ -49,29 +49,30 @@ fn layout_frac_like(
|
||||
binom: bool,
|
||||
span: Span,
|
||||
) -> SourceResult<()> {
|
||||
let short_fall = DELIM_SHORT_FALL.resolve(styles);
|
||||
let axis = scaled!(ctx, styles, axis_height);
|
||||
let thickness = scaled!(ctx, styles, fraction_rule_thickness);
|
||||
let shift_up = scaled!(
|
||||
ctx, styles,
|
||||
text: fraction_numerator_shift_up,
|
||||
display: fraction_numerator_display_style_shift_up,
|
||||
);
|
||||
let shift_down = scaled!(
|
||||
ctx, styles,
|
||||
text: fraction_denominator_shift_down,
|
||||
display: fraction_denominator_display_style_shift_down,
|
||||
);
|
||||
let num_min = scaled!(
|
||||
ctx, styles,
|
||||
text: fraction_numerator_gap_min,
|
||||
display: fraction_num_display_style_gap_min,
|
||||
);
|
||||
let denom_min = scaled!(
|
||||
ctx, styles,
|
||||
text: fraction_denominator_gap_min,
|
||||
display: fraction_denom_display_style_gap_min,
|
||||
);
|
||||
let constants = ctx.font().math();
|
||||
let axis = constants.axis_height.resolve(styles);
|
||||
let thickness = constants.fraction_rule_thickness.resolve(styles);
|
||||
let size = styles.get(EquationElem::size);
|
||||
let shift_up = match size {
|
||||
MathSize::Display => constants.fraction_numerator_display_style_shift_up,
|
||||
_ => constants.fraction_numerator_shift_up,
|
||||
}
|
||||
.resolve(styles);
|
||||
let shift_down = match size {
|
||||
MathSize::Display => constants.fraction_denominator_display_style_shift_down,
|
||||
_ => constants.fraction_denominator_shift_down,
|
||||
}
|
||||
.resolve(styles);
|
||||
let num_min = match size {
|
||||
MathSize::Display => constants.fraction_num_display_style_gap_min,
|
||||
_ => constants.fraction_numerator_gap_min,
|
||||
}
|
||||
.resolve(styles);
|
||||
let denom_min = match size {
|
||||
MathSize::Display => constants.fraction_denom_display_style_gap_min,
|
||||
_ => constants.fraction_denominator_gap_min,
|
||||
}
|
||||
.resolve(styles);
|
||||
|
||||
let num_style = style_for_numerator(styles);
|
||||
let num = ctx.layout_into_frame(num, styles.chain(&num_style))?;
|
||||
@ -82,7 +83,7 @@ fn layout_frac_like(
|
||||
// Add a comma between each element.
|
||||
denom
|
||||
.iter()
|
||||
.flat_map(|a| [SymbolElem::packed(','), a.clone()])
|
||||
.flat_map(|a| [SymbolElem::packed(',').spanned(span), a.clone()])
|
||||
.skip(1),
|
||||
),
|
||||
styles.chain(&denom_style),
|
||||
@ -109,12 +110,18 @@ fn layout_frac_like(
|
||||
frame.push_frame(denom_pos, denom);
|
||||
|
||||
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.center_on_axis();
|
||||
ctx.push(left);
|
||||
|
||||
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.center_on_axis();
|
||||
ctx.push(right);
|
||||
|
@ -1,22 +1,28 @@
|
||||
use std::fmt::{self, Debug, Formatter};
|
||||
|
||||
use az::SaturatingAs;
|
||||
use comemo::Tracked;
|
||||
use rustybuzz::{BufferFlags, UnicodeBuffer};
|
||||
use ttf_parser::GlyphId;
|
||||
use ttf_parser::math::{GlyphAssembly, GlyphConstruction, GlyphPart};
|
||||
use typst_library::diag::{SourceResult, bail, warning};
|
||||
use typst_library::World;
|
||||
use typst_library::diag::warning;
|
||||
use typst_library::foundations::StyleChain;
|
||||
use typst_library::introspection::Tag;
|
||||
use typst_library::layout::{
|
||||
Abs, Axes, Axis, Corner, Em, Frame, FrameItem, Point, Size, VAlignment,
|
||||
};
|
||||
use typst_library::math::{EquationElem, MathSize};
|
||||
use typst_library::text::{Font, Glyph, TextElem, TextItem, features, language};
|
||||
use typst_library::text::{
|
||||
Font, FontFamily, FontVariant, Glyph, TextElem, TextItem, features, language, variant,
|
||||
};
|
||||
use typst_library::visualize::Paint;
|
||||
use typst_syntax::Span;
|
||||
use typst_utils::{Get, default_math_class};
|
||||
use unicode_math_class::MathClass;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
use super::MathContext;
|
||||
use super::{MathContext, families};
|
||||
use crate::inline::create_shape_plan;
|
||||
use crate::modifiers::{FrameModifiers, FrameModify};
|
||||
|
||||
@ -108,6 +114,17 @@ impl MathFragment {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn font(&self, ctx: &MathContext, styles: StyleChain) -> (Font, Abs) {
|
||||
(
|
||||
match self {
|
||||
Self::Glyph(glyph) => glyph.item.font.clone(),
|
||||
_ => ctx.font().clone(),
|
||||
},
|
||||
self.font_size().unwrap_or_else(|| styles.resolve(TextElem::size)),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn font_size(&self) -> Option<Abs> {
|
||||
match self {
|
||||
Self::Glyph(glyph) => Some(glyph.item.size),
|
||||
@ -192,6 +209,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
|
||||
/// assumed.
|
||||
pub fn kern_at_height(&self, corner: Corner, height: Abs) -> Abs {
|
||||
@ -261,77 +303,43 @@ pub struct GlyphFragment {
|
||||
impl GlyphFragment {
|
||||
/// Calls `new` with the given character.
|
||||
pub fn new_char(
|
||||
font: &Font,
|
||||
ctx: &MathContext,
|
||||
styles: StyleChain,
|
||||
c: char,
|
||||
span: Span,
|
||||
) -> SourceResult<Self> {
|
||||
Self::new(font, styles, c.encode_utf8(&mut [0; 4]), span)
|
||||
) -> Option<Self> {
|
||||
Self::new(ctx.engine.world, styles, c.encode_utf8(&mut [0; 4]), span)
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// Selects a font to use and then shapes text.
|
||||
#[comemo::memoize]
|
||||
pub fn new(
|
||||
font: &Font,
|
||||
world: Tracked<dyn World + '_>,
|
||||
styles: StyleChain,
|
||||
text: &str,
|
||||
span: Span,
|
||||
) -> SourceResult<GlyphFragment> {
|
||||
let mut buffer = UnicodeBuffer::new();
|
||||
buffer.push_str(text);
|
||||
buffer.set_language(language(styles));
|
||||
// TODO: Use `rustybuzz::script::MATH` once
|
||||
// https://github.com/harfbuzz/rustybuzz/pull/165 is released.
|
||||
buffer.set_script(
|
||||
rustybuzz::Script::from_iso15924_tag(ttf_parser::Tag::from_bytes(b"math"))
|
||||
.unwrap(),
|
||||
);
|
||||
buffer.set_direction(rustybuzz::Direction::LeftToRight);
|
||||
buffer.set_flags(BufferFlags::REMOVE_DEFAULT_IGNORABLES);
|
||||
) -> Option<GlyphFragment> {
|
||||
assert!(text.graphemes(true).count() == 1);
|
||||
|
||||
let features = features(styles);
|
||||
let plan = create_shape_plan(
|
||||
font,
|
||||
buffer.direction(),
|
||||
buffer.script(),
|
||||
buffer.language().as_ref(),
|
||||
&features,
|
||||
);
|
||||
let (c, font, mut glyph) = shape(
|
||||
world,
|
||||
variant(styles),
|
||||
features(styles),
|
||||
language(styles),
|
||||
styles.get(TextElem::fallback),
|
||||
text,
|
||||
families(styles).collect(),
|
||||
)?;
|
||||
glyph.span.0 = span;
|
||||
|
||||
let buffer = rustybuzz::shape_with_plan(font.rusty(), &plan, buffer);
|
||||
if buffer.len() != 1 {
|
||||
bail!(span, "did not get a single glyph after shaping {}", text);
|
||||
}
|
||||
|
||||
let info = buffer.glyph_infos()[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 c = text[cluster..].chars().next().unwrap();
|
||||
let limits = Limits::for_char(c);
|
||||
let class = styles
|
||||
.get(EquationElem::class)
|
||||
.or_else(|| default_math_class(c))
|
||||
.unwrap_or(MathClass::Normal);
|
||||
|
||||
let glyph = Glyph {
|
||||
id: info.glyph_id as u16,
|
||||
x_advance: font.to_em(pos.x_advance),
|
||||
x_offset: font.to_em(pos.x_offset),
|
||||
y_advance: font.to_em(pos.y_advance),
|
||||
y_offset: font.to_em(pos.y_offset),
|
||||
range: 0..text.len().saturating_as(),
|
||||
span: (span, 0),
|
||||
};
|
||||
|
||||
let item = TextItem {
|
||||
font: font.clone(),
|
||||
font,
|
||||
size: styles.resolve(TextElem::size),
|
||||
fill: styles.get_ref(TextElem::fill).as_decoration(),
|
||||
stroke: styles.resolve(TextElem::stroke).map(|s| s.unwrap_or_default()),
|
||||
@ -361,7 +369,7 @@ impl GlyphFragment {
|
||||
modifiers: FrameModifiers::get_in(styles),
|
||||
};
|
||||
fragment.update_glyph();
|
||||
Ok(fragment)
|
||||
Some(fragment)
|
||||
}
|
||||
|
||||
/// Sets element id and boxes in appropriate way without changing other
|
||||
@ -506,7 +514,7 @@ impl GlyphFragment {
|
||||
/// to the given alignment on the axis.
|
||||
pub fn align_on_axis(&mut self, align: VAlignment) {
|
||||
let h = self.size.y;
|
||||
let axis = axis_height(&self.item.font).unwrap().at(self.item.size);
|
||||
let axis = self.item.font.math().axis_height.at(self.item.size);
|
||||
self.align += self.baseline();
|
||||
self.baseline = Some(align.inv().position(h + axis * 2.0));
|
||||
self.align -= self.baseline();
|
||||
@ -649,10 +657,6 @@ fn kern_at_height(font: &Font, id: GlyphId, corner: Corner, height: Em) -> Optio
|
||||
Some(font.to_em(kern.kern(i)?.value))
|
||||
}
|
||||
|
||||
fn axis_height(font: &Font) -> Option<Em> {
|
||||
Some(font.to_em(font.ttf().tables().math?.constants?.axis_height().value))
|
||||
}
|
||||
|
||||
pub fn stretch_axes(font: &Font, id: u16) -> Axes<bool> {
|
||||
let id = GlyphId(id);
|
||||
let horizontal = font
|
||||
@ -834,6 +838,153 @@ pub fn has_dtls_feat(font: &Font) -> bool {
|
||||
.is_some()
|
||||
}
|
||||
|
||||
#[comemo::memoize]
|
||||
fn shape(
|
||||
world: Tracked<dyn World + '_>,
|
||||
variant: FontVariant,
|
||||
features: Vec<rustybuzz::Feature>,
|
||||
language: rustybuzz::Language,
|
||||
fallback: bool,
|
||||
text: &str,
|
||||
families: Vec<&FontFamily>,
|
||||
) -> Option<(char, Font, Glyph)> {
|
||||
let mut used = vec![];
|
||||
let buffer = UnicodeBuffer::new();
|
||||
shape_glyph(
|
||||
world,
|
||||
&mut used,
|
||||
buffer,
|
||||
variant,
|
||||
features,
|
||||
language,
|
||||
fallback,
|
||||
text,
|
||||
families.into_iter(),
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn shape_glyph<'a>(
|
||||
world: Tracked<'a, dyn World + 'a>,
|
||||
used: &mut Vec<Font>,
|
||||
mut buffer: rustybuzz::UnicodeBuffer,
|
||||
variant: FontVariant,
|
||||
features: Vec<rustybuzz::Feature>,
|
||||
language: rustybuzz::Language,
|
||||
fallback: bool,
|
||||
text: &str,
|
||||
mut families: impl Iterator<Item = &'a FontFamily> + Clone,
|
||||
) -> Option<(char, Font, Glyph)> {
|
||||
// Find the next available family.
|
||||
let book = world.book();
|
||||
let mut selection = None;
|
||||
let mut covers = None;
|
||||
for family in families.by_ref() {
|
||||
selection = book
|
||||
.select(family.as_str(), variant)
|
||||
.and_then(|id| world.font(id))
|
||||
.filter(|font| !used.contains(font));
|
||||
if selection.is_some() {
|
||||
covers = family.covers();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Do font fallback if the families are exhausted and fallback is enabled.
|
||||
if selection.is_none() && fallback {
|
||||
let first = used.first().map(Font::info);
|
||||
selection = book
|
||||
.select_fallback(first, variant, text)
|
||||
.and_then(|id| world.font(id))
|
||||
.filter(|font| !used.contains(font))
|
||||
}
|
||||
|
||||
// Extract the font id or shape notdef glyphs if we couldn't find any font.
|
||||
let Some(font) = selection else {
|
||||
if let Some(font) = used.first().cloned() {
|
||||
// Shape tofu.
|
||||
let glyph = Glyph {
|
||||
id: 0,
|
||||
x_advance: font.x_advance(0).unwrap_or_default(),
|
||||
x_offset: Em::zero(),
|
||||
y_advance: Em::zero(),
|
||||
y_offset: Em::zero(),
|
||||
range: 0..text.len().saturating_as(),
|
||||
span: (Span::detached(), 0),
|
||||
};
|
||||
let c = text.chars().next().unwrap();
|
||||
return Some((c, font, glyph));
|
||||
}
|
||||
return None;
|
||||
};
|
||||
|
||||
// This font has been exhausted and will not be used again.
|
||||
if covers.is_none() {
|
||||
used.push(font.clone());
|
||||
}
|
||||
|
||||
buffer.push_str(text);
|
||||
buffer.set_language(language.clone());
|
||||
// TODO: Use `rustybuzz::script::MATH` once
|
||||
// https://github.com/harfbuzz/rustybuzz/pull/165 is released.
|
||||
buffer.set_script(
|
||||
rustybuzz::Script::from_iso15924_tag(ttf_parser::Tag::from_bytes(b"math"))
|
||||
.unwrap(),
|
||||
);
|
||||
buffer.set_direction(rustybuzz::Direction::LeftToRight);
|
||||
buffer.set_flags(BufferFlags::REMOVE_DEFAULT_IGNORABLES);
|
||||
|
||||
let plan = create_shape_plan(
|
||||
&font,
|
||||
buffer.direction(),
|
||||
buffer.script(),
|
||||
buffer.language().as_ref(),
|
||||
&features,
|
||||
);
|
||||
|
||||
let buffer = rustybuzz::shape_with_plan(font.rusty(), &plan, buffer);
|
||||
match buffer.len() {
|
||||
0 => return None,
|
||||
1 => {}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
|
||||
let info = buffer.glyph_infos()[0];
|
||||
let pos = buffer.glyph_positions()[0];
|
||||
let cluster = info.cluster as usize;
|
||||
let end = text[cluster..]
|
||||
.char_indices()
|
||||
.nth(1)
|
||||
.map(|(i, _)| i)
|
||||
.unwrap_or(text.len());
|
||||
|
||||
if info.glyph_id != 0 && covers.is_none_or(|cov| cov.is_match(&text[cluster..end])) {
|
||||
let glyph = Glyph {
|
||||
id: info.glyph_id as u16,
|
||||
x_advance: font.to_em(pos.x_advance),
|
||||
x_offset: font.to_em(pos.x_offset),
|
||||
y_advance: font.to_em(pos.y_advance),
|
||||
y_offset: font.to_em(pos.y_offset),
|
||||
range: 0..text.len().saturating_as(),
|
||||
span: (Span::detached(), 0),
|
||||
};
|
||||
let c = text[cluster..].chars().next().unwrap();
|
||||
Some((c, font, glyph))
|
||||
} else {
|
||||
shape_glyph(
|
||||
world,
|
||||
used,
|
||||
buffer.clear(),
|
||||
variant,
|
||||
features,
|
||||
language,
|
||||
fallback,
|
||||
text,
|
||||
families,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Describes in which situation a frame should use limits for attachments.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum Limits {
|
||||
|
@ -33,12 +33,13 @@ pub fn layout_lr(
|
||||
let (start_idx, end_idx) = fragments.split_prefix_suffix(|f| f.is_ignorant());
|
||||
let inner_fragments = &mut fragments[start_idx..end_idx];
|
||||
|
||||
let axis = scaled!(ctx, styles, axis_height);
|
||||
let max_extent = inner_fragments
|
||||
.iter()
|
||||
.map(|fragment| (fragment.ascent() - axis).max(fragment.descent() + axis))
|
||||
.max()
|
||||
.unwrap_or_default();
|
||||
let mut max_extent = Abs::zero();
|
||||
for fragment in inner_fragments.iter() {
|
||||
let (font, size) = fragment.font(ctx, styles);
|
||||
let axis = font.math().axis_height.at(size);
|
||||
let extent = (fragment.ascent() - axis).max(fragment.descent() + axis);
|
||||
max_extent = max_extent.max(extent);
|
||||
}
|
||||
|
||||
let relative_to = 2.0 * max_extent;
|
||||
let height = elem.size.resolve(styles);
|
||||
|
@ -1,5 +1,5 @@
|
||||
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::{
|
||||
Abs, Axes, Em, FixedAlignment, Frame, FrameItem, Point, Ratio, Rel, Size,
|
||||
};
|
||||
@ -186,12 +186,10 @@ fn layout_body(
|
||||
// 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
|
||||
// way too big.
|
||||
let paren = GlyphFragment::new_char(
|
||||
ctx.font,
|
||||
styles.chain(&denom_style),
|
||||
'(',
|
||||
Span::detached(),
|
||||
)?;
|
||||
// This will never panic as a paren will never shape into nothing.
|
||||
let paren =
|
||||
GlyphFragment::new_char(ctx, styles.chain(&denom_style), '(', Span::detached())
|
||||
.unwrap();
|
||||
|
||||
for (column, col) in columns.iter().zip(&mut cols) {
|
||||
for (cell, (ascent, descent)) in column.iter().zip(&mut heights) {
|
||||
@ -314,13 +312,14 @@ fn layout_delimiters(
|
||||
span: Span,
|
||||
) -> SourceResult<()> {
|
||||
let short_fall = DELIM_SHORT_FALL.resolve(styles);
|
||||
let axis = scaled!(ctx, styles, axis_height);
|
||||
let axis = ctx.font().math().axis_height.resolve(styles);
|
||||
let height = frame.height();
|
||||
let target = height + VERTICAL_PADDING.of(height);
|
||||
frame.set_baseline(height / 2.0 + axis);
|
||||
|
||||
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.center_on_axis();
|
||||
ctx.push(left);
|
||||
@ -329,7 +328,8 @@ fn layout_delimiters(
|
||||
ctx.push(FrameFragment::new(styles, frame));
|
||||
|
||||
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.center_on_axis();
|
||||
ctx.push(right);
|
||||
|
@ -13,11 +13,12 @@ mod stretch;
|
||||
mod text;
|
||||
mod underover;
|
||||
|
||||
use comemo::Tracked;
|
||||
use typst_library::World;
|
||||
use typst_library::diag::{SourceResult, bail};
|
||||
use typst_library::diag::{At, SourceResult, warning};
|
||||
use typst_library::engine::Engine;
|
||||
use typst_library::foundations::{
|
||||
Content, NativeElement, Packed, Resolve, StyleChain, SymbolElem,
|
||||
Content, NativeElement, Packed, Resolve, Style, StyleChain, SymbolElem,
|
||||
};
|
||||
use typst_library::introspection::{Counter, Locator, SplitLocator, TagElem};
|
||||
use typst_library::layout::{
|
||||
@ -29,10 +30,11 @@ use typst_library::math::*;
|
||||
use typst_library::model::ParElem;
|
||||
use typst_library::routines::{Arenas, RealizationKind};
|
||||
use typst_library::text::{
|
||||
Font, LinebreakElem, SpaceElem, TextEdgeBounds, TextElem, families, variant,
|
||||
Font, FontFlags, LinebreakElem, SpaceElem, TextEdgeBounds, TextElem, variant,
|
||||
};
|
||||
use typst_syntax::Span;
|
||||
use typst_utils::Numeric;
|
||||
use typst_utils::{LazyHash, Numeric};
|
||||
|
||||
use unicode_math_class::MathClass;
|
||||
|
||||
use self::fragment::{
|
||||
@ -53,12 +55,14 @@ pub fn layout_equation_inline(
|
||||
) -> SourceResult<Vec<InlineItem>> {
|
||||
assert!(!elem.block.get(styles));
|
||||
|
||||
let font = find_math_font(engine, styles, elem.span())?;
|
||||
let span = elem.span();
|
||||
let font = get_font(engine.world, styles, span)?;
|
||||
warn_non_math_font(&font, engine, span);
|
||||
|
||||
let mut locator = locator.split();
|
||||
let mut ctx = MathContext::new(engine, &mut locator, region, &font);
|
||||
let mut ctx = MathContext::new(engine, &mut locator, region, font.clone());
|
||||
|
||||
let scale_style = style_for_script_scale(&ctx);
|
||||
let scale_style = style_for_script_scale(&font);
|
||||
let styles = styles.chain(&scale_style);
|
||||
|
||||
let run = ctx.layout_into_run(&elem.body, styles)?;
|
||||
@ -108,12 +112,13 @@ pub fn layout_equation_block(
|
||||
assert!(elem.block.get(styles));
|
||||
|
||||
let span = elem.span();
|
||||
let font = find_math_font(engine, styles, span)?;
|
||||
let font = get_font(engine.world, styles, span)?;
|
||||
warn_non_math_font(&font, engine, span);
|
||||
|
||||
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(), font.clone());
|
||||
|
||||
let scale_style = style_for_script_scale(&ctx);
|
||||
let scale_style = style_for_script_scale(&font);
|
||||
let styles = styles.chain(&scale_style);
|
||||
|
||||
let full_equation_builder = ctx
|
||||
@ -234,24 +239,6 @@ pub fn layout_equation_block(
|
||||
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(
|
||||
equation_builder: MathRunFrameBuilder,
|
||||
number: Frame,
|
||||
@ -370,10 +357,8 @@ struct MathContext<'a, 'v, 'e> {
|
||||
engine: &'v mut Engine<'e>,
|
||||
locator: &'v mut SplitLocator<'a>,
|
||||
region: Region,
|
||||
// Font-related.
|
||||
font: &'a Font,
|
||||
constants: ttf_parser::math::Constants<'a>,
|
||||
// Mutable.
|
||||
fonts_stack: Vec<Font>,
|
||||
fragments: Vec<MathFragment>,
|
||||
}
|
||||
|
||||
@ -383,23 +368,24 @@ impl<'a, 'v, 'e> MathContext<'a, 'v, 'e> {
|
||||
engine: &'v mut Engine<'e>,
|
||||
locator: &'v mut SplitLocator<'a>,
|
||||
base: Size,
|
||||
font: &'a Font,
|
||||
font: Font,
|
||||
) -> 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 {
|
||||
engine,
|
||||
locator,
|
||||
region: Region::new(base, Axes::splat(false)),
|
||||
font,
|
||||
constants,
|
||||
fonts_stack: vec![font],
|
||||
fragments: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current base font.
|
||||
#[inline]
|
||||
fn font(&self) -> &Font {
|
||||
// Will always be at least one font in the stack.
|
||||
self.fonts_stack.last().unwrap()
|
||||
}
|
||||
|
||||
/// Push a fragment.
|
||||
fn push(&mut self, fragment: impl Into<MathFragment>) {
|
||||
self.fragments.push(fragment.into());
|
||||
@ -469,19 +455,21 @@ impl<'a, 'v, 'e> MathContext<'a, 'v, 'e> {
|
||||
styles,
|
||||
)?;
|
||||
|
||||
let outer = styles;
|
||||
let outer_styles = styles;
|
||||
let outer_font = styles.get_ref(TextElem::font);
|
||||
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;
|
||||
}
|
||||
|
||||
// Whilst this check isn't exact, it more or less suffices as a
|
||||
// change in font variant probably won't have an effect on metrics.
|
||||
if styles != outer_styles && styles.get_ref(TextElem::font) != outer_font {
|
||||
self.fonts_stack
|
||||
.push(get_font(self.engine.world, styles, elem.span())?);
|
||||
let scale_style = style_for_script_scale(self.font());
|
||||
layout_realized(elem, self, styles.chain(&scale_style))?;
|
||||
self.fonts_stack.pop();
|
||||
} else {
|
||||
layout_realized(elem, self, styles)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -496,8 +484,7 @@ fn layout_realized(
|
||||
if let Some(elem) = elem.to_packed::<TagElem>() {
|
||||
ctx.push(MathFragment::Tag(elem.tag.clone()));
|
||||
} else if elem.is::<SpaceElem>() {
|
||||
let space_width = ctx.font.space_width().unwrap_or(THICK);
|
||||
ctx.push(MathFragment::Space(space_width.resolve(styles)));
|
||||
ctx.push(MathFragment::Space(ctx.font().math().space_width.resolve(styles)));
|
||||
} else if elem.is::<LinebreakElem>() {
|
||||
ctx.push(MathFragment::Linebreak);
|
||||
} else if let Some(elem) = elem.to_packed::<HElem>() {
|
||||
@ -567,7 +554,7 @@ fn layout_realized(
|
||||
} else {
|
||||
let mut frame = layout_external(elem, ctx, styles)?;
|
||||
if !frame.has_baseline() {
|
||||
let axis = scaled!(ctx, styles, axis_height);
|
||||
let axis = ctx.font().math().axis_height.resolve(styles);
|
||||
frame.set_baseline(frame.height() / 2.0 + axis);
|
||||
}
|
||||
ctx.push(
|
||||
@ -667,3 +654,43 @@ fn layout_external(
|
||||
ctx.region,
|
||||
)
|
||||
}
|
||||
|
||||
/// Styles to add font constants to the style chain.
|
||||
fn style_for_script_scale(font: &Font) -> LazyHash<Style> {
|
||||
EquationElem::script_scale
|
||||
.set((
|
||||
font.math().script_percent_scale_down,
|
||||
font.math().script_script_percent_scale_down,
|
||||
))
|
||||
.wrap()
|
||||
}
|
||||
|
||||
/// Get the current base font.
|
||||
fn get_font(
|
||||
world: Tracked<dyn World + '_>,
|
||||
styles: StyleChain,
|
||||
span: Span,
|
||||
) -> SourceResult<Font> {
|
||||
let variant = variant(styles);
|
||||
families(styles)
|
||||
.find_map(|family| {
|
||||
world
|
||||
.book()
|
||||
.select(family.as_str(), variant)
|
||||
.and_then(|id| world.font(id))
|
||||
.filter(|_| family.covers().is_none())
|
||||
})
|
||||
.ok_or("no font could be found")
|
||||
.at(span)
|
||||
}
|
||||
|
||||
/// Check if the top-level base font has a MATH table.
|
||||
fn warn_non_math_font(font: &Font, engine: &mut Engine, span: Span) {
|
||||
if !font.info().flags.contains(FontFlags::MATH) {
|
||||
engine.sink.warn(warning!(
|
||||
span,
|
||||
"current font is not designed for math";
|
||||
hint: "rendering may be poor"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
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::math::{EquationElem, MathSize, RootElem};
|
||||
use typst_library::text::TextElem;
|
||||
use typst_library::visualize::{FixedStroke, Geometry};
|
||||
|
||||
use super::{FrameFragment, GlyphFragment, MathContext, style_cramped};
|
||||
use super::{FrameFragment, MathContext, style_cramped};
|
||||
|
||||
/// Lays out a [`RootElem`].
|
||||
///
|
||||
@ -17,45 +17,61 @@ pub fn layout_root(
|
||||
ctx: &mut MathContext,
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<()> {
|
||||
let index = elem.index.get_ref(styles);
|
||||
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.
|
||||
let radicand = {
|
||||
let cramped = style_cramped();
|
||||
let styles = styles.chain(&cramped);
|
||||
let run = ctx.layout_into_run(&elem.radicand, styles)?;
|
||||
let multiline = run.is_multiline();
|
||||
let mut radicand = run.into_fragment(styles).into_frame();
|
||||
let radicand = run.into_fragment(styles);
|
||||
if multiline {
|
||||
// Align the frame center line with the math axis.
|
||||
radicand.set_baseline(
|
||||
radicand.height() / 2.0 + scaled!(ctx, styles, axis_height),
|
||||
);
|
||||
}
|
||||
let (font, size) = radicand.font(ctx, styles);
|
||||
let axis = font.math().axis_height.at(size);
|
||||
let mut radicand = radicand.into_frame();
|
||||
radicand.set_baseline(radicand.height() / 2.0 + axis);
|
||||
radicand
|
||||
} else {
|
||||
radicand.into_frame()
|
||||
}
|
||||
};
|
||||
|
||||
// Layout root symbol.
|
||||
let mut sqrt =
|
||||
ctx.layout_into_fragment(&SymbolElem::packed('√').spanned(span), styles)?;
|
||||
|
||||
let (font, size) = sqrt.font(ctx, styles);
|
||||
let thickness = font.math().radical_rule_thickness.at(size);
|
||||
let extra_ascender = font.math().radical_extra_ascender.at(size);
|
||||
let kern_before = font.math().radical_kern_before_degree.at(size);
|
||||
let kern_after = font.math().radical_kern_after_degree.at(size);
|
||||
let raise_factor = font.math().radical_degree_bottom_raise_percent;
|
||||
let gap = match styles.get(EquationElem::size) {
|
||||
MathSize::Display => font.math().radical_display_style_vertical_gap,
|
||||
_ => font.math().radical_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 mut sqrt = GlyphFragment::new_char(ctx.font, styles, '√', span)?;
|
||||
sqrt.stretch_vertical(ctx, target);
|
||||
let sqrt = sqrt.into_frame();
|
||||
|
||||
// Layout the index.
|
||||
let sscript = EquationElem::size.set(MathSize::ScriptScript).wrap();
|
||||
let index = index
|
||||
let index = elem
|
||||
.index
|
||||
.get_ref(styles)
|
||||
.as_ref()
|
||||
.map(|elem| ctx.layout_into_frame(elem, styles.chain(&sscript)))
|
||||
.transpose()?;
|
||||
@ -107,19 +123,7 @@ pub fn layout_root(
|
||||
}
|
||||
|
||||
frame.push_frame(sqrt_pos, sqrt);
|
||||
frame.push(
|
||||
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(line_pos, line);
|
||||
frame.push_frame(radicand_pos, radicand);
|
||||
ctx.push(FrameFragment::new(styles, frame));
|
||||
|
||||
|
@ -1,61 +1,15 @@
|
||||
use ttf_parser::Tag;
|
||||
use ttf_parser::math::MathValue;
|
||||
use typst_library::foundations::{Style, StyleChain};
|
||||
use typst_library::layout::{Abs, Em, FixedAlignment, Frame, Point, Size};
|
||||
use typst_library::math::{EquationElem, MathSize};
|
||||
use typst_library::text::{FontFeatures, TextElem};
|
||||
use typst_utils::LazyHash;
|
||||
use typst_library::text::{FontFamily, FontFeatures, TextElem};
|
||||
use typst_utils::{LazyHash, singleton};
|
||||
|
||||
use super::{LeftRightAlternator, MathContext, MathFragment, MathRun};
|
||||
|
||||
macro_rules! scaled {
|
||||
($ctx:expr, $styles:expr, text: $text:ident, display: $display:ident $(,)?) => {
|
||||
match $styles.get(typst_library::math::EquationElem::size) {
|
||||
typst_library::math::MathSize::Display => scaled!($ctx, $styles, $display),
|
||||
_ => scaled!($ctx, $styles, $text),
|
||||
}
|
||||
};
|
||||
($ctx:expr, $styles:expr, $name:ident) => {
|
||||
$crate::math::Scaled::scaled(
|
||||
$ctx.constants.$name(),
|
||||
$ctx,
|
||||
$styles.resolve(typst_library::text::TextElem::size),
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! percent {
|
||||
($ctx:expr, $name:ident) => {
|
||||
$ctx.constants.$name() as f64 / 100.0
|
||||
};
|
||||
}
|
||||
use super::{LeftRightAlternator, MathFragment, MathRun};
|
||||
|
||||
/// How much less high scaled delimiters can be than what they wrap.
|
||||
pub const DELIM_SHORT_FALL: Em = Em::new(0.1);
|
||||
|
||||
/// Converts some unit to an absolute length with the current font & font size.
|
||||
pub trait Scaled {
|
||||
fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs;
|
||||
}
|
||||
|
||||
impl Scaled for i16 {
|
||||
fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs {
|
||||
ctx.font.to_em(self).at(font_size)
|
||||
}
|
||||
}
|
||||
|
||||
impl Scaled for u16 {
|
||||
fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs {
|
||||
ctx.font.to_em(self).at(font_size)
|
||||
}
|
||||
}
|
||||
|
||||
impl Scaled for MathValue<'_> {
|
||||
fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs {
|
||||
self.value.scaled(ctx, font_size)
|
||||
}
|
||||
}
|
||||
|
||||
/// Styles something as cramped.
|
||||
pub fn style_cramped() -> LazyHash<Style> {
|
||||
EquationElem::cramped.set(true).wrap()
|
||||
@ -106,14 +60,24 @@ pub fn style_for_denominator(styles: StyleChain) -> [LazyHash<Style>; 2] {
|
||||
[style_for_numerator(styles), EquationElem::cramped.set(true).wrap()]
|
||||
}
|
||||
|
||||
/// Styles to add font constants to the style chain.
|
||||
pub fn style_for_script_scale(ctx: &MathContext) -> LazyHash<Style> {
|
||||
EquationElem::script_scale
|
||||
.set((
|
||||
ctx.constants.script_percent_scale_down(),
|
||||
ctx.constants.script_script_percent_scale_down(),
|
||||
))
|
||||
.wrap()
|
||||
/// Resolve a prioritized iterator over the font families for math.
|
||||
pub fn families(styles: StyleChain<'_>) -> impl Iterator<Item = &'_ FontFamily> + Clone {
|
||||
let fallbacks = singleton!(Vec<FontFamily>, {
|
||||
[
|
||||
"new computer modern math",
|
||||
"libertinus serif",
|
||||
"twitter color emoji",
|
||||
"noto color emoji",
|
||||
"apple color emoji",
|
||||
"segoe ui emoji",
|
||||
]
|
||||
.into_iter()
|
||||
.map(FontFamily::new)
|
||||
.collect()
|
||||
});
|
||||
|
||||
let tail = if styles.get(TextElem::fallback) { fallbacks.as_slice() } else { &[] };
|
||||
styles.get_ref(TextElem::font).into_iter().chain(tail.iter())
|
||||
}
|
||||
|
||||
/// Stack rows on top of each other.
|
||||
|
@ -1,9 +1,7 @@
|
||||
use std::f64::consts::SQRT_2;
|
||||
|
||||
use codex::styling::{MathStyle, to_style};
|
||||
use ecow::EcoString;
|
||||
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::math::{EquationElem, MathSize};
|
||||
use typst_library::text::{
|
||||
@ -52,7 +50,7 @@ fn layout_text_lines<'a>(
|
||||
}
|
||||
}
|
||||
let mut frame = MathRun::new(fragments).into_frame(styles);
|
||||
let axis = scaled!(ctx, styles, axis_height);
|
||||
let axis = ctx.font().math().axis_height.resolve(styles);
|
||||
frame.set_baseline(frame.height() / 2.0 + axis);
|
||||
Ok(FrameFragment::new(styles, frame))
|
||||
}
|
||||
@ -80,7 +78,9 @@ fn layout_inline_text(
|
||||
let style = MathStyle::select(unstyled_c, variant, bold, italic);
|
||||
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());
|
||||
}
|
||||
let frame = MathRun::new(fragments).into_frame(styles);
|
||||
@ -132,8 +132,8 @@ pub fn layout_symbol(
|
||||
// Switch dotless char to normal when we have the dtls OpenType feature.
|
||||
// This should happen before the main styling pass.
|
||||
let dtls = style_dtls();
|
||||
let (unstyled_c, symbol_styles) = match try_dotless(elem.text) {
|
||||
Some(c) if has_dtls_feat(ctx.font) => (c, styles.chain(&dtls)),
|
||||
let (unstyled_c, symbol_styles) = match (try_dotless(elem.text), ctx.font().clone()) {
|
||||
(Some(c), font) if has_dtls_feat(&font) => (c, styles.chain(&dtls)),
|
||||
_ => (elem.text, styles),
|
||||
};
|
||||
|
||||
@ -144,39 +144,27 @@ pub fn layout_symbol(
|
||||
let style = MathStyle::select(unstyled_c, variant, bold, italic);
|
||||
let text: EcoString = to_style(unstyled_c, style).collect();
|
||||
|
||||
let fragment: MathFragment =
|
||||
match GlyphFragment::new(ctx.font, symbol_styles, &text, elem.span()) {
|
||||
Ok(mut glyph) => {
|
||||
adjust_glyph_layout(&mut glyph, ctx, styles);
|
||||
glyph.into()
|
||||
}
|
||||
Err(_) => {
|
||||
// Not in the math font, fallback to normal inline text layout.
|
||||
// TODO: Should replace this with proper fallback in [`GlyphFragment::new`].
|
||||
layout_inline_text(&text, elem.span(), ctx, styles)?.into()
|
||||
}
|
||||
};
|
||||
ctx.push(fragment);
|
||||
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 let Some(mut glyph) =
|
||||
GlyphFragment::new(ctx.engine.world, symbol_styles, &text, elem.span())
|
||||
{
|
||||
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);
|
||||
let height = glyph
|
||||
.item
|
||||
.font
|
||||
.math()
|
||||
.display_operator_min_height
|
||||
.at(glyph.item.size);
|
||||
glyph.stretch_vertical(ctx, height);
|
||||
};
|
||||
// TeXbook p 155. Large operators are always vertically centered on the
|
||||
// axis.
|
||||
// TeXbook p 155. Large operators are always vertically centered on
|
||||
// the axis.
|
||||
glyph.center_on_axis();
|
||||
}
|
||||
ctx.push(glyph);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The non-dotless version of a dotless character that can be used with the
|
||||
|
@ -1,5 +1,5 @@
|
||||
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::math::{
|
||||
OverbraceElem, OverbracketElem, OverlineElem, OverparenElem, OvershellElem,
|
||||
@ -10,8 +10,8 @@ use typst_library::visualize::{FixedStroke, Geometry};
|
||||
use typst_syntax::Span;
|
||||
|
||||
use super::{
|
||||
FrameFragment, GlyphFragment, LeftRightAlternator, MathContext, MathRun, stack,
|
||||
style_cramped, style_for_subscript, style_for_superscript,
|
||||
FrameFragment, LeftRightAlternator, MathContext, MathRun, stack, style_cramped,
|
||||
style_for_subscript, style_for_superscript,
|
||||
};
|
||||
|
||||
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);
|
||||
match position {
|
||||
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)?;
|
||||
|
||||
let (font, size) = content.font(ctx, styles);
|
||||
let sep = font.math().underbar_extra_descender.at(size);
|
||||
bar_height = font.math().underbar_rule_thickness.at(size);
|
||||
let gap = font.math().underbar_vertical_gap.at(size);
|
||||
extra_height = sep + bar_height + gap;
|
||||
|
||||
line_pos = Point::with_y(content.height() + gap + bar_height / 2.0);
|
||||
content_pos = Point::zero();
|
||||
baseline = content.ascent();
|
||||
line_adjust = -content.italics_correction();
|
||||
}
|
||||
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();
|
||||
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);
|
||||
let sep = font.math().overbar_extra_ascender.at(size);
|
||||
bar_height = font.math().overbar_rule_thickness.at(size);
|
||||
let gap = font.math().overbar_vertical_gap.at(size);
|
||||
extra_height = sep + bar_height + gap;
|
||||
|
||||
line_pos = Point::with_y(sep + bar_height / 2.0);
|
||||
content_pos = Point::with_y(extra_height);
|
||||
@ -285,7 +288,8 @@ fn layout_underoverspreader(
|
||||
let body = ctx.layout_into_run(body, styles)?;
|
||||
let body_class = body.class();
|
||||
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());
|
||||
|
||||
let mut rows = vec![];
|
||||
|
@ -98,14 +98,11 @@ pub fn cal(
|
||||
/// ```example
|
||||
/// #let scr(it) = text(
|
||||
/// features: ("ss01",),
|
||||
/// box($cal(it)$),
|
||||
/// $cal(it)$,
|
||||
/// )
|
||||
///
|
||||
/// 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"])]
|
||||
pub fn scr(
|
||||
/// The content to style.
|
||||
|
@ -12,14 +12,16 @@ pub use self::variant::{FontStretch, FontStyle, FontVariant, FontWeight};
|
||||
use std::cell::OnceCell;
|
||||
use std::fmt::{self, Debug, Formatter};
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, OnceLock};
|
||||
|
||||
use ttf_parser::GlyphId;
|
||||
use ttf_parser::{GlyphId, name_id};
|
||||
|
||||
use self::book::find_name;
|
||||
use crate::foundations::{Bytes, Cast};
|
||||
use crate::layout::{Abs, Em, Frame};
|
||||
use crate::text::{BottomEdge, TopEdge};
|
||||
use crate::text::{
|
||||
BottomEdge, DEFAULT_SUBSCRIPT_METRICS, DEFAULT_SUPERSCRIPT_METRICS, TopEdge,
|
||||
};
|
||||
|
||||
/// An OpenType font.
|
||||
///
|
||||
@ -95,6 +97,12 @@ impl Font {
|
||||
&self.0.metrics
|
||||
}
|
||||
|
||||
/// The font's math constants.
|
||||
#[inline]
|
||||
pub fn math(&self) -> &MathConstants {
|
||||
self.0.metrics.math.get_or_init(|| FontMetrics::init_math(self))
|
||||
}
|
||||
|
||||
/// The number of font units per one em.
|
||||
pub fn units_per_em(&self) -> f64 {
|
||||
self.0.metrics.units_per_em
|
||||
@ -121,11 +129,6 @@ impl Font {
|
||||
.map(|units| self.to_em(units))
|
||||
}
|
||||
|
||||
/// Look up the width of a space.
|
||||
pub fn space_width(&self) -> Option<Em> {
|
||||
self.0.ttf.glyph_index(' ').and_then(|id| self.x_advance(id.0))
|
||||
}
|
||||
|
||||
/// Lookup a name by id.
|
||||
pub fn find_name(&self, id: u16) -> Option<String> {
|
||||
find_name(&self.0.ttf, id)
|
||||
@ -210,7 +213,7 @@ impl PartialEq for Font {
|
||||
}
|
||||
|
||||
/// Metrics of a font.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FontMetrics {
|
||||
/// How many font units represent one em unit.
|
||||
pub units_per_em: f64,
|
||||
@ -232,6 +235,8 @@ pub struct FontMetrics {
|
||||
pub subscript: Option<ScriptMetrics>,
|
||||
/// Metrics for superscripts, if provided by the font.
|
||||
pub superscript: Option<ScriptMetrics>,
|
||||
/// Metrics for math layout.
|
||||
pub math: OnceLock<Box<MathConstants>>,
|
||||
}
|
||||
|
||||
impl FontMetrics {
|
||||
@ -292,9 +297,185 @@ impl FontMetrics {
|
||||
overline,
|
||||
superscript,
|
||||
subscript,
|
||||
math: OnceLock::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn init_math(font: &Font) -> Box<MathConstants> {
|
||||
let ttf = font.ttf();
|
||||
let metrics = font.metrics();
|
||||
|
||||
let space_width = ttf
|
||||
.glyph_index(' ')
|
||||
.and_then(|id| ttf.glyph_hor_advance(id).map(|units| font.to_em(units)))
|
||||
.unwrap_or(typst_library::math::THICK);
|
||||
|
||||
let is_cambria = || {
|
||||
font.find_name(name_id::POST_SCRIPT_NAME)
|
||||
.is_some_and(|name| name == "CambriaMath")
|
||||
};
|
||||
|
||||
Box::new(
|
||||
ttf.tables()
|
||||
.math
|
||||
.and_then(|math| math.constants)
|
||||
.map(|constants| MathConstants {
|
||||
space_width,
|
||||
script_percent_scale_down: constants.script_percent_scale_down(),
|
||||
script_script_percent_scale_down: constants
|
||||
.script_script_percent_scale_down(),
|
||||
display_operator_min_height: font.to_em(if is_cambria() {
|
||||
constants.delimited_sub_formula_min_height()
|
||||
} else {
|
||||
constants.display_operator_min_height()
|
||||
}),
|
||||
axis_height: font.to_em(constants.axis_height().value),
|
||||
accent_base_height: font.to_em(constants.accent_base_height().value),
|
||||
flattened_accent_base_height: font
|
||||
.to_em(constants.flattened_accent_base_height().value),
|
||||
subscript_shift_down: font
|
||||
.to_em(constants.subscript_shift_down().value),
|
||||
subscript_top_max: font.to_em(constants.subscript_top_max().value),
|
||||
subscript_baseline_drop_min: font
|
||||
.to_em(constants.subscript_baseline_drop_min().value),
|
||||
superscript_shift_up: font
|
||||
.to_em(constants.superscript_shift_up().value),
|
||||
superscript_shift_up_cramped: font
|
||||
.to_em(constants.superscript_shift_up_cramped().value),
|
||||
superscript_bottom_min: font
|
||||
.to_em(constants.superscript_bottom_min().value),
|
||||
superscript_baseline_drop_max: font
|
||||
.to_em(constants.superscript_baseline_drop_max().value),
|
||||
sub_superscript_gap_min: font
|
||||
.to_em(constants.sub_superscript_gap_min().value),
|
||||
superscript_bottom_max_with_subscript: font
|
||||
.to_em(constants.superscript_bottom_max_with_subscript().value),
|
||||
space_after_script: font.to_em(constants.space_after_script().value),
|
||||
upper_limit_gap_min: font
|
||||
.to_em(constants.upper_limit_gap_min().value),
|
||||
upper_limit_baseline_rise_min: font
|
||||
.to_em(constants.upper_limit_baseline_rise_min().value),
|
||||
lower_limit_gap_min: font
|
||||
.to_em(constants.lower_limit_gap_min().value),
|
||||
lower_limit_baseline_drop_min: font
|
||||
.to_em(constants.lower_limit_baseline_drop_min().value),
|
||||
fraction_numerator_shift_up: font
|
||||
.to_em(constants.fraction_numerator_shift_up().value),
|
||||
fraction_numerator_display_style_shift_up: font.to_em(
|
||||
constants.fraction_numerator_display_style_shift_up().value,
|
||||
),
|
||||
fraction_denominator_shift_down: font
|
||||
.to_em(constants.fraction_denominator_shift_down().value),
|
||||
fraction_denominator_display_style_shift_down: font.to_em(
|
||||
constants.fraction_denominator_display_style_shift_down().value,
|
||||
),
|
||||
fraction_numerator_gap_min: font
|
||||
.to_em(constants.fraction_numerator_gap_min().value),
|
||||
fraction_num_display_style_gap_min: font
|
||||
.to_em(constants.fraction_num_display_style_gap_min().value),
|
||||
fraction_rule_thickness: font
|
||||
.to_em(constants.fraction_rule_thickness().value),
|
||||
fraction_denominator_gap_min: font
|
||||
.to_em(constants.fraction_denominator_gap_min().value),
|
||||
fraction_denom_display_style_gap_min: font
|
||||
.to_em(constants.fraction_denom_display_style_gap_min().value),
|
||||
overbar_vertical_gap: font
|
||||
.to_em(constants.overbar_vertical_gap().value),
|
||||
overbar_rule_thickness: font
|
||||
.to_em(constants.overbar_rule_thickness().value),
|
||||
overbar_extra_ascender: font
|
||||
.to_em(constants.overbar_extra_ascender().value),
|
||||
underbar_vertical_gap: font
|
||||
.to_em(constants.underbar_vertical_gap().value),
|
||||
underbar_rule_thickness: font
|
||||
.to_em(constants.underbar_rule_thickness().value),
|
||||
underbar_extra_descender: font
|
||||
.to_em(constants.underbar_extra_descender().value),
|
||||
radical_vertical_gap: font
|
||||
.to_em(constants.radical_vertical_gap().value),
|
||||
radical_display_style_vertical_gap: font
|
||||
.to_em(constants.radical_display_style_vertical_gap().value),
|
||||
radical_rule_thickness: font
|
||||
.to_em(constants.radical_rule_thickness().value),
|
||||
radical_extra_ascender: font
|
||||
.to_em(constants.radical_extra_ascender().value),
|
||||
radical_kern_before_degree: font
|
||||
.to_em(constants.radical_kern_before_degree().value),
|
||||
radical_kern_after_degree: font
|
||||
.to_em(constants.radical_kern_after_degree().value),
|
||||
radical_degree_bottom_raise_percent: constants
|
||||
.radical_degree_bottom_raise_percent()
|
||||
as f64
|
||||
/ 100.0,
|
||||
})
|
||||
// Most of these fallback constants are from the MathML Core
|
||||
// spec, with the exceptions of
|
||||
// - `flattened_accent_base_height` from Building Math Fonts
|
||||
// - `overbar_rule_thickness` and `underbar_rule_thickness`
|
||||
// from our best guess
|
||||
// - `script_percent_scale_down` and
|
||||
// `script_script_percent_scale_down` from Building Math
|
||||
// Fonts as the defaults given in MathML Core have more
|
||||
// precision than i16.
|
||||
//
|
||||
// https://www.w3.org/TR/mathml-core/#layout-constants-mathconstants
|
||||
// https://github.com/notofonts/math/blob/main/documentation/building-math-fonts/index.md
|
||||
.unwrap_or(MathConstants {
|
||||
space_width,
|
||||
script_percent_scale_down: 70,
|
||||
script_script_percent_scale_down: 50,
|
||||
display_operator_min_height: Em::zero(),
|
||||
axis_height: metrics.x_height / 2.0,
|
||||
accent_base_height: metrics.x_height,
|
||||
flattened_accent_base_height: metrics.cap_height,
|
||||
subscript_shift_down: metrics
|
||||
.subscript
|
||||
.map(|metrics| metrics.vertical_offset)
|
||||
.unwrap_or(DEFAULT_SUBSCRIPT_METRICS.vertical_offset),
|
||||
subscript_top_max: 0.8 * metrics.x_height,
|
||||
subscript_baseline_drop_min: Em::zero(),
|
||||
superscript_shift_up: metrics
|
||||
.superscript
|
||||
.map(|metrics| metrics.vertical_offset)
|
||||
.unwrap_or(DEFAULT_SUPERSCRIPT_METRICS.vertical_offset),
|
||||
superscript_shift_up_cramped: Em::zero(),
|
||||
superscript_bottom_min: 0.25 * metrics.x_height,
|
||||
superscript_baseline_drop_max: Em::zero(),
|
||||
sub_superscript_gap_min: 4.0 * metrics.underline.thickness,
|
||||
superscript_bottom_max_with_subscript: 0.8 * metrics.x_height,
|
||||
space_after_script: Em::new(1.0 / 24.0),
|
||||
upper_limit_gap_min: Em::zero(),
|
||||
upper_limit_baseline_rise_min: Em::zero(),
|
||||
lower_limit_gap_min: Em::zero(),
|
||||
lower_limit_baseline_drop_min: Em::zero(),
|
||||
fraction_numerator_shift_up: Em::zero(),
|
||||
fraction_numerator_display_style_shift_up: Em::zero(),
|
||||
fraction_denominator_shift_down: Em::zero(),
|
||||
fraction_denominator_display_style_shift_down: Em::zero(),
|
||||
fraction_numerator_gap_min: metrics.underline.thickness,
|
||||
fraction_num_display_style_gap_min: 3.0 * metrics.underline.thickness,
|
||||
fraction_rule_thickness: metrics.underline.thickness,
|
||||
fraction_denominator_gap_min: metrics.underline.thickness,
|
||||
fraction_denom_display_style_gap_min: 3.0
|
||||
* metrics.underline.thickness,
|
||||
overbar_vertical_gap: 3.0 * metrics.underline.thickness,
|
||||
overbar_rule_thickness: metrics.underline.thickness,
|
||||
overbar_extra_ascender: metrics.underline.thickness,
|
||||
underbar_vertical_gap: 3.0 * metrics.underline.thickness,
|
||||
underbar_rule_thickness: metrics.underline.thickness,
|
||||
underbar_extra_descender: metrics.underline.thickness,
|
||||
radical_vertical_gap: 1.25 * metrics.underline.thickness,
|
||||
radical_display_style_vertical_gap: metrics.underline.thickness
|
||||
+ 0.25 * metrics.x_height,
|
||||
radical_rule_thickness: metrics.underline.thickness,
|
||||
radical_extra_ascender: metrics.underline.thickness,
|
||||
radical_kern_before_degree: Em::new(5.0 / 18.0),
|
||||
radical_kern_after_degree: Em::new(-10.0 / 18.0),
|
||||
radical_degree_bottom_raise_percent: 0.6,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/// Look up a vertical metric.
|
||||
pub fn vertical(&self, metric: VerticalFontMetric) -> Em {
|
||||
match metric {
|
||||
@ -335,6 +516,58 @@ pub struct ScriptMetrics {
|
||||
pub vertical_offset: Em,
|
||||
}
|
||||
|
||||
/// Constants from the OpenType MATH constants table used in Typst.
|
||||
///
|
||||
/// Ones not currently used are omitted.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct MathConstants {
|
||||
// This is not from the OpenType MATH spec.
|
||||
pub space_width: Em,
|
||||
// These are both i16 instead of f64 as they need to go on the StyleChain.
|
||||
pub script_percent_scale_down: i16,
|
||||
pub script_script_percent_scale_down: i16,
|
||||
pub display_operator_min_height: Em,
|
||||
pub axis_height: Em,
|
||||
pub accent_base_height: Em,
|
||||
pub flattened_accent_base_height: Em,
|
||||
pub subscript_shift_down: Em,
|
||||
pub subscript_top_max: Em,
|
||||
pub subscript_baseline_drop_min: Em,
|
||||
pub superscript_shift_up: Em,
|
||||
pub superscript_shift_up_cramped: Em,
|
||||
pub superscript_bottom_min: Em,
|
||||
pub superscript_baseline_drop_max: Em,
|
||||
pub sub_superscript_gap_min: Em,
|
||||
pub superscript_bottom_max_with_subscript: Em,
|
||||
pub space_after_script: Em,
|
||||
pub upper_limit_gap_min: Em,
|
||||
pub upper_limit_baseline_rise_min: Em,
|
||||
pub lower_limit_gap_min: Em,
|
||||
pub lower_limit_baseline_drop_min: Em,
|
||||
pub fraction_numerator_shift_up: Em,
|
||||
pub fraction_numerator_display_style_shift_up: Em,
|
||||
pub fraction_denominator_shift_down: Em,
|
||||
pub fraction_denominator_display_style_shift_down: Em,
|
||||
pub fraction_numerator_gap_min: Em,
|
||||
pub fraction_num_display_style_gap_min: Em,
|
||||
pub fraction_rule_thickness: Em,
|
||||
pub fraction_denominator_gap_min: Em,
|
||||
pub fraction_denom_display_style_gap_min: Em,
|
||||
pub overbar_vertical_gap: Em,
|
||||
pub overbar_rule_thickness: Em,
|
||||
pub overbar_extra_ascender: Em,
|
||||
pub underbar_vertical_gap: Em,
|
||||
pub underbar_rule_thickness: Em,
|
||||
pub underbar_extra_descender: Em,
|
||||
pub radical_vertical_gap: Em,
|
||||
pub radical_display_style_vertical_gap: Em,
|
||||
pub radical_rule_thickness: Em,
|
||||
pub radical_extra_ascender: Em,
|
||||
pub radical_kern_before_degree: Em,
|
||||
pub radical_kern_after_degree: Em,
|
||||
pub radical_degree_bottom_raise_percent: f64,
|
||||
}
|
||||
|
||||
/// Identifies a vertical metric of a font.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
|
||||
pub enum VerticalFontMetric {
|
||||
|
@ -176,14 +176,14 @@ impl ScriptKind {
|
||||
}
|
||||
}
|
||||
}
|
||||
static DEFAULT_SUBSCRIPT_METRICS: ScriptMetrics = ScriptMetrics {
|
||||
pub static DEFAULT_SUBSCRIPT_METRICS: ScriptMetrics = ScriptMetrics {
|
||||
width: Em::new(0.6),
|
||||
height: Em::new(0.6),
|
||||
horizontal_offset: Em::zero(),
|
||||
vertical_offset: Em::new(-0.2),
|
||||
};
|
||||
|
||||
static DEFAULT_SUPERSCRIPT_METRICS: ScriptMetrics = ScriptMetrics {
|
||||
pub static DEFAULT_SUPERSCRIPT_METRICS: ScriptMetrics = ScriptMetrics {
|
||||
width: Em::new(0.6),
|
||||
height: Em::new(0.6),
|
||||
horizontal_offset: Em::zero(),
|
||||
|
BIN
tests/ref/math-accent-show-rule-1.png
Normal file
After Width: | Height: | Size: 507 B |
BIN
tests/ref/math-accent-show-rule-2.png
Normal file
After Width: | Height: | Size: 730 B |
BIN
tests/ref/math-accent-show-rule-3.png
Normal file
After Width: | Height: | Size: 355 B |
BIN
tests/ref/math-accent-show-rule-4.png
Normal file
After Width: | Height: | Size: 225 B |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
BIN
tests/ref/math-delim-show-rule-1.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
tests/ref/math-delim-show-rule-2.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
tests/ref/math-delim-show-rule-3.png
Normal file
After Width: | Height: | Size: 858 B |
BIN
tests/ref/math-delim-show-rule-4.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
tests/ref/math-delim-show-rule-5.png
Normal file
After Width: | Height: | Size: 674 B |
BIN
tests/ref/math-font-covers.png
Normal file
After Width: | Height: | Size: 387 B |
BIN
tests/ref/math-font-fallback-class.png
Normal file
After Width: | Height: | Size: 224 B |
Before Width: | Height: | Size: 402 B After Width: | Height: | Size: 400 B |
BIN
tests/ref/math-font-features-switch.png
Normal file
After Width: | Height: | Size: 968 B |
BIN
tests/ref/math-font-warning.png
Normal file
After Width: | Height: | Size: 256 B |
BIN
tests/ref/math-glyph-show-rule.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 611 B After Width: | Height: | Size: 607 B |
BIN
tests/ref/math-op-font.png
Normal file
After Width: | Height: | Size: 440 B |
BIN
tests/ref/math-op-set-font.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
tests/ref/math-primes-show-rule.png
Normal file
After Width: | Height: | Size: 856 B |
BIN
tests/ref/math-root-show-rule-1.png
Normal file
After Width: | Height: | Size: 791 B |
BIN
tests/ref/math-root-show-rule-2.png
Normal file
After Width: | Height: | Size: 299 B |
BIN
tests/ref/math-root-show-rule-3.png
Normal file
After Width: | Height: | Size: 260 B |
BIN
tests/ref/math-root-show-rule-4.png
Normal file
After Width: | Height: | Size: 607 B |
BIN
tests/ref/math-root-show-rule-5.png
Normal file
After Width: | Height: | Size: 946 B |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
@ -21,6 +21,85 @@ $ x := #table(columns: 2)[x][y]/mat(1, 2, 3)
|
||||
#let here = text.with(font: "Noto Sans")
|
||||
$#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 ---
|
||||
// Test boxes without a baseline act as if the baseline is at the base
|
||||
#{
|
||||
|
@ -28,3 +28,20 @@ $ bold(op("bold", limits: #true))_x y $
|
||||
--- math-non-math-content ---
|
||||
// With non-text content
|
||||
$ 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
|
||||
|
@ -17,6 +17,40 @@ $ nothing $
|
||||
$ "hi ∅ hey" $
|
||||
$ 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}]")),
|
||||
"New Computer Modern Math"
|
||||
),
|
||||
features: ("ss01",),
|
||||
)
|
||||
$ cal(P)_i (X) * cal(C)_1 $
|
||||
|
||||
--- math-font-warning ---
|
||||
#show math.equation: set text(font: "Libertinus Serif")
|
||||
// Warning: 1-14 current font is not designed for math
|
||||
// Hint: 1-14 rendering may be poor
|
||||
$ x + y = z $
|
||||
|
||||
--- math-font-error ---
|
||||
// Warning: 37-54 unknown font family: libertinus math
|
||||
#show math.equation: set text(font: "Libertinus Math", fallback: false)
|
||||
// Error: 1-37 no font could be found
|
||||
$ brace.double.l -1 brace.double.r $
|
||||
|
||||
--- math-font-fallback-class ---
|
||||
// Test that math class is preserved even when the result is a tofu.
|
||||
#show math.equation: set text(font: "Fira Math", fallback: false)
|
||||
$ brace.double.l -1 brace.double.r $
|
||||
$ lr(brace.double.l -1 brace.double.r) $
|
||||
|
||||
--- math-optical-size-nested-scripts ---
|
||||
// Test transition from script to scriptscript.
|
||||
#[
|
||||
|