Merge 0d0481ce5e494e6476b1d2078abeb096c18f8855 into ff0dc5ab6608504c802d6965587151caf2c757f6

This commit is contained in:
Max 2025-06-03 12:54:49 +00:00 committed by GitHub
commit f11e7671b0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 925 additions and 983 deletions

View File

@ -9,6 +9,7 @@ mod prepare;
mod shaping; mod shaping;
pub use self::box_::layout_box; pub use self::box_::layout_box;
pub use self::shaping::create_shape_plan;
use comemo::{Track, Tracked, TrackedMut}; use comemo::{Track, Tracked, TrackedMut};
use typst_library::diag::SourceResult; use typst_library::diag::SourceResult;

View File

@ -1,18 +1,16 @@
use std::borrow::Cow; use std::borrow::Cow;
use std::fmt::{self, Debug, Formatter}; use std::fmt::{self, Debug, Formatter};
use std::str::FromStr;
use std::sync::Arc; use std::sync::Arc;
use az::SaturatingAs; use az::SaturatingAs;
use ecow::EcoString;
use rustybuzz::{BufferFlags, ShapePlan, UnicodeBuffer}; use rustybuzz::{BufferFlags, ShapePlan, UnicodeBuffer};
use ttf_parser::Tag; use ttf_parser::Tag;
use typst_library::engine::Engine; use typst_library::engine::Engine;
use typst_library::foundations::{Smart, StyleChain}; use typst_library::foundations::{Smart, StyleChain};
use typst_library::layout::{Abs, Dir, Em, Frame, FrameItem, Point, Size}; use typst_library::layout::{Abs, Dir, Em, Frame, FrameItem, Point, Size};
use typst_library::text::{ use typst_library::text::{
families, features, is_default_ignorable, variant, Font, FontFamily, FontVariant, families, features, is_default_ignorable, language, variant, Font, FontFamily,
Glyph, Lang, Region, TextEdgeBounds, TextElem, TextItem, FontVariant, Glyph, Lang, Region, TextEdgeBounds, TextElem, TextItem,
}; };
use typst_library::World; use typst_library::World;
use typst_utils::SliceExt; use typst_utils::SliceExt;
@ -295,6 +293,8 @@ impl<'a> ShapedText<'a> {
+ justification_left + justification_left
+ justification_right, + justification_right,
x_offset: shaped.x_offset + justification_left, x_offset: shaped.x_offset + justification_left,
y_advance: Em::zero(),
y_offset: Em::zero(),
range: (shaped.range.start - range.start).saturating_as() range: (shaped.range.start - range.start).saturating_as()
..(shaped.range.end - range.start).saturating_as(), ..(shaped.range.end - range.start).saturating_as(),
span, span,
@ -934,7 +934,7 @@ fn shape_segment<'a>(
/// Create a shape plan. /// Create a shape plan.
#[comemo::memoize] #[comemo::memoize]
fn create_shape_plan( pub fn create_shape_plan(
font: &Font, font: &Font,
direction: rustybuzz::Direction, direction: rustybuzz::Direction,
script: rustybuzz::Script, script: rustybuzz::Script,
@ -1044,20 +1044,8 @@ fn calculate_adjustability(ctx: &mut ShapingContext, lang: Lang, region: Option<
/// Difference between non-breaking and normal space. /// Difference between non-breaking and normal space.
fn nbsp_delta(font: &Font) -> Option<Em> { fn nbsp_delta(font: &Font) -> Option<Em> {
let space = font.ttf().glyph_index(' ')?.0;
let nbsp = font.ttf().glyph_index('\u{00A0}')?.0; let nbsp = font.ttf().glyph_index('\u{00A0}')?.0;
Some(font.advance(nbsp)? - font.advance(space)?) Some(font.advance(nbsp)? - font.space_width()?)
}
/// Process the language and region of a style chain into a
/// rustybuzz-compatible BCP 47 language.
fn language(styles: StyleChain) -> rustybuzz::Language {
let mut bcp: EcoString = TextElem::lang_in(styles).as_str().into();
if let Some(region) = TextElem::region_in(styles) {
bcp.push('-');
bcp.push_str(region.as_str());
}
rustybuzz::Language::from_str(&bcp).unwrap()
} }
/// Returns true if all glyphs in `glyphs` have ranges within the range `range`. /// Returns true if all glyphs in `glyphs` have ranges within the range `range`.

View File

@ -3,7 +3,9 @@ use typst_library::foundations::{Packed, StyleChain};
use typst_library::layout::{Em, Frame, Point, Size}; use typst_library::layout::{Em, Frame, Point, Size};
use typst_library::math::{Accent, AccentElem}; use typst_library::math::{Accent, AccentElem};
use super::{style_cramped, FrameFragment, GlyphFragment, MathContext, MathFragment}; use super::{
style_cramped, style_dtls, style_flac, FrameFragment, MathContext, MathFragment,
};
/// How much the accent can be shorter than the base. /// How much the accent can be shorter than the base.
const ACCENT_SHORT_FALL: Em = Em::new(0.5); const ACCENT_SHORT_FALL: Em = Em::new(0.5);
@ -15,15 +17,12 @@ pub fn layout_accent(
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
let cramped = style_cramped(); // Try to replace the base glyph with its dotless variant.
let mut base = ctx.layout_into_fragment(&elem.base, styles.chain(&cramped))?; let dtls = style_dtls();
let base_styles = if elem.dotless(styles) { styles.chain(&dtls) } else { styles };
// Try to replace a glyph with its dotless variant. let cramped = style_cramped();
if elem.dotless(styles) { let base = ctx.layout_into_fragment(&elem.base, base_styles.chain(&cramped))?;
if let MathFragment::Glyph(glyph) = &mut base {
glyph.make_dotless_form(ctx);
}
}
// Preserve class to preserve automatic spacing. // Preserve class to preserve automatic spacing.
let base_class = base.class(); let base_class = base.class();
@ -32,26 +31,27 @@ pub fn layout_accent(
let width = elem.size(styles).relative_to(base.width()); let width = elem.size(styles).relative_to(base.width());
let Accent(c) = elem.accent; let Accent(c) = elem.accent;
let mut glyph = GlyphFragment::new(ctx, styles, c, elem.span()); let mut glyph = ctx.layout_into_glyph(c, elem.span(), styles)?;
let flattened_base_height = value!(glyph.text, flattened_accent_base_height);
let accent_base_height = value!(glyph.text, accent_base_height);
// Try to replace accent glyph with flattened variant. // Try to replace the accent glyph with its flattened variant.
let flattened_base_height = scaled!(ctx, styles, flattened_accent_base_height); let flac = style_flac();
if base.ascent() > flattened_base_height { if base.ascent() > flattened_base_height {
glyph.make_flattened_accent_form(ctx); glyph = ctx.layout_into_glyph(c, elem.span(), styles.chain(&flac))?;
} }
// Forcing the accent to be at least as large as the base makes it too // Forcing the accent to be at least as large as the base makes it too
// wide in many case. // wide in many case.
let short_fall = ACCENT_SHORT_FALL.at(glyph.font_size); let short_fall = ACCENT_SHORT_FALL.at(glyph.text.size);
let variant = glyph.stretch_horizontal(ctx, width, short_fall); glyph.stretch_horizontal(ctx, width, short_fall);
let accent = variant.frame; let accent_attach = glyph.accent_attach;
let accent_attach = variant.accent_attach; let accent = glyph.into_frame();
// 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 need // 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. // a larger gap so that the accent doesn't move too low.
let accent_base_height = scaled!(ctx, styles, accent_base_height);
let gap = -accent.descent() - base.ascent().min(accent_base_height); let gap = -accent.descent() - base.ascent().min(accent_base_height);
let size = Size::new(base.width(), accent.height() + gap + base.height()); let size = Size::new(base.width(), accent.height() + gap + base.height());
let accent_pos = Point::with_x(base_attach - accent_attach); let accent_pos = Point::with_x(base_attach - accent_attach);

View File

@ -1,14 +1,16 @@
use typst_library::diag::SourceResult; use typst_library::diag::SourceResult;
use typst_library::foundations::{Packed, StyleChain, SymbolElem}; use typst_library::foundations::{Packed, StyleChain};
use typst_library::layout::{Abs, Axis, Corner, Frame, Point, Rel, Size}; 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::{
stretch_fragment, style_for_subscript, style_for_superscript, FrameFragment, Limits, find_math_font, stretch_fragment, style_for_subscript, style_for_superscript,
MathContext, MathFragment, FrameFragment, Limits, MathContext, MathFragment,
}; };
macro_rules! measure { macro_rules! measure {
@ -66,7 +68,6 @@ pub fn layout_attach(
let relative_to_width = measure!(t, width).max(measure!(b, width)); let relative_to_width = measure!(t, width).max(measure!(b, width));
stretch_fragment( stretch_fragment(
ctx, ctx,
styles,
&mut base, &mut base,
Some(Axis::X), Some(Axis::X),
Some(relative_to_width), Some(relative_to_width),
@ -103,14 +104,12 @@ pub fn layout_primes(
4 => '⁗', 4 => '⁗',
_ => unreachable!(), _ => unreachable!(),
}; };
let f = ctx.layout_into_fragment(&SymbolElem::packed(c), styles)?; let f = ctx.layout_into_glyph(c, 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_glyph('', elem.span(), styles)?.into_frame();
.layout_into_fragment(&SymbolElem::packed(''), styles)?
.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()));
frame.set_baseline(prime.ascent()); frame.set_baseline(prime.ascent());
@ -174,18 +173,21 @@ fn layout_attachments(
) -> SourceResult<()> { ) -> SourceResult<()> {
let base_class = base.class(); let base_class = base.class();
// TODO: should use base's font.
let font = find_math_font(ctx.engine, styles, Span::detached())?;
// 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, styles, &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, styles, &base, [t.as_ref(), b.as_ref()]);
// Calculate the final frame height. // Calculate the final frame height.
let ascent = base let ascent = base
@ -215,12 +217,11 @@ 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 = constant!(font, styles, space_after_script);
// 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.
let (tl_pre_width, bl_pre_width) = compute_pre_script_widths( let (tl_pre_width, bl_pre_width) = compute_pre_script_widths(
ctx,
&base, &base,
[tl.as_ref(), bl.as_ref()], [tl.as_ref(), bl.as_ref()],
(tx_shift, bx_shift), (tx_shift, bx_shift),
@ -231,7 +232,6 @@ fn layout_attachments(
// base's width. Also calculate each post-script's kerning (we need this for // base's width. Also calculate each post-script's kerning (we need this for
// its position later). // its position later).
let ((tr_post_width, tr_kern), (br_post_width, br_kern)) = compute_post_script_widths( let ((tr_post_width, tr_kern), (br_post_width, br_kern)) = compute_post_script_widths(
ctx,
&base, &base,
[tr.as_ref(), br.as_ref()], [tr.as_ref(), br.as_ref()],
(tx_shift, bx_shift), (tx_shift, bx_shift),
@ -287,14 +287,13 @@ fn layout_attachments(
/// post-script's kerning value. The first tuple is for the post-superscript, /// post-script's kerning value. The first tuple is for the post-superscript,
/// and the second is for the post-subscript. /// and the second is for the post-subscript.
fn compute_post_script_widths( fn compute_post_script_widths(
ctx: &MathContext,
base: &MathFragment, base: &MathFragment,
[tr, br]: [Option<&MathFragment>; 2], [tr, br]: [Option<&MathFragment>; 2],
(tr_shift, br_shift): (Abs, Abs), (tr_shift, br_shift): (Abs, Abs),
space_after_post_script: Abs, space_after_post_script: Abs,
) -> ((Abs, Abs), (Abs, Abs)) { ) -> ((Abs, Abs), (Abs, Abs)) {
let tr_values = tr.map_or_default(|tr| { let tr_values = tr.map_or_default(|tr| {
let kern = math_kern(ctx, base, tr, tr_shift, Corner::TopRight); let kern = math_kern(base, tr, tr_shift, Corner::TopRight);
(space_after_post_script + tr.width() + kern, kern) (space_after_post_script + tr.width() + kern, kern)
}); });
@ -302,7 +301,7 @@ fn compute_post_script_widths(
// need to shift the post-subscript left by the base's italic correction // need to shift the post-subscript left by the base's italic correction
// (see the kerning algorithm as described in the OpenType MATH spec). // (see the kerning algorithm as described in the OpenType MATH spec).
let br_values = br.map_or_default(|br| { let br_values = br.map_or_default(|br| {
let kern = math_kern(ctx, base, br, br_shift, Corner::BottomRight) let kern = math_kern(base, br, br_shift, Corner::BottomRight)
- base.italics_correction(); - base.italics_correction();
(space_after_post_script + br.width() + kern, kern) (space_after_post_script + br.width() + kern, kern)
}); });
@ -317,19 +316,18 @@ fn compute_post_script_widths(
/// extends left of the base's width and the second being the distance the /// extends left of the base's width and the second being the distance the
/// pre-subscript extends left of the base's width. /// pre-subscript extends left of the base's width.
fn compute_pre_script_widths( fn compute_pre_script_widths(
ctx: &MathContext,
base: &MathFragment, base: &MathFragment,
[tl, bl]: [Option<&MathFragment>; 2], [tl, bl]: [Option<&MathFragment>; 2],
(tl_shift, bl_shift): (Abs, Abs), (tl_shift, bl_shift): (Abs, Abs),
space_before_pre_script: Abs, space_before_pre_script: Abs,
) -> (Abs, Abs) { ) -> (Abs, Abs) {
let tl_pre_width = tl.map_or_default(|tl| { let tl_pre_width = tl.map_or_default(|tl| {
let kern = math_kern(ctx, base, tl, tl_shift, Corner::TopLeft); let kern = math_kern(base, tl, tl_shift, Corner::TopLeft);
space_before_pre_script + tl.width() + kern space_before_pre_script + tl.width() + kern
}); });
let bl_pre_width = bl.map_or_default(|bl| { let bl_pre_width = bl.map_or_default(|bl| {
let kern = math_kern(ctx, base, bl, bl_shift, Corner::BottomLeft); let kern = math_kern(base, bl, bl_shift, Corner::BottomLeft);
space_before_pre_script + bl.width() + kern space_before_pre_script + bl.width() + kern
}); });
@ -368,7 +366,7 @@ 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, styles: StyleChain,
base: &MathFragment, base: &MathFragment,
[t, b]: [Option<&MathFragment>; 2], [t, b]: [Option<&MathFragment>; 2],
@ -379,14 +377,14 @@ fn compute_limit_shifts(
// 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 = constant!(font, styles, upper_limit_gap_min);
let upper_rise_min = scaled!(ctx, styles, upper_limit_baseline_rise_min); let upper_rise_min = constant!(font, styles, upper_limit_baseline_rise_min);
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 = constant!(font, styles, lower_limit_gap_min);
let lower_drop_min = scaled!(ctx, styles, lower_limit_baseline_drop_min); let lower_drop_min = constant!(font, styles, lower_limit_baseline_drop_min);
base.descent() + lower_drop_min.max(lower_gap_min + b.ascent()) base.descent() + lower_drop_min.max(lower_gap_min + b.ascent())
}); });
@ -397,25 +395,25 @@ 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, styles: StyleChain,
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 EquationElem::cramped_in(styles) { let sup_shift_up = if EquationElem::cramped_in(styles) {
scaled!(ctx, styles, superscript_shift_up_cramped) constant!(font, styles, superscript_shift_up_cramped)
} else { } else {
scaled!(ctx, styles, superscript_shift_up) constant!(font, styles, superscript_shift_up)
}; };
let sup_bottom_min = scaled!(ctx, styles, superscript_bottom_min); let sup_bottom_min = constant!(font, styles, superscript_bottom_min);
let sup_bottom_max_with_sub = let sup_bottom_max_with_sub =
scaled!(ctx, styles, superscript_bottom_max_with_subscript); constant!(font, styles, superscript_bottom_max_with_subscript);
let sup_drop_max = scaled!(ctx, styles, superscript_baseline_drop_max); let sup_drop_max = constant!(font, styles, superscript_baseline_drop_max);
let gap_min = scaled!(ctx, styles, sub_superscript_gap_min); let gap_min = constant!(font, styles, sub_superscript_gap_min);
let sub_shift_down = scaled!(ctx, styles, subscript_shift_down); let sub_shift_down = constant!(font, styles, subscript_shift_down);
let sub_top_max = scaled!(ctx, styles, subscript_top_max); let sub_top_max = constant!(font, styles, subscript_top_max);
let sub_drop_min = scaled!(ctx, styles, subscript_baseline_drop_min); let sub_drop_min = constant!(font, styles, subscript_baseline_drop_min);
let mut shift_up = Abs::zero(); let mut shift_up = Abs::zero();
let mut shift_down = Abs::zero(); let mut shift_down = Abs::zero();
@ -467,13 +465,7 @@ fn compute_script_shifts(
/// a negative value means shifting the script closer to the base. Requires the /// a negative value means shifting the script closer to the base. Requires the
/// distance from the base's baseline to the script's baseline, as well as the /// distance from the base's baseline to the script's baseline, as well as the
/// script's corner (tl, tr, bl, br). /// script's corner (tl, tr, bl, br).
fn math_kern( fn math_kern(base: &MathFragment, script: &MathFragment, shift: Abs, pos: Corner) -> Abs {
ctx: &MathContext,
base: &MathFragment,
script: &MathFragment,
shift: Abs,
pos: Corner,
) -> Abs {
// This process is described under the MathKernInfo table in the OpenType // This process is described under the MathKernInfo table in the OpenType
// MATH spec. // MATH spec.
@ -498,8 +490,8 @@ fn math_kern(
// Calculate the sum of kerning values for each correction height. // Calculate the sum of kerning values for each correction height.
let summed_kern = |height| { let summed_kern = |height| {
let base_kern = base.kern_at_height(ctx, pos, height); let base_kern = base.kern_at_height(pos, height);
let attach_kern = script.kern_at_height(ctx, pos.inv(), height); let attach_kern = script.kern_at_height(pos.inv(), height);
base_kern + attach_kern base_kern + attach_kern
}; };

View File

@ -7,7 +7,7 @@ use typst_library::visualize::{FixedStroke, Geometry};
use typst_syntax::Span; use typst_syntax::Span;
use super::{ use super::{
style_for_denominator, style_for_numerator, FrameFragment, GlyphFragment, find_math_font, style_for_denominator, style_for_numerator, FrameFragment,
MathContext, DELIM_SHORT_FALL, MathContext, DELIM_SHORT_FALL,
}; };
@ -49,29 +49,30 @@ 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, styles, span)?;
let axis = scaled!(ctx, styles, axis_height); let axis = constant!(font, styles, axis_height);
let thickness = scaled!(ctx, styles, fraction_rule_thickness); let thickness = constant!(font, styles, fraction_rule_thickness);
let shift_up = scaled!( let shift_up = constant!(
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!( let shift_down = constant!(
ctx, styles, 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!( let num_min = constant!(
ctx, styles, 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!( let denom_min = constant!(
ctx, styles, 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,
); );
let short_fall = DELIM_SHORT_FALL.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))?;
@ -109,14 +110,14 @@ 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(ctx, styles, '(', span) let mut left = ctx.layout_into_glyph('(', span, styles)?;
.stretch_vertical(ctx, height, short_fall); left.stretch_vertical(ctx, height, short_fall);
left.center_on_axis(ctx); 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(ctx, styles, ')', span) let mut right = ctx.layout_into_glyph(')', span, styles)?;
.stretch_vertical(ctx, height, short_fall); right.stretch_vertical(ctx, height, short_fall);
right.center_on_axis(ctx); right.center_on_axis();
ctx.push(right); ctx.push(right);
} else { } else {
frame.push( frame.push(

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,9 @@ use typst_library::math::{EquationElem, LrElem, MidElem};
use typst_utils::SliceExt; use typst_utils::SliceExt;
use unicode_math_class::MathClass; use unicode_math_class::MathClass;
use super::{stretch_fragment, MathContext, MathFragment, DELIM_SHORT_FALL}; use super::{
find_math_font, stretch_fragment, MathContext, MathFragment, DELIM_SHORT_FALL,
};
/// Lays out an [`LrElem`]. /// Lays out an [`LrElem`].
#[typst_macros::time(name = "math.lr", span = elem.span())] #[typst_macros::time(name = "math.lr", span = elem.span())]
@ -33,7 +35,8 @@ 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 font = find_math_font(ctx.engine, styles, elem.span())?;
let axis = constant!(font, styles, axis_height);
let max_extent = inner_fragments let max_extent = inner_fragments
.iter() .iter()
.map(|fragment| (fragment.ascent() - axis).max(fragment.descent() + axis)) .map(|fragment| (fragment.ascent() - axis).max(fragment.descent() + axis))
@ -45,20 +48,20 @@ pub fn layout_lr(
// Scale up fragments at both ends. // Scale up fragments at both ends.
match inner_fragments { match inner_fragments {
[one] => scale(ctx, styles, one, relative_to, height, None), [one] => scale(ctx, one, relative_to, height, None),
[first, .., last] => { [first, .., last] => {
scale(ctx, styles, first, relative_to, height, Some(MathClass::Opening)); scale(ctx, first, relative_to, height, Some(MathClass::Opening));
scale(ctx, styles, last, relative_to, height, Some(MathClass::Closing)); scale(ctx, last, relative_to, height, Some(MathClass::Closing));
} }
_ => {} _ => {}
} }
// Handle MathFragment::Variant fragments that should be scaled up. // Handle MathFragment::Glyph fragments that should be scaled up.
for fragment in inner_fragments.iter_mut() { for fragment in inner_fragments.iter_mut() {
if let MathFragment::Variant(ref mut variant) = fragment { if let MathFragment::Glyph(ref mut glyph) = fragment {
if variant.mid_stretched == Some(false) { if glyph.mid_stretched == Some(false) {
variant.mid_stretched = Some(true); glyph.mid_stretched = Some(true);
scale(ctx, styles, fragment, relative_to, height, Some(MathClass::Large)); scale(ctx, fragment, relative_to, height, Some(MathClass::Large));
} }
} }
} }
@ -95,18 +98,9 @@ pub fn layout_mid(
let mut fragments = ctx.layout_into_fragments(&elem.body, styles)?; let mut fragments = ctx.layout_into_fragments(&elem.body, styles)?;
for fragment in &mut fragments { for fragment in &mut fragments {
match fragment { if let MathFragment::Glyph(ref mut glyph) = fragment {
MathFragment::Glyph(glyph) => { glyph.mid_stretched = Some(false);
let mut new = glyph.clone().into_variant(); glyph.class = MathClass::Fence;
new.mid_stretched = Some(false);
new.class = MathClass::Fence;
*fragment = MathFragment::Variant(new);
}
MathFragment::Variant(variant) => {
variant.mid_stretched = Some(false);
variant.class = MathClass::Fence;
}
_ => {}
} }
} }
@ -117,7 +111,6 @@ pub fn layout_mid(
/// Scale a math fragment to a height. /// Scale a math fragment to a height.
fn scale( fn scale(
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain,
fragment: &mut MathFragment, fragment: &mut MathFragment,
relative_to: Abs, relative_to: Abs,
height: Rel<Abs>, height: Rel<Abs>,
@ -132,7 +125,6 @@ fn scale(
let short_fall = DELIM_SHORT_FALL.at(fragment.font_size().unwrap_or_default()); let short_fall = DELIM_SHORT_FALL.at(fragment.font_size().unwrap_or_default());
stretch_fragment( stretch_fragment(
ctx, ctx,
styles,
fragment, fragment,
Some(Axis::Y), Some(Axis::Y),
Some(relative_to), Some(relative_to),

View File

@ -1,5 +1,5 @@
use typst_library::diag::{bail, warning, SourceResult}; use typst_library::diag::{bail, warning, SourceResult};
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,
}; };
@ -9,8 +9,8 @@ use typst_library::visualize::{FillRule, FixedStroke, Geometry, LineCap, Shape};
use typst_syntax::Span; use typst_syntax::Span;
use super::{ use super::{
alignments, delimiter_alignment, style_for_denominator, AlignmentResult, alignments, delimiter_alignment, find_math_font, style_for_denominator,
FrameFragment, GlyphFragment, LeftRightAlternator, MathContext, DELIM_SHORT_FALL, AlignmentResult, FrameFragment, LeftRightAlternator, MathContext, DELIM_SHORT_FALL,
}; };
const VERTICAL_PADDING: Ratio = Ratio::new(0.1); const VERTICAL_PADDING: Ratio = Ratio::new(0.1);
@ -184,7 +184,7 @@ fn layout_body(
// 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 = let paren =
GlyphFragment::new(ctx, styles.chain(&denom_style), '(', Span::detached()); ctx.layout_into_fragment(&SymbolElem::packed('('), styles.chain(&denom_style))?;
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) {
@ -202,8 +202,8 @@ fn layout_body(
)); ));
} }
ascent.set_max(cell.ascent().max(paren.ascent)); ascent.set_max(cell.ascent().max(paren.ascent()));
descent.set_max(cell.descent().max(paren.descent)); descent.set_max(cell.descent().max(paren.descent()));
col.push(cell); col.push(cell);
} }
@ -307,24 +307,25 @@ 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, styles, span)?;
let axis = constant!(font, styles, axis_height);
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) = left { if let Some(left_c) = left {
let mut left = GlyphFragment::new(ctx, styles, left, span) let mut left = ctx.layout_into_glyph(left_c, span, styles)?;
.stretch_vertical(ctx, target, short_fall); left.stretch_vertical(ctx, target, short_fall);
left.align_on_axis(ctx, delimiter_alignment(left.c)); left.align_on_axis(delimiter_alignment(left_c));
ctx.push(left); ctx.push(left);
} }
ctx.push(FrameFragment::new(styles, frame)); ctx.push(FrameFragment::new(styles, frame));
if let Some(right) = right { if let Some(right_c) = right {
let mut right = GlyphFragment::new(ctx, styles, right, span) let mut right = ctx.layout_into_glyph(right_c, span, styles)?;
.stretch_vertical(ctx, target, short_fall); right.stretch_vertical(ctx, target, short_fall);
right.align_on_axis(ctx, delimiter_alignment(right.c)); right.align_on_axis(delimiter_alignment(right_c));
ctx.push(right); ctx.push(right);
} }

View File

@ -13,9 +13,7 @@ mod stretch;
mod text; mod text;
mod underover; mod underover;
use rustybuzz::Feature; use typst_library::diag::SourceResult;
use ttf_parser::Tag;
use typst_library::diag::{bail, SourceResult};
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,
@ -29,20 +27,17 @@ 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, SpaceElem, TextEdgeBounds, TextElem};
families, features, variant, Font, LinebreakElem, SpaceElem, TextEdgeBounds, TextElem,
};
use typst_library::World;
use typst_syntax::Span; use typst_syntax::Span;
use typst_utils::Numeric; use typst_utils::Numeric;
use unicode_math_class::MathClass; use unicode_math_class::MathClass;
use self::fragment::{ use self::fragment::{
FrameFragment, GlyphFragment, GlyphwiseSubsts, Limits, MathFragment, VariantFragment, has_dtls_feat, stretch_axes, FrameFragment, GlyphFragment, Limits, MathFragment,
}; };
use self::run::{LeftRightAlternator, MathRun, MathRunFrameBuilder}; use self::run::{LeftRightAlternator, MathRun, MathRunFrameBuilder};
use self::shared::*; use self::shared::*;
use self::stretch::{stretch_fragment, stretch_glyph}; use self::stretch::stretch_fragment;
/// Layout an inline equation (in a paragraph). /// Layout an inline equation (in a paragraph).
#[typst_macros::time(span = elem.span())] #[typst_macros::time(span = elem.span())]
@ -55,12 +50,11 @@ pub fn layout_equation_inline(
) -> SourceResult<Vec<InlineItem>> { ) -> SourceResult<Vec<InlineItem>> {
assert!(!elem.block(styles)); assert!(!elem.block(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, styles, 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, 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)?;
@ -110,12 +104,12 @@ pub fn layout_equation_block(
assert!(elem.block(styles)); assert!(elem.block(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, styles, 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, 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
@ -236,24 +230,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,
@ -372,16 +348,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,
ttf: &'a ttf_parser::Face<'a>,
table: ttf_parser::math::Table<'a>,
constants: ttf_parser::math::Constants<'a>,
dtls_table: Option<GlyphwiseSubsts<'a>>,
flac_table: Option<GlyphwiseSubsts<'a>>,
ssty_table: Option<GlyphwiseSubsts<'a>>,
glyphwise_tables: Option<Vec<GlyphwiseSubsts<'a>>>,
space_width: Em,
// Mutable. // Mutable.
fragments: Vec<MathFragment>, fragments: Vec<MathFragment>,
} }
@ -391,46 +357,12 @@ impl<'a, 'v, 'e> MathContext<'a, 'v, 'e> {
fn new( fn new(
engine: &'v mut Engine<'e>, engine: &'v mut Engine<'e>,
locator: &'v mut SplitLocator<'a>, locator: &'v mut SplitLocator<'a>,
styles: StyleChain<'a>,
base: Size, base: Size,
font: &'a Font,
) -> Self { ) -> Self {
let math_table = font.ttf().tables().math.unwrap();
let gsub_table = font.ttf().tables().gsub;
let constants = math_table.constants.unwrap();
let feat = |tag: &[u8; 4]| {
GlyphwiseSubsts::new(gsub_table, Feature::new(Tag::from_bytes(tag), 0, ..))
};
let features = features(styles);
let glyphwise_tables = Some(
features
.into_iter()
.filter_map(|feature| GlyphwiseSubsts::new(gsub_table, feature))
.collect(),
);
let ttf = font.ttf();
let space_width = ttf
.glyph_index(' ')
.and_then(|id| ttf.glyph_hor_advance(id))
.map(|advance| font.to_em(advance))
.unwrap_or(THICK);
Self { Self {
engine, engine,
locator, locator,
region: Region::new(base, Axes::splat(false)), region: Region::new(base, Axes::splat(false)),
font,
ttf,
table: math_table,
constants,
dtls_table: feat(b"dtls"),
flac_table: feat(b"flac"),
ssty_table: feat(b"ssty"),
glyphwise_tables,
space_width,
fragments: vec![], fragments: vec![],
} }
} }
@ -469,6 +401,23 @@ impl<'a, 'v, 'e> MathContext<'a, 'v, 'e> {
Ok(std::mem::replace(&mut self.fragments, prev)) Ok(std::mem::replace(&mut self.fragments, prev))
} }
fn layout_into_glyph(
&mut self,
c: char,
span: Span,
styles: StyleChain,
) -> SourceResult<GlyphFragment> {
let prev = std::mem::take(&mut self.fragments);
let elem = SymbolElem::packed(c).spanned(span);
self.layout_into_self(&elem, styles)?;
let MathFragment::Glyph(glyph) =
std::mem::replace(&mut self.fragments, prev).remove(0)
else {
unreachable!()
};
Ok(glyph)
}
/// Layout the given element and return the result as a /// Layout the given element and return the result as a
/// unified [`MathFragment`]. /// unified [`MathFragment`].
fn layout_into_fragment( fn layout_into_fragment(
@ -504,15 +453,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 && TextElem::font_in(styles) != TextElem::font_in(outer) {
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)?;
} }
@ -529,7 +470,9 @@ 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>() {
ctx.push(MathFragment::Space(ctx.space_width.resolve(styles))); let font = find_math_font(ctx.engine, styles, elem.span())?;
let space_width = font.space_width().unwrap_or(THICK).resolve(styles);
ctx.push(MathFragment::Space(space_width));
} else if elem.is::<LinebreakElem>() { } else if elem.is::<LinebreakElem>() {
ctx.push(MathFragment::Linebreak); ctx.push(MathFragment::Linebreak);
} else if let Some(elem) = elem.to_packed::<HElem>() { } else if let Some(elem) = elem.to_packed::<HElem>() {
@ -599,7 +542,8 @@ fn layout_realized(
} 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() {
let axis = scaled!(ctx, styles, axis_height); let font = find_math_font(ctx.engine, styles, elem.span())?;
let axis = constant!(font, styles, axis_height);
frame.set_baseline(frame.height() / 2.0 + axis); frame.set_baseline(frame.height() / 2.0 + axis);
} }
ctx.push( ctx.push(

View File

@ -2,10 +2,9 @@ use typst_library::diag::SourceResult;
use typst_library::foundations::{Packed, StyleChain}; use typst_library::foundations::{Packed, StyleChain};
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::visualize::{FixedStroke, Geometry}; use typst_library::visualize::{FixedStroke, Geometry};
use super::{style_cramped, FrameFragment, GlyphFragment, MathContext}; use super::{find_math_font, style_cramped, FrameFragment, MathContext};
/// Lays out a [`RootElem`]. /// Lays out a [`RootElem`].
/// ///
@ -17,20 +16,8 @@ pub fn layout_root(
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
let index = elem.index(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();
@ -39,23 +26,41 @@ pub fn layout_root(
let multiline = run.is_multiline(); let multiline = run.is_multiline();
let mut radicand = run.into_fragment(styles).into_frame(); let mut radicand = run.into_fragment(styles).into_frame();
if multiline { if multiline {
let font = find_math_font(ctx.engine, styles, elem.span())?;
let axis = constant!(font, styles, axis_height);
// Align the frame center line with the math axis. // Align the frame center line with the math axis.
radicand.set_baseline( radicand.set_baseline(radicand.height() / 2.0 + axis);
radicand.height() / 2.0 + scaled!(ctx, styles, axis_height),
);
} }
radicand radicand
}; };
// Layout root symbol. // Layout root symbol.
let mut sqrt = ctx.layout_into_glyph('√', span, styles)?;
let gap = value!(
sqrt.text, styles,
inline: radical_vertical_gap,
display: radical_display_style_vertical_gap,
);
let thickness = value!(sqrt.text, radical_rule_thickness);
let extra_ascender = value!(sqrt.text, radical_extra_ascender);
let kern_before = value!(sqrt.text, radical_kern_before_degree);
let kern_after = value!(sqrt.text, radical_kern_after_degree);
let raise_factor = percent!(sqrt.text, radical_degree_bottom_raise_percent);
let line = FrameItem::Shape(
Geometry::Line(Point::with_x(radicand.width()))
.stroked(FixedStroke::from_pair(sqrt.text.fill.clone(), thickness)),
span,
);
let target = radicand.height() + thickness + gap; let target = radicand.height() + thickness + gap;
let sqrt = GlyphFragment::new(ctx, styles, '√', span) sqrt.stretch_vertical(ctx, target, Abs::zero());
.stretch_vertical(ctx, target, Abs::zero()) let sqrt = sqrt.into_frame();
.frame;
// Layout the index. // Layout the index.
let sscript = EquationElem::set_size(MathSize::ScriptScript).wrap(); let sscript = EquationElem::set_size(MathSize::ScriptScript).wrap();
let index = index let index = elem
.index(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 +112,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(
TextElem::fill_in(styles).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,57 +1,103 @@
use ttf_parser::math::MathValue; use ttf_parser::Tag;
use typst_library::diag::{bail, SourceResult};
use typst_library::engine::Engine;
use typst_library::foundations::{Style, StyleChain}; use typst_library::foundations::{Style, StyleChain};
use typst_library::layout::{Abs, Em, FixedAlignment, Frame, Point, Size, VAlignment}; use typst_library::layout::{Abs, Em, FixedAlignment, Frame, Point, Size, VAlignment};
use typst_library::math::{EquationElem, MathSize}; use typst_library::math::{EquationElem, MathSize};
use typst_library::text::{families, variant, Font, FontFeatures, TextElem};
use typst_library::World;
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! percent {
($ctx:expr, $styles:expr, text: $text:ident, display: $display:ident $(,)?) => { ($text:expr, $name:ident) => {
match typst_library::math::EquationElem::size_in($styles) { $text
typst_library::math::MathSize::Display => scaled!($ctx, $styles, $display), .font
_ => scaled!($ctx, $styles, $text), .ttf()
} .tables()
}; .math
($ctx:expr, $styles:expr, $name:ident) => { .and_then(|math| math.constants)
$crate::math::Scaled::scaled( .map(|constants| constants.$name())
$ctx.constants.$name(), .unwrap() as f64
$ctx, / 100.0
typst_library::text::TextElem::size_in($styles),
)
}; };
} }
macro_rules! percent { macro_rules! word {
($ctx:expr, $name:ident) => { ($text:expr, $name:ident) => {
$ctx.constants.$name() as f64 / 100.0 $text
.font
.ttf()
.tables()
.math
.and_then(|math| math.constants)
.map(|constants| $text.font.to_em(constants.$name()).at($text.size))
.unwrap()
};
}
macro_rules! value {
($text:expr, $styles:expr, inline: $inline:ident, display: $display:ident $(,)?) => {
match typst_library::math::EquationElem::size_in($styles) {
typst_library::math::MathSize::Display => value!($text, $display),
_ => value!($text, $inline),
}
};
($text:expr, $name:ident) => {
$text
.font
.ttf()
.tables()
.math
.and_then(|math| math.constants)
.map(|constants| $text.font.to_em(constants.$name().value).at($text.size))
.unwrap()
};
}
macro_rules! constant {
($font:expr, $styles:expr, text: $text:ident, display: $display:ident $(,)?) => {
match typst_library::math::EquationElem::size_in($styles) {
typst_library::math::MathSize::Display => constant!($font, $styles, $display),
_ => constant!($font, $styles, $text),
}
};
($font:expr, $styles:expr, $name:ident) => {
typst_library::foundations::Resolve::resolve(
$font
.ttf()
.tables()
.math
.and_then(|math| math.constants)
.map(|constants| $font.to_em(constants.$name().value))
.unwrap(),
$styles,
)
}; };
} }
/// How much less high scaled delimiters can be than what they wrap. /// How much less high scaled delimiters can be than what they wrap.
pub const DELIM_SHORT_FALL: Em = Em::new(0.1); pub const DELIM_SHORT_FALL: Em = Em::new(0.1);
/// Converts some unit to an absolute length with the current font & font size. pub fn find_math_font(
pub trait Scaled { engine: &mut Engine<'_>,
fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs; styles: StyleChain,
} span: Span,
) -> SourceResult<Font> {
impl Scaled for i16 { let variant = variant(styles);
fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs { let world = engine.world;
ctx.font.to_em(self).at(font_size) 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?;
impl Scaled for u16 { // Take the base font as the "main" math font.
fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs { family.covers().map_or(Some(font), |_| None)
ctx.font.to_em(self).at(font_size) }) else {
} bail!(span, "current font does not support math");
} };
Ok(font)
impl Scaled for MathValue<'_> {
fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs {
self.value.scaled(ctx, font_size)
}
} }
/// Styles something as cramped. /// Styles something as cramped.
@ -59,6 +105,14 @@ pub fn style_cramped() -> LazyHash<Style> {
EquationElem::set_cramped(true).wrap() EquationElem::set_cramped(true).wrap()
} }
pub fn style_flac() -> LazyHash<Style> {
TextElem::set_features(FontFeatures(vec![(Tag::from_bytes(b"flac"), 1)])).wrap()
}
pub fn style_dtls() -> LazyHash<Style> {
TextElem::set_features(FontFeatures(vec![(Tag::from_bytes(b"dtls"), 1)])).wrap()
}
/// The style for subscripts in the current style. /// The style for subscripts in the current style.
pub fn style_for_subscript(styles: StyleChain) -> [LazyHash<Style>; 2] { pub fn style_for_subscript(styles: StyleChain) -> [LazyHash<Style>; 2] {
[style_for_superscript(styles), EquationElem::set_cramped(true).wrap()] [style_for_superscript(styles), EquationElem::set_cramped(true).wrap()]
@ -89,10 +143,11 @@ 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::set_script_scale(( EquationElem::set_script_scale((
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

@ -1,19 +1,10 @@
use ttf_parser::math::{GlyphAssembly, GlyphConstruction, GlyphPart};
use ttf_parser::LazyArray16;
use typst_library::diag::{warning, SourceResult}; use typst_library::diag::{warning, SourceResult};
use typst_library::foundations::{Packed, StyleChain}; use typst_library::foundations::{Packed, StyleChain};
use typst_library::layout::{Abs, Axis, Frame, Point, Rel, Size}; use typst_library::layout::{Abs, Axis, Rel};
use typst_library::math::StretchElem; use typst_library::math::StretchElem;
use typst_utils::Get; use typst_utils::Get;
use super::{ use super::{stretch_axes, MathContext, MathFragment};
delimiter_alignment, GlyphFragment, MathContext, MathFragment, Scaled,
VariantFragment,
};
use crate::modifiers::FrameModify;
/// Maximum number of times extenders can be repeated.
const MAX_REPEATS: usize = 1024;
/// Lays out a [`StretchElem`]. /// Lays out a [`StretchElem`].
#[typst_macros::time(name = "math.stretch", span = elem.span())] #[typst_macros::time(name = "math.stretch", span = elem.span())]
@ -23,15 +14,7 @@ pub fn layout_stretch(
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
let mut fragment = ctx.layout_into_fragment(&elem.body, styles)?; let mut fragment = ctx.layout_into_fragment(&elem.body, styles)?;
stretch_fragment( stretch_fragment(ctx, &mut fragment, None, None, elem.size(styles), Abs::zero());
ctx,
styles,
&mut fragment,
None,
None,
elem.size(styles),
Abs::zero(),
);
ctx.push(fragment); ctx.push(fragment);
Ok(()) Ok(())
} }
@ -39,269 +22,50 @@ pub fn layout_stretch(
/// Attempts to stretch the given fragment by/to the amount given in stretch. /// Attempts to stretch the given fragment by/to the amount given in stretch.
pub fn stretch_fragment( pub fn stretch_fragment(
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain,
fragment: &mut MathFragment, fragment: &mut MathFragment,
axis: Option<Axis>, axis: Option<Axis>,
relative_to: Option<Abs>, relative_to: Option<Abs>,
stretch: Rel<Abs>, stretch: Rel<Abs>,
short_fall: Abs, short_fall: Abs,
) { ) {
let glyph = match fragment { let size = fragment.size();
MathFragment::Glyph(glyph) => glyph.clone(),
MathFragment::Variant(variant) => { let MathFragment::Glyph(ref mut glyph) = fragment else { return };
GlyphFragment::new(ctx, styles, variant.c, variant.span)
}
_ => return,
};
// Return if we attempt to stretch along an axis which isn't stretchable, // Return if we attempt to stretch along an axis which isn't stretchable,
// so that the original fragment isn't modified. // so that the original fragment isn't modified.
let Some(stretch_axis) = stretch_axis(ctx, &glyph) else { return }; let axes = stretch_axes(&glyph.text.font, glyph.base_id);
let axis = axis.unwrap_or(stretch_axis); let stretch_axis = if let Some(axis) = axis {
if axis != stretch_axis { if !axes.get(axis) {
return; return;
}
let relative_to_size = relative_to.unwrap_or_else(|| fragment.size().get(axis));
let mut variant = stretch_glyph(
ctx,
glyph,
stretch.relative_to(relative_to_size),
short_fall,
axis,
);
if axis == Axis::Y {
variant.align_on_axis(ctx, delimiter_alignment(variant.c));
}
*fragment = MathFragment::Variant(variant);
}
/// Return whether the glyph is stretchable and if it is, along which axis it
/// can be stretched.
fn stretch_axis(ctx: &mut MathContext, base: &GlyphFragment) -> Option<Axis> {
let base_id = base.id;
let vertical = ctx
.table
.variants
.and_then(|variants| variants.vertical_constructions.get(base_id))
.map(|_| Axis::Y);
let horizontal = ctx
.table
.variants
.and_then(|variants| variants.horizontal_constructions.get(base_id))
.map(|_| Axis::X);
match (vertical, horizontal) {
(vertical, None) => vertical,
(None, horizontal) => horizontal,
_ => {
// As far as we know, there aren't any glyphs that have both
// vertical and horizontal constructions. So for the time being, we
// will assume that a glyph cannot have both.
ctx.engine.sink.warn(warning!(
base.span,
"glyph has both vertical and horizontal constructions";
hint: "this is probably a font bug";
hint: "please file an issue at https://github.com/typst/typst/issues"
));
None
} }
} axis
} } else {
match (axes.x, axes.y) {
/// Try to stretch a glyph to a desired width or height. (true, false) => Axis::X,
/// (false, true) => Axis::Y,
/// The resulting frame may not have the exact desired width. (false, false) => return,
pub fn stretch_glyph( (true, true) => {
ctx: &mut MathContext, // As far as we know, there aren't any glyphs that have both
mut base: GlyphFragment, // vertical and horizontal constructions. So for the time being, we
target: Abs, // will assume that a glyph cannot have both.
short_fall: Abs, ctx.engine.sink.warn(warning!(
axis: Axis, glyph.text.glyphs[0].span.0,
) -> VariantFragment { "glyph has both vertical and horizontal constructions";
// If the base glyph is good enough, use it. hint: "this is probably a font bug";
let advance = match axis { hint: "please file an issue at https://github.com/typst/typst/issues"
Axis::X => base.width, ));
Axis::Y => base.height(), return;
};
let short_target = target - short_fall;
if short_target <= advance {
return base.into_variant();
}
let mut min_overlap = Abs::zero();
let construction = ctx
.table
.variants
.and_then(|variants| {
min_overlap = variants.min_connector_overlap.scaled(ctx, base.font_size);
match axis {
Axis::X => variants.horizontal_constructions,
Axis::Y => variants.vertical_constructions,
} }
.get(base.id)
})
.unwrap_or(GlyphConstruction { assembly: None, variants: LazyArray16::new(&[]) });
// Search for a pre-made variant with a good advance.
let mut best_id = base.id;
let mut best_advance = base.width;
for variant in construction.variants {
best_id = variant.variant_glyph;
best_advance = base.font.to_em(variant.advance_measurement).at(base.font_size);
if short_target <= best_advance {
break;
} }
}
// This is either good or the best we've got.
if short_target <= best_advance || construction.assembly.is_none() {
base.set_id(ctx, best_id);
return base.into_variant();
}
// Assemble from parts.
let assembly = construction.assembly.unwrap();
assemble(ctx, base, assembly, min_overlap, target, axis)
}
/// Assemble a glyph from parts.
fn assemble(
ctx: &mut MathContext,
base: GlyphFragment,
assembly: GlyphAssembly,
min_overlap: Abs,
target: Abs,
axis: Axis,
) -> VariantFragment {
// Determine the number of times the extenders need to be repeated as well
// as a ratio specifying how much to spread the parts apart
// (0 = maximal overlap, 1 = minimal overlap).
let mut full;
let mut ratio;
let mut repeat = 0;
loop {
full = Abs::zero();
ratio = 0.0;
let mut parts = parts(assembly, repeat).peekable();
let mut growable = Abs::zero();
while let Some(part) = parts.next() {
let mut advance = part.full_advance.scaled(ctx, base.font_size);
if let Some(next) = parts.peek() {
let max_overlap = part
.end_connector_length
.min(next.start_connector_length)
.scaled(ctx, base.font_size);
if max_overlap < min_overlap {
// This condition happening is indicative of a bug in the
// font.
ctx.engine.sink.warn(warning!(
base.span,
"glyph has assembly parts with overlap less than minConnectorOverlap";
hint: "its rendering may appear broken - this is probably a font bug";
hint: "please file an issue at https://github.com/typst/typst/issues"
));
}
advance -= max_overlap;
growable += max_overlap - min_overlap;
}
full += advance;
}
if full < target {
let delta = target - full;
ratio = (delta / growable).min(1.0);
full += ratio * growable;
}
if target <= full || repeat >= MAX_REPEATS {
break;
}
repeat += 1;
}
let mut selected = vec![];
let mut parts = parts(assembly, repeat).peekable();
while let Some(part) = parts.next() {
let mut advance = part.full_advance.scaled(ctx, base.font_size);
if let Some(next) = parts.peek() {
let max_overlap = part
.end_connector_length
.min(next.start_connector_length)
.scaled(ctx, base.font_size);
advance -= max_overlap;
advance += ratio * (max_overlap - min_overlap);
}
let mut fragment = base.clone();
fragment.set_id(ctx, part.glyph_id);
selected.push((fragment, advance));
}
let size;
let baseline;
match axis {
Axis::X => {
let height = base.ascent + base.descent;
size = Size::new(full, height);
baseline = base.ascent;
}
Axis::Y => {
let axis = ctx.constants.axis_height().scaled(ctx, base.font_size);
let width = selected.iter().map(|(f, _)| f.width).max().unwrap_or_default();
size = Size::new(width, full);
baseline = full / 2.0 + axis;
}
}
let mut frame = Frame::soft(size);
let mut offset = Abs::zero();
frame.set_baseline(baseline);
frame.modify(&base.modifiers);
for (fragment, advance) in selected {
let pos = match axis {
Axis::X => Point::new(offset, frame.baseline() - fragment.ascent),
Axis::Y => Point::with_y(full - offset - fragment.height()),
};
frame.push_frame(pos, fragment.into_frame());
offset += advance;
}
let accent_attach = match axis {
Axis::X => frame.width() / 2.0,
Axis::Y => base.accent_attach,
}; };
VariantFragment { let relative_to_size = relative_to.unwrap_or_else(|| size.get(stretch_axis));
c: base.c,
frame, glyph.stretch(ctx, stretch.relative_to(relative_to_size), short_fall, stretch_axis);
font_size: base.font_size,
italics_correction: Abs::zero(), if stretch_axis == Axis::Y {
accent_attach, // TODO: this should use delimiter_alignment
class: base.class, glyph.center_on_axis();
math_size: base.math_size,
span: base.span,
limits: base.limits,
mid_stretched: None,
extended_shape: true,
} }
} }
/// Return an iterator over the assembly's parts with extenders repeated the
/// specified number of times.
fn parts(assembly: GlyphAssembly, repeat: usize) -> impl Iterator<Item = GlyphPart> + '_ {
assembly.parts.into_iter().flat_map(move |part| {
let count = if part.part_flags.extender() { repeat } else { 1 };
std::iter::repeat_n(part, count)
})
}

View File

@ -12,7 +12,10 @@ use typst_syntax::{is_newline, Span};
use unicode_math_class::MathClass; use unicode_math_class::MathClass;
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
use super::{FrameFragment, GlyphFragment, MathContext, MathFragment, MathRun}; use super::{
find_math_font, has_dtls_feat, style_dtls, FrameFragment, GlyphFragment, MathContext,
MathFragment, MathRun,
};
/// Lays out a [`TextElem`]. /// Lays out a [`TextElem`].
pub fn layout_text( pub fn layout_text(
@ -48,7 +51,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, styles, span)?;
let axis = constant!(font, styles, axis_height);
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))
} }
@ -65,19 +69,11 @@ fn layout_inline_text(
// Small optimization for numbers. Note that this lays out slightly // Small optimization for numbers. Note that this lays out slightly
// differently to normal text and is worth re-evaluating in the future. // differently to normal text and is worth re-evaluating in the future.
let mut fragments = vec![]; let mut fragments = vec![];
let is_single = text.chars().count() == 1;
for unstyled_c in text.chars() { for unstyled_c in text.chars() {
let c = styled_char(styles, unstyled_c, false); let c = styled_char(styles, unstyled_c, false);
let mut glyph = GlyphFragment::new(ctx, styles, c, span); let glyph =
if is_single { GlyphFragment::new(ctx, styles, c.encode_utf8(&mut [0; 4]), span)?
// Duplicate what `layout_glyph` does exactly even if it's .unwrap();
// probably incorrect here.
match EquationElem::size_in(styles) {
MathSize::Script => glyph.make_script_size(ctx),
MathSize::ScriptScript => glyph.make_script_script_size(ctx),
_ => {}
}
}
fragments.push(glyph.into()); fragments.push(glyph.into());
} }
let frame = MathRun::new(fragments).into_frame(styles); let frame = MathRun::new(fragments).into_frame(styles);
@ -126,55 +122,32 @@ pub fn layout_symbol(
) -> SourceResult<()> { ) -> SourceResult<()> {
// 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 (unstyled_c, dtls) = match try_dotless(elem.text) { // let dtls = style_dtls();
Some(c) if ctx.dtls_table.is_some() => (c, true), // let (unstyled_c, symbol_styles) = match try_dotless(elem.text) {
_ => (elem.text, false), // Some(c) if has_dtls_feat(ctx.font) => (c, styles.chain(&dtls)),
// _ => (elem.text, styles),
// };
let c = styled_char(styles, elem.text, true);
let Some(mut glyph) =
GlyphFragment::new(ctx, styles, c.encode_utf8(&mut [0; 4]), elem.span())?
else {
return Ok(());
}; };
let c = styled_char(styles, unstyled_c, true);
let fragment = match GlyphFragment::try_new(ctx, styles, c, elem.span()) {
Some(glyph) => layout_glyph(glyph, dtls, ctx, styles),
None => {
// Not in the math font, fallback to normal inline text layout.
layout_inline_text(c.encode_utf8(&mut [0; 4]), elem.span(), ctx, styles)?
.into()
}
};
ctx.push(fragment);
Ok(())
}
/// Layout a [`GlyphFragment`].
fn layout_glyph(
mut glyph: GlyphFragment,
dtls: bool,
ctx: &mut MathContext,
styles: StyleChain,
) -> MathFragment {
if dtls {
glyph.make_dotless_form(ctx);
}
let math_size = EquationElem::size_in(styles); let math_size = EquationElem::size_in(styles);
match math_size {
MathSize::Script => glyph.make_script_size(ctx),
MathSize::ScriptScript => glyph.make_script_script_size(ctx),
_ => {}
}
if glyph.class == MathClass::Large { if glyph.class == MathClass::Large {
let mut variant = if math_size == MathSize::Display { if math_size == MathSize::Display {
let height = scaled!(ctx, styles, display_operator_min_height) let height =
.max(SQRT_2 * glyph.height()); word!(glyph.text, display_operator_min_height).max(SQRT_2 * glyph.size.y);
glyph.stretch_vertical(ctx, height, Abs::zero()) glyph.stretch_vertical(ctx, height, Abs::zero());
} else {
glyph.into_variant()
}; };
// TeXbook p 155. Large operators are always vertically centered on the // TeXbook p 155. Large operators are always vertically centered on the
// axis. // axis.
variant.center_on_axis(ctx); glyph.center_on_axis();
variant.into()
} else {
glyph.into()
} }
ctx.push(glyph);
Ok(())
} }
/// Style the character by selecting the unicode codepoint for italic, bold, /// Style the character by selecting the unicode codepoint for italic, bold,

View File

@ -10,8 +10,8 @@ use typst_library::visualize::{FixedStroke, Geometry};
use typst_syntax::Span; use typst_syntax::Span;
use super::{ use super::{
stack, style_cramped, style_for_subscript, style_for_superscript, FrameFragment, find_math_font, stack, style_cramped, style_for_subscript, style_for_superscript,
GlyphFragment, LeftRightAlternator, MathContext, MathRun, FrameFragment, LeftRightAlternator, MathContext, MathRun,
}; };
const BRACE_GAP: Em = Em::new(0.25); const BRACE_GAP: Em = Em::new(0.25);
@ -206,11 +206,12 @@ fn layout_underoverline(
position: Position, position: Position,
) -> SourceResult<()> { ) -> SourceResult<()> {
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);
let font = find_math_font(ctx.engine, styles, span)?;
match position { match position {
Position::Under => { Position::Under => {
let sep = scaled!(ctx, styles, underbar_extra_descender); let sep = constant!(font, styles, underbar_extra_descender);
bar_height = scaled!(ctx, styles, underbar_rule_thickness); bar_height = constant!(font, styles, underbar_rule_thickness);
let gap = scaled!(ctx, styles, underbar_vertical_gap); let gap = constant!(font, styles, underbar_vertical_gap);
extra_height = sep + bar_height + gap; extra_height = sep + bar_height + gap;
content = ctx.layout_into_fragment(body, styles)?; content = ctx.layout_into_fragment(body, styles)?;
@ -221,9 +222,9 @@ fn layout_underoverline(
line_adjust = -content.italics_correction(); line_adjust = -content.italics_correction();
} }
Position::Over => { Position::Over => {
let sep = scaled!(ctx, styles, overbar_extra_ascender); let sep = constant!(font, styles, overbar_extra_ascender);
bar_height = scaled!(ctx, styles, overbar_rule_thickness); bar_height = constant!(font, styles, overbar_rule_thickness);
let gap = scaled!(ctx, styles, overbar_vertical_gap); let gap = constant!(font, styles, overbar_vertical_gap);
extra_height = sep + bar_height + gap; extra_height = sep + bar_height + gap;
let cramped = style_cramped(); let cramped = style_cramped();
@ -285,14 +286,14 @@ 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 glyph = GlyphFragment::new(ctx, styles, c, span); let mut glyph = ctx.layout_into_glyph(c, span, styles)?;
let stretched = glyph.stretch_horizontal(ctx, body.width(), Abs::zero()); glyph.stretch_horizontal(ctx, body.width(), Abs::zero());
let mut rows = vec![]; let mut rows = vec![];
let baseline = match position { let baseline = match position {
Position::Under => { Position::Under => {
rows.push(MathRun::new(vec![body])); rows.push(MathRun::new(vec![body]));
rows.push(stretched.into()); rows.push(glyph.into());
if let Some(annotation) = annotation { if let Some(annotation) = annotation {
let under_style = style_for_subscript(styles); let under_style = style_for_subscript(styles);
let annotation_styles = styles.chain(&under_style); let annotation_styles = styles.chain(&under_style);
@ -306,7 +307,7 @@ fn layout_underoverspreader(
let annotation_styles = styles.chain(&over_style); let annotation_styles = styles.chain(&over_style);
rows.extend(ctx.layout_into_run(annotation, annotation_styles)?.rows()); rows.extend(ctx.layout_into_run(annotation, annotation_styles)?.rows());
} }
rows.push(stretched.into()); rows.push(glyph.into());
rows.push(MathRun::new(vec![body])); rows.push(MathRun::new(vec![body]));
rows.len() - 1 rows.len() - 1
} }

View File

@ -80,14 +80,11 @@ pub fn sans(
/// ```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 = "Calligraphic", keywords = ["mathcal", "mathscr"])] #[func(title = "Calligraphic", keywords = ["mathcal", "mathscr"])]
pub fn cal( pub fn cal(
/// The content to style. /// The content to style.

View File

@ -113,6 +113,11 @@ impl Font {
.map(|units| self.to_em(units)) .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.advance(id.0))
}
/// Lookup a name by id. /// Lookup a name by id.
pub fn find_name(&self, id: u16) -> Option<String> { pub fn find_name(&self, id: u16) -> Option<String> {
find_name(&self.0.ttf, id) find_name(&self.0.ttf, id)

View File

@ -35,6 +35,11 @@ impl TextItem {
pub fn width(&self) -> Abs { pub fn width(&self) -> Abs {
self.glyphs.iter().map(|g| g.x_advance).sum::<Em>().at(self.size) self.glyphs.iter().map(|g| g.x_advance).sum::<Em>().at(self.size)
} }
/// The height of the text run.
pub fn height(&self) -> Abs {
self.glyphs.iter().map(|g| g.y_advance).sum::<Em>().at(self.size)
}
} }
impl Debug for TextItem { impl Debug for TextItem {
@ -54,6 +59,10 @@ pub struct Glyph {
pub x_advance: Em, pub x_advance: Em,
/// The horizontal offset of the glyph. /// The horizontal offset of the glyph.
pub x_offset: Em, pub x_offset: Em,
/// The advance height of the glyph.
pub y_advance: Em,
/// The vertical offset of the glyph.
pub y_offset: Em,
/// The range of the glyph in its item's text. The range's length may /// The range of the glyph in its item's text. The range's length may
/// be more than one due to multi-byte UTF-8 encoding or ligatures. /// be more than one due to multi-byte UTF-8 encoding or ligatures.
pub range: Range<u16>, pub range: Range<u16>,
@ -115,4 +124,13 @@ impl<'a> TextItemView<'a> {
.sum::<Em>() .sum::<Em>()
.at(self.item.size) .at(self.item.size)
} }
/// The total height of this text slice
pub fn height(&self) -> Abs {
self.glyphs()
.iter()
.map(|g| g.y_advance)
.sum::<Em>()
.at(self.item.size)
}
} }

View File

@ -30,6 +30,7 @@ pub use self::space::*;
use std::fmt::{self, Debug, Formatter}; use std::fmt::{self, Debug, Formatter};
use std::hash::Hash; use std::hash::Hash;
use std::str::FromStr;
use std::sync::LazyLock; use std::sync::LazyLock;
use ecow::{eco_format, EcoString}; use ecow::{eco_format, EcoString};
@ -937,6 +938,7 @@ pub fn families(styles: StyleChain) -> impl Iterator<Item = &FontFamily> + Clone
"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)
@ -1283,6 +1285,12 @@ pub fn features(styles: StyleChain) -> Vec<Feature> {
feat(b"frac", 1); feat(b"frac", 1);
} }
match EquationElem::size_in(styles) {
MathSize::Script => feat(b"ssty", 1),
MathSize::ScriptScript => feat(b"ssty", 2),
_ => {}
}
for (tag, value) in TextElem::features_in(styles).0 { for (tag, value) in TextElem::features_in(styles).0 {
tags.push(Feature::new(tag, value, ..)) tags.push(Feature::new(tag, value, ..))
} }
@ -1290,6 +1298,17 @@ pub fn features(styles: StyleChain) -> Vec<Feature> {
tags tags
} }
/// Process the language and region of a style chain into a
/// rustybuzz-compatible BCP 47 language.
pub fn language(styles: StyleChain) -> rustybuzz::Language {
let mut bcp: EcoString = TextElem::lang_in(styles).as_str().into();
if let Some(region) = TextElem::region_in(styles) {
bcp.push('-');
bcp.push_str(region.as_str());
}
rustybuzz::Language::from_str(&bcp).unwrap()
}
/// A toggle that turns on and off alternatingly if folded. /// A toggle that turns on and off alternatingly if folded.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct ItalicToggle(pub bool); pub struct ItalicToggle(pub bool);

View File

@ -120,13 +120,15 @@ impl krilla::text::Glyph for PdfGlyph {
} }
#[inline(always)] #[inline(always)]
fn y_offset(&self, _: f32) -> f32 { fn y_offset(&self, size: f32) -> f32 {
0.0 // Don't use `Em::at`, because it contains an expensive check whether the result is finite.
self.0.y_offset.get() as f32 * size
} }
#[inline(always)] #[inline(always)]
fn y_advance(&self, _: f32) -> f32 { fn y_advance(&self, size: f32) -> f32 {
0.0 // Don't use `Em::at`, because it contains an expensive check whether the result is finite.
self.0.y_advance.get() as f32 * size
} }
fn location(&self) -> Option<Location> { fn location(&self) -> Option<Location> {

View File

@ -14,18 +14,20 @@ use crate::{shape, AbsExt, State};
/// Render a text run into the canvas. /// Render a text run into the canvas.
pub fn render_text(canvas: &mut sk::Pixmap, state: State, text: &TextItem) { pub fn render_text(canvas: &mut sk::Pixmap, state: State, text: &TextItem) {
let mut x = Abs::zero(); let mut x = Abs::zero();
let mut y = Abs::zero();
for glyph in &text.glyphs { for glyph in &text.glyphs {
let id = GlyphId(glyph.id); let id = GlyphId(glyph.id);
let offset = x + glyph.x_offset.at(text.size); let x_offset = x + glyph.x_offset.at(text.size);
let y_offset = y + glyph.y_offset.at(text.size);
if should_outline(&text.font, glyph) { if should_outline(&text.font, glyph) {
let state = state.pre_translate(Point::with_x(offset)); let state = state.pre_translate(Point::new(x_offset, -y_offset));
render_outline_glyph(canvas, state, text, id); render_outline_glyph(canvas, state, text, id);
} else { } else {
let upem = text.font.units_per_em(); let upem = text.font.units_per_em();
let text_scale = text.size / upem; let text_scale = text.size / upem;
let state = state let state = state
.pre_translate(Point::new(offset, -text.size)) .pre_translate(Point::new(x_offset, -y_offset - text.size))
.pre_scale(Axes::new(text_scale, text_scale)); .pre_scale(Axes::new(text_scale, text_scale));
let (glyph_frame, _) = glyph_frame(&text.font, glyph.id); let (glyph_frame, _) = glyph_frame(&text.font, glyph.id);
@ -33,6 +35,7 @@ pub fn render_text(canvas: &mut sk::Pixmap, state: State, text: &TextItem) {
} }
x += glyph.x_advance.at(text.size); x += glyph.x_advance.at(text.size);
y += glyph.y_advance.at(text.size);
} }
} }

View File

@ -25,25 +25,32 @@ impl SVGRenderer {
self.xml.write_attribute("transform", "scale(1, -1)"); self.xml.write_attribute("transform", "scale(1, -1)");
let mut x: f64 = 0.0; let mut x: f64 = 0.0;
let mut y: f64 = 0.0;
for glyph in &text.glyphs { for glyph in &text.glyphs {
let id = GlyphId(glyph.id); let id = GlyphId(glyph.id);
let offset = x + glyph.x_offset.at(text.size).to_pt(); let x_offset = x + glyph.x_offset.at(text.size).to_pt();
let y_offset = y + glyph.y_offset.at(text.size).to_pt();
self.render_svg_glyph(text, id, offset, scale) self.render_svg_glyph(text, id, x_offset, y_offset, scale)
.or_else(|| self.render_bitmap_glyph(text, id, offset)) .or_else(|| self.render_bitmap_glyph(text, id, x_offset, y_offset))
.or_else(|| { .or_else(|| {
self.render_outline_glyph( self.render_outline_glyph(
state state
.pre_concat(Transform::scale(Ratio::one(), -Ratio::one())) .pre_concat(Transform::scale(Ratio::one(), -Ratio::one()))
.pre_translate(Point::new(Abs::pt(offset), Abs::zero())), .pre_translate(Point::new(
Abs::pt(x_offset),
Abs::pt(y_offset),
)),
text, text,
id, id,
offset, x_offset,
y_offset,
scale, scale,
) )
}); });
x += glyph.x_advance.at(text.size).to_pt(); x += glyph.x_advance.at(text.size).to_pt();
y += glyph.y_advance.at(text.size).to_pt();
} }
self.xml.end_element(); self.xml.end_element();
@ -55,6 +62,7 @@ impl SVGRenderer {
text: &TextItem, text: &TextItem,
id: GlyphId, id: GlyphId,
x_offset: f64, x_offset: f64,
y_offset: f64,
scale: f64, scale: f64,
) -> Option<()> { ) -> Option<()> {
let data_url = convert_svg_glyph_to_base64_url(&text.font, id)?; let data_url = convert_svg_glyph_to_base64_url(&text.font, id)?;
@ -73,6 +81,7 @@ impl SVGRenderer {
self.xml.start_element("use"); self.xml.start_element("use");
self.xml.write_attribute_fmt("xlink:href", format_args!("#{id}")); self.xml.write_attribute_fmt("xlink:href", format_args!("#{id}"));
self.xml.write_attribute("x", &x_offset); self.xml.write_attribute("x", &x_offset);
self.xml.write_attribute("y", &y_offset);
self.xml.end_element(); self.xml.end_element();
Some(()) Some(())
@ -84,6 +93,7 @@ impl SVGRenderer {
text: &TextItem, text: &TextItem,
id: GlyphId, id: GlyphId,
x_offset: f64, x_offset: f64,
y_offset: f64,
) -> Option<()> { ) -> Option<()> {
let (image, bitmap_x_offset, bitmap_y_offset) = let (image, bitmap_x_offset, bitmap_y_offset) =
convert_bitmap_glyph_to_image(&text.font, id)?; convert_bitmap_glyph_to_image(&text.font, id)?;
@ -109,6 +119,7 @@ impl SVGRenderer {
// it. // it.
let scale_factor = target_height / image.height(); let scale_factor = target_height / image.height();
self.xml.write_attribute("x", &(x_offset / scale_factor)); self.xml.write_attribute("x", &(x_offset / scale_factor));
self.xml.write_attribute("y", &(y_offset / scale_factor));
self.xml.write_attribute_fmt( self.xml.write_attribute_fmt(
"transform", "transform",
format_args!("scale({scale_factor} -{scale_factor})",), format_args!("scale({scale_factor} -{scale_factor})",),
@ -125,6 +136,7 @@ impl SVGRenderer {
text: &TextItem, text: &TextItem,
glyph_id: GlyphId, glyph_id: GlyphId,
x_offset: f64, x_offset: f64,
y_offset: f64,
scale: f64, scale: f64,
) -> Option<()> { ) -> Option<()> {
let scale = Ratio::new(scale); let scale = Ratio::new(scale);
@ -139,6 +151,7 @@ impl SVGRenderer {
self.xml.start_element("use"); self.xml.start_element("use");
self.xml.write_attribute_fmt("xlink:href", format_args!("#{id}")); self.xml.write_attribute_fmt("xlink:href", format_args!("#{id}"));
self.xml.write_attribute_fmt("x", format_args!("{x_offset}")); self.xml.write_attribute_fmt("x", format_args!("{x_offset}"));
self.xml.write_attribute_fmt("y", format_args!("{y_offset}"));
self.write_fill( self.write_fill(
&text.fill, &text.fill,
FillRule::default(), FillRule::default(),

Binary file not shown.

Before

Width:  |  Height:  |  Size: 868 B

After

Width:  |  Height:  |  Size: 884 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 465 B

After

Width:  |  Height:  |  Size: 461 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 644 B

After

Width:  |  Height:  |  Size: 716 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 726 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 670 B

After

Width:  |  Height:  |  Size: 687 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 402 B

After

Width:  |  Height:  |  Size: 492 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 902 B

After

Width:  |  Height:  |  Size: 897 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 648 B

After

Width:  |  Height:  |  Size: 640 B

View File

@ -51,6 +51,12 @@ $hat(i), hat(i, dotless: #false), accent(j, tilde), accent(j, tilde, dotless: #f
#set math.accent(dotless: false) #set math.accent(dotless: false)
$ hat(i) $ $ hat(i) $
--- math-accent-dotless-greedy ---
// Currently the dotless style propogates to everything in the accent's base,
// even though it shouldn't.
$ arrow(P_(c, i dot j) P_(1, i) j) \
arrow(P_(c, i dot j) P_(1, i) j, dotless: #false) $
--- math-accent-flattened --- --- math-accent-flattened ---
// Test flattened accent glyph variants. // Test flattened accent glyph variants.
#show math.equation: set text(font: "STIX Two Math") #show math.equation: set text(font: "STIX Two Math")

View File

@ -179,7 +179,7 @@ $ a0 + a1 + a0_2 \
#{ #{
let var = $x^1$ let var = $x^1$
for i in range(24) { for i in range(24) {
var = $var$ var = $var$
} }
$var_2$ $var_2$
} }

View File

@ -196,12 +196,7 @@ $ mat(#1, #(foo: "bar")) $
--- issue-2268-mat-augment-color --- --- issue-2268-mat-augment-color ---
// The augment line should be of the same color as the text // The augment line should be of the same color as the text
#set text( #set text(fill: yellow)
font: "New Computer Modern",
lang: "en",
fill: yellow,
)
$mat(augment: #1, M, v) arrow.r.squiggly mat(augment: #1, R, b)$ $mat(augment: #1, M, v) arrow.r.squiggly mat(augment: #1, R, b)$
--- math-mat-delims --- --- math-mat-delims ---
@ -263,7 +258,7 @@ $ mat(a; b; c) mat(a \ b \ c) $
--- math-mat-vec-cases-unity --- --- math-mat-vec-cases-unity ---
// Test that matrices, vectors, and cases are all laid out the same. // Test that matrices, vectors, and cases are all laid out the same.
$ mat(z_(n_p); a^2) $ mat(z_(n_p); a^2)
vec(z_(n_p), a^2) vec(z_(n_p), a^2)
cases(reverse: #true, delim: \(, z_(n_p), a^2) cases(reverse: #true, delim: \(, z_(n_p), a^2)
cases(delim: \(, z_(n_p), a^2) $ cases(delim: \(, z_(n_p), a^2) $

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.