summaryrefslogtreecommitdiff
path: root/crates/typst-layout/src/math/fragment.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/typst-layout/src/math/fragment.rs')
-rw-r--r--crates/typst-layout/src/math/fragment.rs704
1 files changed, 704 insertions, 0 deletions
diff --git a/crates/typst-layout/src/math/fragment.rs b/crates/typst-layout/src/math/fragment.rs
new file mode 100644
index 00000000..19a4494e
--- /dev/null
+++ b/crates/typst-layout/src/math/fragment.rs
@@ -0,0 +1,704 @@
+use std::fmt::{self, Debug, Formatter};
+
+use rustybuzz::Feature;
+use smallvec::SmallVec;
+use ttf_parser::gsub::{
+ AlternateSet, AlternateSubstitution, SingleSubstitution, SubstitutionSubtable,
+};
+use ttf_parser::opentype_layout::LayoutTable;
+use ttf_parser::{GlyphId, Rect};
+use typst_library::foundations::StyleChain;
+use typst_library::introspection::Tag;
+use typst_library::layout::{
+ Abs, Axis, Corner, Em, Frame, FrameItem, HideElem, Point, Size, VAlignment,
+};
+use typst_library::math::{EquationElem, MathSize};
+use typst_library::model::{Destination, LinkElem};
+use typst_library::text::{Font, Glyph, Lang, Region, TextElem, TextItem};
+use typst_library::visualize::Paint;
+use typst_syntax::Span;
+use unicode_math_class::MathClass;
+
+use super::{scaled_font_size, stretch_glyph, MathContext, Scaled};
+
+#[derive(Debug, Clone)]
+pub enum MathFragment {
+ Glyph(GlyphFragment),
+ Variant(VariantFragment),
+ Frame(FrameFragment),
+ Spacing(Abs, bool),
+ Space(Abs),
+ Linebreak,
+ Align,
+ Tag(Tag),
+}
+
+impl MathFragment {
+ pub fn size(&self) -> Size {
+ Size::new(self.width(), self.height())
+ }
+
+ pub fn width(&self) -> Abs {
+ match self {
+ Self::Glyph(glyph) => glyph.width,
+ Self::Variant(variant) => variant.frame.width(),
+ Self::Frame(fragment) => fragment.frame.width(),
+ Self::Spacing(amount, _) => *amount,
+ Self::Space(amount) => *amount,
+ _ => Abs::zero(),
+ }
+ }
+
+ pub fn height(&self) -> Abs {
+ match self {
+ Self::Glyph(glyph) => glyph.height(),
+ Self::Variant(variant) => variant.frame.height(),
+ Self::Frame(fragment) => fragment.frame.height(),
+ _ => Abs::zero(),
+ }
+ }
+
+ pub fn ascent(&self) -> Abs {
+ match self {
+ Self::Glyph(glyph) => glyph.ascent,
+ Self::Variant(variant) => variant.frame.ascent(),
+ Self::Frame(fragment) => fragment.frame.baseline(),
+ _ => Abs::zero(),
+ }
+ }
+
+ pub fn descent(&self) -> Abs {
+ match self {
+ Self::Glyph(glyph) => glyph.descent,
+ Self::Variant(variant) => variant.frame.descent(),
+ Self::Frame(fragment) => fragment.frame.descent(),
+ _ => Abs::zero(),
+ }
+ }
+
+ pub fn is_ignorant(&self) -> bool {
+ match self {
+ Self::Frame(fragment) => fragment.ignorant,
+ Self::Tag(_) => true,
+ _ => false,
+ }
+ }
+
+ pub fn class(&self) -> MathClass {
+ match self {
+ Self::Glyph(glyph) => glyph.class,
+ Self::Variant(variant) => variant.class,
+ Self::Frame(fragment) => fragment.class,
+ Self::Spacing(_, _) => MathClass::Space,
+ Self::Space(_) => MathClass::Space,
+ Self::Linebreak => MathClass::Space,
+ Self::Align => MathClass::Special,
+ Self::Tag(_) => MathClass::Special,
+ }
+ }
+
+ pub fn math_size(&self) -> Option<MathSize> {
+ match self {
+ Self::Glyph(glyph) => Some(glyph.math_size),
+ Self::Variant(variant) => Some(variant.math_size),
+ Self::Frame(fragment) => Some(fragment.math_size),
+ _ => None,
+ }
+ }
+
+ pub fn font_size(&self) -> Option<Abs> {
+ match self {
+ Self::Glyph(glyph) => Some(glyph.font_size),
+ Self::Variant(variant) => Some(variant.font_size),
+ Self::Frame(fragment) => Some(fragment.font_size),
+ _ => None,
+ }
+ }
+
+ pub fn set_class(&mut self, class: MathClass) {
+ match self {
+ Self::Glyph(glyph) => glyph.class = class,
+ Self::Variant(variant) => variant.class = class,
+ Self::Frame(fragment) => fragment.class = class,
+ _ => {}
+ }
+ }
+
+ pub fn set_limits(&mut self, limits: Limits) {
+ match self {
+ Self::Glyph(glyph) => glyph.limits = limits,
+ Self::Variant(variant) => variant.limits = limits,
+ Self::Frame(fragment) => fragment.limits = limits,
+ _ => {}
+ }
+ }
+
+ pub fn is_spaced(&self) -> bool {
+ if self.class() == MathClass::Fence {
+ return true;
+ }
+
+ matches!(
+ self,
+ MathFragment::Frame(FrameFragment {
+ spaced: true,
+ class: MathClass::Normal | MathClass::Alphabetic,
+ ..
+ })
+ )
+ }
+
+ pub fn is_text_like(&self) -> bool {
+ match self {
+ Self::Glyph(_) | Self::Variant(_) => self.class() != MathClass::Large,
+ MathFragment::Frame(frame) => frame.text_like,
+ _ => false,
+ }
+ }
+
+ pub fn italics_correction(&self) -> Abs {
+ match self {
+ Self::Glyph(glyph) => glyph.italics_correction,
+ Self::Variant(variant) => variant.italics_correction,
+ Self::Frame(fragment) => fragment.italics_correction,
+ _ => Abs::zero(),
+ }
+ }
+
+ pub fn accent_attach(&self) -> Abs {
+ match self {
+ Self::Glyph(glyph) => glyph.accent_attach,
+ Self::Variant(variant) => variant.accent_attach,
+ Self::Frame(fragment) => fragment.accent_attach,
+ _ => self.width() / 2.0,
+ }
+ }
+
+ pub fn into_frame(self) -> Frame {
+ match self {
+ Self::Glyph(glyph) => glyph.into_frame(),
+ Self::Variant(variant) => variant.frame,
+ Self::Frame(fragment) => fragment.frame,
+ Self::Tag(tag) => {
+ let mut frame = Frame::soft(Size::zero());
+ frame.push(Point::zero(), FrameItem::Tag(tag));
+ frame
+ }
+ _ => Frame::soft(self.size()),
+ }
+ }
+
+ pub fn limits(&self) -> Limits {
+ match self {
+ MathFragment::Glyph(glyph) => glyph.limits,
+ MathFragment::Variant(variant) => variant.limits,
+ MathFragment::Frame(fragment) => fragment.limits,
+ _ => Limits::Never,
+ }
+ }
+
+ /// If no kern table is provided for a corner, a kerning amount of zero is
+ /// assumed.
+ pub fn kern_at_height(&self, ctx: &MathContext, corner: Corner, height: Abs) -> Abs {
+ match self {
+ Self::Glyph(glyph) => {
+ kern_at_height(ctx, glyph.font_size, glyph.id, corner, height)
+ .unwrap_or_default()
+ }
+ _ => Abs::zero(),
+ }
+ }
+}
+
+impl From<GlyphFragment> for MathFragment {
+ fn from(glyph: GlyphFragment) -> Self {
+ Self::Glyph(glyph)
+ }
+}
+
+impl From<VariantFragment> for MathFragment {
+ fn from(variant: VariantFragment) -> Self {
+ Self::Variant(variant)
+ }
+}
+
+impl From<FrameFragment> for MathFragment {
+ fn from(fragment: FrameFragment) -> Self {
+ Self::Frame(fragment)
+ }
+}
+
+#[derive(Clone)]
+pub struct GlyphFragment {
+ pub id: GlyphId,
+ pub c: char,
+ pub font: Font,
+ pub lang: Lang,
+ pub region: Option<Region>,
+ pub fill: Paint,
+ pub shift: Abs,
+ pub width: Abs,
+ pub ascent: Abs,
+ pub descent: Abs,
+ pub italics_correction: Abs,
+ pub accent_attach: Abs,
+ pub font_size: Abs,
+ pub class: MathClass,
+ pub math_size: MathSize,
+ pub span: Span,
+ pub dests: SmallVec<[Destination; 1]>,
+ pub hidden: bool,
+ pub limits: Limits,
+}
+
+impl GlyphFragment {
+ pub fn new(ctx: &MathContext, styles: StyleChain, c: char, span: Span) -> Self {
+ let id = ctx.ttf.glyph_index(c).unwrap_or_default();
+ let id = Self::adjust_glyph_index(ctx, id);
+ Self::with_id(ctx, styles, c, id, span)
+ }
+
+ pub fn try_new(
+ ctx: &MathContext,
+ styles: StyleChain,
+ c: char,
+ span: Span,
+ ) -> Option<Self> {
+ let id = ctx.ttf.glyph_index(c)?;
+ let id = Self::adjust_glyph_index(ctx, id);
+ Some(Self::with_id(ctx, styles, c, id, span))
+ }
+
+ pub fn with_id(
+ ctx: &MathContext,
+ styles: StyleChain,
+ c: char,
+ id: GlyphId,
+ span: Span,
+ ) -> Self {
+ let class = EquationElem::class_in(styles)
+ .or_else(|| match c {
+ ':' => Some(MathClass::Relation),
+ '.' | '/' | '⋯' | '⋱' | '⋰' | '⋮' => Some(MathClass::Normal),
+ _ => unicode_math_class::class(c),
+ })
+ .unwrap_or(MathClass::Normal);
+
+ let mut fragment = Self {
+ id,
+ c,
+ font: ctx.font.clone(),
+ lang: TextElem::lang_in(styles),
+ region: TextElem::region_in(styles),
+ fill: TextElem::fill_in(styles).as_decoration(),
+ shift: TextElem::baseline_in(styles),
+ font_size: scaled_font_size(ctx, styles),
+ math_size: EquationElem::size_in(styles),
+ width: Abs::zero(),
+ ascent: Abs::zero(),
+ descent: Abs::zero(),
+ limits: Limits::for_char(c),
+ italics_correction: Abs::zero(),
+ accent_attach: Abs::zero(),
+ class,
+ span,
+ dests: LinkElem::dests_in(styles),
+ hidden: HideElem::hidden_in(styles),
+ };
+ fragment.set_id(ctx, id);
+ fragment
+ }
+
+ /// Apply GSUB substitutions.
+ fn adjust_glyph_index(ctx: &MathContext, id: GlyphId) -> GlyphId {
+ if let Some(glyphwise_tables) = &ctx.glyphwise_tables {
+ glyphwise_tables.iter().fold(id, |id, table| table.apply(id))
+ } else {
+ id
+ }
+ }
+
+ /// Sets element id and boxes in appropriate way without changing other
+ /// styles. This is used to replace the glyph with a stretch variant.
+ pub fn set_id(&mut self, ctx: &MathContext, id: GlyphId) {
+ let advance = ctx.ttf.glyph_hor_advance(id).unwrap_or_default();
+ let italics = italics_correction(ctx, id, self.font_size).unwrap_or_default();
+ let bbox = ctx.ttf.glyph_bounding_box(id).unwrap_or(Rect {
+ x_min: 0,
+ y_min: 0,
+ x_max: 0,
+ y_max: 0,
+ });
+
+ let mut width = advance.scaled(ctx, self.font_size);
+ let accent_attach =
+ accent_attach(ctx, id, self.font_size).unwrap_or((width + italics) / 2.0);
+
+ if !is_extended_shape(ctx, id) {
+ width += italics;
+ }
+
+ self.id = id;
+ self.width = width;
+ self.ascent = bbox.y_max.scaled(ctx, self.font_size);
+ self.descent = -bbox.y_min.scaled(ctx, self.font_size);
+ self.italics_correction = italics;
+ self.accent_attach = accent_attach;
+ }
+
+ pub fn height(&self) -> Abs {
+ self.ascent + self.descent
+ }
+
+ pub fn into_variant(self) -> VariantFragment {
+ VariantFragment {
+ c: self.c,
+ font_size: self.font_size,
+ italics_correction: self.italics_correction,
+ accent_attach: self.accent_attach,
+ class: self.class,
+ math_size: self.math_size,
+ span: self.span,
+ limits: self.limits,
+ frame: self.into_frame(),
+ mid_stretched: None,
+ }
+ }
+
+ pub fn into_frame(self) -> Frame {
+ let item = TextItem {
+ font: self.font.clone(),
+ size: self.font_size,
+ fill: self.fill,
+ lang: self.lang,
+ region: self.region,
+ text: self.c.into(),
+ stroke: None,
+ glyphs: vec![Glyph {
+ id: self.id.0,
+ x_advance: Em::from_length(self.width, self.font_size),
+ x_offset: Em::zero(),
+ range: 0..self.c.len_utf8() as u16,
+ span: (self.span, 0),
+ }],
+ };
+ let size = Size::new(self.width, self.ascent + self.descent);
+ let mut frame = Frame::soft(size);
+ frame.set_baseline(self.ascent);
+ frame.push(Point::with_y(self.ascent + self.shift), FrameItem::Text(item));
+ frame.post_process_raw(self.dests, self.hidden);
+ frame
+ }
+
+ pub fn make_scriptsize(&mut self, ctx: &MathContext) {
+ let alt_id =
+ script_alternatives(ctx, self.id).and_then(|alts| alts.alternates.get(0));
+
+ if let Some(alt_id) = alt_id {
+ self.set_id(ctx, alt_id);
+ }
+ }
+
+ pub fn make_scriptscriptsize(&mut self, ctx: &MathContext) {
+ let alts = script_alternatives(ctx, self.id);
+ let alt_id = alts
+ .and_then(|alts| alts.alternates.get(1).or_else(|| alts.alternates.get(0)));
+
+ if let Some(alt_id) = alt_id {
+ self.set_id(ctx, alt_id);
+ }
+ }
+
+ /// Try to stretch a glyph to a desired height.
+ pub fn stretch_vertical(
+ self,
+ ctx: &MathContext,
+ height: Abs,
+ short_fall: Abs,
+ ) -> VariantFragment {
+ stretch_glyph(ctx, self, height, short_fall, Axis::Y)
+ }
+
+ /// Try to stretch a glyph to a desired width.
+ pub fn stretch_horizontal(
+ self,
+ ctx: &MathContext,
+ width: Abs,
+ short_fall: Abs,
+ ) -> VariantFragment {
+ stretch_glyph(ctx, self, width, short_fall, Axis::X)
+ }
+}
+
+impl Debug for GlyphFragment {
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ write!(f, "GlyphFragment({:?})", self.c)
+ }
+}
+
+#[derive(Clone)]
+pub struct VariantFragment {
+ pub c: char,
+ pub italics_correction: Abs,
+ pub accent_attach: Abs,
+ pub frame: Frame,
+ pub font_size: Abs,
+ pub class: MathClass,
+ pub math_size: MathSize,
+ pub span: Span,
+ pub limits: Limits,
+ pub mid_stretched: Option<bool>,
+}
+
+impl VariantFragment {
+ /// Vertically adjust the fragment's frame so that it is centered
+ /// on the axis.
+ pub fn center_on_axis(&mut self, ctx: &MathContext) {
+ self.align_on_axis(ctx, VAlignment::Horizon)
+ }
+
+ /// Vertically adjust the fragment's frame so that it is aligned
+ /// to the given alignment on the axis.
+ pub fn align_on_axis(&mut self, ctx: &MathContext, align: VAlignment) {
+ let h = self.frame.height();
+ let axis = ctx.constants.axis_height().scaled(ctx, self.font_size);
+ self.frame.set_baseline(align.inv().position(h + axis * 2.0));
+ }
+}
+
+impl Debug for VariantFragment {
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ write!(f, "VariantFragment({:?})", self.c)
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct FrameFragment {
+ pub frame: Frame,
+ pub font_size: Abs,
+ pub class: MathClass,
+ pub math_size: MathSize,
+ pub limits: Limits,
+ pub spaced: bool,
+ pub base_ascent: Abs,
+ pub italics_correction: Abs,
+ pub accent_attach: Abs,
+ pub text_like: bool,
+ pub ignorant: bool,
+}
+
+impl FrameFragment {
+ pub fn new(ctx: &MathContext, styles: StyleChain, frame: Frame) -> Self {
+ let base_ascent = frame.ascent();
+ let accent_attach = frame.width() / 2.0;
+ Self {
+ frame: frame.post_processed(styles),
+ font_size: scaled_font_size(ctx, styles),
+ class: EquationElem::class_in(styles).unwrap_or(MathClass::Normal),
+ math_size: EquationElem::size_in(styles),
+ limits: Limits::Never,
+ spaced: false,
+ base_ascent,
+ italics_correction: Abs::zero(),
+ accent_attach,
+ text_like: false,
+ ignorant: false,
+ }
+ }
+
+ pub fn with_class(self, class: MathClass) -> Self {
+ Self { class, ..self }
+ }
+
+ pub fn with_limits(self, limits: Limits) -> Self {
+ Self { limits, ..self }
+ }
+
+ pub fn with_spaced(self, spaced: bool) -> Self {
+ Self { spaced, ..self }
+ }
+
+ pub fn with_base_ascent(self, base_ascent: Abs) -> Self {
+ Self { base_ascent, ..self }
+ }
+
+ pub fn with_italics_correction(self, italics_correction: Abs) -> Self {
+ Self { italics_correction, ..self }
+ }
+
+ pub fn with_accent_attach(self, accent_attach: Abs) -> Self {
+ Self { accent_attach, ..self }
+ }
+
+ pub fn with_text_like(self, text_like: bool) -> Self {
+ Self { text_like, ..self }
+ }
+
+ pub fn with_ignorant(self, ignorant: bool) -> Self {
+ Self { ignorant, ..self }
+ }
+}
+
+/// Look up the italics correction for a glyph.
+fn italics_correction(ctx: &MathContext, id: GlyphId, font_size: Abs) -> Option<Abs> {
+ Some(
+ ctx.table
+ .glyph_info?
+ .italic_corrections?
+ .get(id)?
+ .scaled(ctx, font_size),
+ )
+}
+
+/// Loop up the top accent attachment position for a glyph.
+fn accent_attach(ctx: &MathContext, id: GlyphId, font_size: Abs) -> Option<Abs> {
+ Some(
+ ctx.table
+ .glyph_info?
+ .top_accent_attachments?
+ .get(id)?
+ .scaled(ctx, font_size),
+ )
+}
+
+/// Look up the script/scriptscript alternates for a glyph
+fn script_alternatives<'a>(
+ ctx: &MathContext<'a, '_, '_>,
+ id: GlyphId,
+) -> Option<AlternateSet<'a>> {
+ ctx.ssty_table.and_then(|ssty| {
+ ssty.coverage.get(id).and_then(|index| ssty.alternate_sets.get(index))
+ })
+}
+
+/// Look up whether a glyph is an extended shape.
+fn is_extended_shape(ctx: &MathContext, id: GlyphId) -> bool {
+ ctx.table
+ .glyph_info
+ .and_then(|info| info.extended_shapes)
+ .and_then(|info| info.get(id))
+ .is_some()
+}
+
+/// Look up a kerning value at a specific corner and height.
+fn kern_at_height(
+ ctx: &MathContext,
+ font_size: Abs,
+ id: GlyphId,
+ corner: Corner,
+ height: Abs,
+) -> Option<Abs> {
+ let kerns = ctx.table.glyph_info?.kern_infos?.get(id)?;
+ let kern = match corner {
+ Corner::TopLeft => kerns.top_left,
+ Corner::TopRight => kerns.top_right,
+ Corner::BottomRight => kerns.bottom_right,
+ Corner::BottomLeft => kerns.bottom_left,
+ }?;
+
+ let mut i = 0;
+ while i < kern.count() && height > kern.height(i)?.scaled(ctx, font_size) {
+ i += 1;
+ }
+
+ Some(kern.kern(i)?.scaled(ctx, font_size))
+}
+
+/// Describes in which situation a frame should use limits for attachments.
+#[derive(Debug, Copy, Clone)]
+pub enum Limits {
+ /// Always scripts.
+ Never,
+ /// Display limits only in `display` math.
+ Display,
+ /// Always limits.
+ Always,
+}
+
+impl Limits {
+ /// The default limit configuration if the given character is the base.
+ pub fn for_char(c: char) -> Self {
+ match unicode_math_class::class(c) {
+ Some(MathClass::Large) => {
+ if is_integral_char(c) {
+ Limits::Never
+ } else {
+ Limits::Display
+ }
+ }
+ Some(MathClass::Relation) => Limits::Always,
+ _ => Limits::Never,
+ }
+ }
+
+ /// The default limit configuration for a math class.
+ pub fn for_class(class: MathClass) -> Self {
+ match class {
+ MathClass::Large => Self::Display,
+ MathClass::Relation => Self::Always,
+ _ => Self::Never,
+ }
+ }
+
+ /// Whether limits should be displayed in this context.
+ pub fn active(&self, styles: StyleChain) -> bool {
+ match self {
+ Self::Always => true,
+ Self::Display => EquationElem::size_in(styles) == MathSize::Display,
+ Self::Never => false,
+ }
+ }
+}
+
+/// Determines if the character is one of a variety of integral signs.
+fn is_integral_char(c: char) -> bool {
+ ('∫'..='∳').contains(&c) || ('⨋'..='⨜').contains(&c)
+}
+
+/// An OpenType substitution table that is applicable to glyph-wise substitutions.
+pub enum GlyphwiseSubsts<'a> {
+ Single(SingleSubstitution<'a>),
+ Alternate(AlternateSubstitution<'a>, u32),
+}
+
+impl<'a> GlyphwiseSubsts<'a> {
+ pub fn new(gsub: LayoutTable<'a>, feature: Feature) -> Option<Self> {
+ let table = gsub
+ .features
+ .find(ttf_parser::Tag(feature.tag.0))
+ .and_then(|feature| feature.lookup_indices.get(0))
+ .and_then(|index| gsub.lookups.get(index))?;
+ let table = table.subtables.get::<SubstitutionSubtable>(0)?;
+ match table {
+ SubstitutionSubtable::Single(single_glyphs) => {
+ Some(Self::Single(single_glyphs))
+ }
+ SubstitutionSubtable::Alternate(alt_glyphs) => {
+ Some(Self::Alternate(alt_glyphs, feature.value))
+ }
+ _ => None,
+ }
+ }
+
+ pub fn try_apply(&self, glyph_id: GlyphId) -> Option<GlyphId> {
+ match self {
+ Self::Single(single) => match single {
+ SingleSubstitution::Format1 { coverage, delta } => coverage
+ .get(glyph_id)
+ .map(|_| GlyphId(glyph_id.0.wrapping_add(*delta as u16))),
+ SingleSubstitution::Format2 { coverage, substitutes } => {
+ coverage.get(glyph_id).and_then(|idx| substitutes.get(idx))
+ }
+ },
+ Self::Alternate(alternate, value) => alternate
+ .coverage
+ .get(glyph_id)
+ .and_then(|idx| alternate.alternate_sets.get(idx))
+ .and_then(|set| set.alternates.get(*value as u16)),
+ }
+ }
+
+ pub fn apply(&self, glyph_id: GlyphId) -> GlyphId {
+ self.try_apply(glyph_id).unwrap_or(glyph_id)
+ }
+}