diff --git a/library/src/math/accent.rs b/library/src/math/accent.rs index 8cae4268b..809939258 100644 --- a/library/src/math/accent.rs +++ b/library/src/math/accent.rs @@ -118,7 +118,7 @@ impl LayoutMath for AccentNode { frame.set_baseline(baseline); frame.push_frame(accent_pos, accent); frame.push_frame(base_pos, base.to_frame(ctx)); - ctx.push(frame); + ctx.push(FrameFragment::new(ctx, frame)); Ok(()) } diff --git a/library/src/math/align.rs b/library/src/math/align.rs index 82b461e9d..e8a9c14a5 100644 --- a/library/src/math/align.rs +++ b/library/src/math/align.rs @@ -29,8 +29,7 @@ pub(super) fn alignments(rows: &[MathRow]) -> Vec { let count = rows .iter() .map(|row| { - row.0 - .iter() + row.iter() .filter(|fragment| matches!(fragment, MathFragment::Align)) .count() }) @@ -42,7 +41,7 @@ pub(super) fn alignments(rows: &[MathRow]) -> Vec { for row in rows { let mut x = Abs::zero(); let mut i = 0; - for fragment in &row.0 { + for fragment in row.iter() { if matches!(fragment, MathFragment::Align) { if i < current { x = points[i]; diff --git a/library/src/math/attach.rs b/library/src/math/attach.rs index 110ea4b79..8a1644beb 100644 --- a/library/src/math/attach.rs +++ b/library/src/math/attach.rs @@ -231,7 +231,7 @@ fn scripts( frame.push_frame(sub_pos, sub); } - ctx.push(FrameFragment::new(frame).with_class(class)); + ctx.push(FrameFragment::new(ctx, frame).with_class(class)); Ok(()) } @@ -284,7 +284,7 @@ fn limits( frame.push_frame(bottom_pos, bottom); } - ctx.push(FrameFragment::new(frame).with_class(class)); + ctx.push(FrameFragment::new(ctx, frame).with_class(class)); Ok(()) } diff --git a/library/src/math/ctx.rs b/library/src/math/ctx.rs index d682746e2..4f390817a 100644 --- a/library/src/math/ctx.rs +++ b/library/src/math/ctx.rs @@ -31,7 +31,7 @@ pub struct MathContext<'a, 'b, 'v> { pub table: ttf_parser::math::Table<'a>, pub constants: ttf_parser::math::Constants<'a>, pub space_width: Em, - pub row: MathRow, + pub fragments: Vec, pub map: StyleMap, pub style: MathStyle, pub size: Abs, @@ -69,7 +69,7 @@ impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> { table, constants, space_width, - row: MathRow::new(), + fragments: vec![], map: StyleMap::new(), style: MathStyle { variant: MathVariant::Serif, @@ -88,45 +88,45 @@ impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> { } pub fn push(&mut self, fragment: impl Into) { - self.row.push(self.size, self.space_width, self.style, fragment); + self.fragments.push(fragment.into()); } - pub fn extend(&mut self, row: MathRow) { - let mut iter = row.0.into_iter(); - if let Some(first) = iter.next() { - self.push(first); - } - self.row.0.extend(iter); - } - - pub fn layout_non_math(&mut self, content: &Content) -> SourceResult { - Ok(content - .layout(&mut self.vt, self.outer.chain(&self.map), self.regions)? - .into_frame()) + pub fn extend(&mut self, fragments: Vec) { + self.fragments.extend(fragments); } pub fn layout_fragment( &mut self, node: &dyn LayoutMath, ) -> SourceResult { - let row = self.layout_row(node)?; - Ok(if row.0.len() == 1 { - row.0.into_iter().next().unwrap() - } else { - row.to_frame(self).into() - }) + let row = self.layout_fragments(node)?; + Ok(MathRow::new(row).to_fragment(self)) + } + + pub fn layout_fragments( + &mut self, + node: &dyn LayoutMath, + ) -> SourceResult> { + let prev = std::mem::take(&mut self.fragments); + node.layout_math(self)?; + Ok(std::mem::replace(&mut self.fragments, prev)) } pub fn layout_row(&mut self, node: &dyn LayoutMath) -> SourceResult { - let prev = std::mem::take(&mut self.row); - node.layout_math(self)?; - Ok(std::mem::replace(&mut self.row, prev)) + let fragments = self.layout_fragments(node)?; + Ok(MathRow::new(fragments)) } pub fn layout_frame(&mut self, node: &dyn LayoutMath) -> SourceResult { Ok(self.layout_fragment(node)?.to_frame(self)) } + pub fn layout_content(&mut self, content: &Content) -> SourceResult { + Ok(content + .layout(&mut self.vt, self.outer.chain(&self.map), self.regions)? + .into_frame()) + } + pub fn layout_text(&mut self, text: &str) -> SourceResult<()> { let mut chars = text.chars(); if let Some(glyph) = chars @@ -146,13 +146,13 @@ impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> { } } else if text.chars().all(|c| c.is_ascii_digit()) { // Numbers aren't that difficult. - let mut vec = vec![]; + let mut fragments = vec![]; for c in text.chars() { let c = self.style.styled_char(c); - vec.push(GlyphFragment::new(self, c).into()); + fragments.push(GlyphFragment::new(self, c).into()); } - let frame = MathRow(vec).to_frame(self); - self.push(frame); + let frame = MathRow::new(fragments).to_frame(self); + self.push(FrameFragment::new(self, frame)); } else { // Anything else is handled by Typst's standard text layout. let spaced = text.graphemes(true).count() > 1; @@ -161,9 +161,9 @@ impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> { style = style.with_italic(false); } let text: EcoString = text.chars().map(|c| style.styled_char(c)).collect(); - let frame = self.layout_non_math(&TextNode::packed(text))?; + let frame = self.layout_content(&TextNode::packed(text))?; self.push( - FrameFragment::new(frame) + FrameFragment::new(self, frame) .with_class(MathClass::Alphabetic) .with_spaced(spaced), ); diff --git a/library/src/math/delimited.rs b/library/src/math/delimited.rs index cdd5718c7..b69e4c39d 100644 --- a/library/src/math/delimited.rs +++ b/library/src/math/delimited.rs @@ -60,10 +60,9 @@ impl LayoutMath for LrNode { } } - let mut row = ctx.layout_row(body)?; + let mut fragments = ctx.layout_fragments(body)?; let axis = scaled!(ctx, axis_height); - let max_extent = row - .0 + let max_extent = fragments .iter() .map(|fragment| (fragment.ascent() - axis).max(fragment.descent() + axis)) .max() @@ -75,7 +74,7 @@ impl LayoutMath for LrNode { .resolve(ctx.styles()) .relative_to(2.0 * max_extent); - match row.0.as_mut_slice() { + match fragments.as_mut_slice() { [one] => scale(ctx, one, height, None), [first, .., last] => { scale(ctx, first, height, Some(MathClass::Opening)); @@ -84,7 +83,7 @@ impl LayoutMath for LrNode { _ => {} } - ctx.extend(row); + ctx.extend(fragments); Ok(()) } diff --git a/library/src/math/frac.rs b/library/src/math/frac.rs index 9f7fb9d0d..283686c8a 100644 --- a/library/src/math/frac.rs +++ b/library/src/math/frac.rs @@ -153,7 +153,7 @@ fn layout( if binom { ctx.push(GlyphFragment::new(ctx, '(').stretch_vertical(ctx, height, short_fall)); - ctx.push(frame); + ctx.push(FrameFragment::new(ctx, frame)); ctx.push(GlyphFragment::new(ctx, ')').stretch_vertical(ctx, height, short_fall)); } else { frame.push( @@ -165,7 +165,7 @@ fn layout( }), ), ); - ctx.push(frame); + ctx.push(FrameFragment::new(ctx, frame)); } Ok(()) diff --git a/library/src/math/fragment.rs b/library/src/math/fragment.rs index bef3f5780..f1c374a14 100644 --- a/library/src/math/fragment.rs +++ b/library/src/math/fragment.rs @@ -6,7 +6,7 @@ pub enum MathFragment { Variant(VariantFragment), Frame(FrameFragment), Spacing(Abs), - Space, + Space(Abs), Linebreak, Align, } @@ -22,6 +22,7 @@ impl MathFragment { Self::Variant(variant) => variant.frame.width(), Self::Frame(fragment) => fragment.frame.width(), Self::Spacing(amount) => *amount, + Self::Space(amount) => *amount, _ => Abs::zero(), } } @@ -62,6 +63,24 @@ impl MathFragment { } } + pub fn style(&self) -> Option { + match self { + Self::Glyph(glyph) => Some(glyph.style), + Self::Variant(variant) => Some(variant.style), + Self::Frame(fragment) => Some(fragment.style), + _ => None, + } + } + + pub fn font_size(&self) -> Option { + 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 = Some(class), @@ -71,8 +90,11 @@ impl MathFragment { } } - pub fn participating(&self) -> bool { - !matches!(self, Self::Space | Self::Spacing(_) | Self::Align) + pub fn is_spaced(&self) -> bool { + match self { + MathFragment::Frame(frame) => frame.spaced, + _ => self.class() == Some(MathClass::Fence), + } } pub fn italics_correction(&self) -> Abs { @@ -111,23 +133,18 @@ impl From for MathFragment { } } -impl From for MathFragment { - fn from(frame: Frame) -> Self { - Self::Frame(FrameFragment::new(frame)) - } -} - -#[derive(Debug, Clone, Copy)] +#[derive(Clone, Copy)] pub struct GlyphFragment { pub id: GlyphId, pub c: char, pub lang: Lang, pub fill: Paint, - pub font_size: Abs, pub width: Abs, pub ascent: Abs, pub descent: Abs, pub italics_correction: Abs, + pub style: MathStyle, + pub font_size: Abs, pub class: Option, } @@ -163,6 +180,7 @@ impl GlyphFragment { c, lang: ctx.styles().get(TextNode::LANG), fill: ctx.styles().get(TextNode::FILL), + style: ctx.style, font_size: ctx.size, width, ascent: bbox.y_max.scaled(ctx), @@ -184,6 +202,8 @@ impl GlyphFragment { c: self.c, id: Some(self.id), frame: self.to_frame(ctx), + style: self.style, + font_size: self.font_size, italics_correction: self.italics_correction, class: self.class, } @@ -210,30 +230,48 @@ impl GlyphFragment { } } -#[derive(Debug, Clone)] +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 id: Option, - pub frame: Frame, pub italics_correction: Abs, + pub frame: Frame, + pub style: MathStyle, + pub font_size: Abs, pub class: Option, } +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 class: MathClass, pub limits: bool, pub spaced: bool, + pub style: MathStyle, + pub font_size: Abs, + pub class: MathClass, } impl FrameFragment { - pub fn new(frame: Frame) -> Self { + pub fn new(ctx: &MathContext, frame: Frame) -> Self { Self { frame, - class: MathClass::Normal, limits: false, spaced: false, + font_size: ctx.size, + style: ctx.style, + class: MathClass::Normal, } } diff --git a/library/src/math/matrix.rs b/library/src/math/matrix.rs index e5ae6a1d2..c47f2d5ee 100644 --- a/library/src/math/matrix.rs +++ b/library/src/math/matrix.rs @@ -307,7 +307,7 @@ fn layout_delimiters( ctx.push(GlyphFragment::new(ctx, left).stretch_vertical(ctx, target, short_fall)); } - ctx.push(frame); + ctx.push(FrameFragment::new(ctx, frame)); if let Some(right) = right { ctx.push( diff --git a/library/src/math/mod.rs b/library/src/math/mod.rs index 4e60faca7..656fc2e01 100644 --- a/library/src/math/mod.rs +++ b/library/src/math/mod.rs @@ -261,7 +261,7 @@ impl LayoutMath for Content { } if self.is::() { - ctx.push(MathFragment::Space); + ctx.push(MathFragment::Space(ctx.space_width.scaled(ctx))); return Ok(()); } @@ -288,8 +288,8 @@ impl LayoutMath for Content { return node.layout_math(ctx); } - let frame = ctx.layout_non_math(self)?; - ctx.push(FrameFragment::new(frame).with_spaced(true)); + let frame = ctx.layout_content(self)?; + ctx.push(FrameFragment::new(ctx, frame).with_spaced(true)); Ok(()) } diff --git a/library/src/math/op.rs b/library/src/math/op.rs index 6e52cea5f..8cc007ead 100644 --- a/library/src/math/op.rs +++ b/library/src/math/op.rs @@ -38,9 +38,9 @@ impl OpNode { impl LayoutMath for OpNode { fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - let frame = ctx.layout_non_math(&TextNode(self.text.clone()).pack())?; + let frame = ctx.layout_content(&TextNode(self.text.clone()).pack())?; ctx.push( - FrameFragment::new(frame) + FrameFragment::new(ctx, frame) .with_class(MathClass::Large) .with_limits(self.limits), ); diff --git a/library/src/math/root.rs b/library/src/math/root.rs index 2d27bb114..e1c8d9878 100644 --- a/library/src/math/root.rs +++ b/library/src/math/root.rs @@ -158,7 +158,7 @@ fn layout( ); frame.push_frame(radicand_pos, radicand); - ctx.push(frame); + ctx.push(FrameFragment::new(ctx, frame)); Ok(()) } diff --git a/library/src/math/row.rs b/library/src/math/row.rs index 41136a9e3..ecd37f462 100644 --- a/library/src/math/row.rs +++ b/library/src/math/row.rs @@ -5,55 +5,55 @@ use super::*; pub const TIGHT_LEADING: Em = Em::new(0.25); #[derive(Debug, Default, Clone)] -pub struct MathRow(pub Vec); +pub struct MathRow(Vec); impl MathRow { - pub fn new() -> Self { - Self(vec![]) - } + pub fn new(fragments: Vec) -> Self { + let mut iter = fragments.into_iter().peekable(); + let mut last: Option = None; + let mut space: Option = None; + let mut resolved: Vec = vec![]; - pub fn width(&self) -> Abs { - self.0.iter().map(|fragment| fragment.width()).sum() - } - - pub fn height(&self) -> Abs { - self.ascent() + self.descent() - } - - pub fn ascent(&self) -> Abs { - self.0.iter().map(MathFragment::ascent).max().unwrap_or_default() - } - - pub fn descent(&self) -> Abs { - self.0.iter().map(MathFragment::descent).max().unwrap_or_default() - } - - pub fn push( - &mut self, - font_size: Abs, - space_width: Em, - style: MathStyle, - fragment: impl Into, - ) { - let mut fragment = fragment.into(); - if !fragment.participating() { - self.0.push(fragment); - return; - } - - let mut space = false; - for (i, prev) in self.0.iter().enumerate().rev() { - if !prev.participating() { - space |= matches!(prev, MathFragment::Space); - if matches!(prev, MathFragment::Spacing(_)) { - break; + while let Some(mut fragment) = iter.next() { + match fragment { + // Keep space only if supported by spaced fragments. + MathFragment::Space(_) => { + if last.is_some() { + space = Some(fragment); + } + continue; } - continue; + + // Explicit spacing disables automatic spacing. + MathFragment::Spacing(_) => { + last = None; + space = None; + resolved.push(fragment); + continue; + } + + // Alignment points are resolved later. + MathFragment::Align => { + resolved.push(fragment); + continue; + } + + // New line, new things. + MathFragment::Linebreak => { + resolved.push(fragment); + space = None; + last = None; + continue; + } + + _ => {} } - if fragment.class() == Some(MathClass::Vary) { - if matches!( - prev.class(), + // Convert variable operators into binary operators if something + // precedes them. + if fragment.class() == Some(MathClass::Vary) + && matches!( + last.and_then(|i| resolved[i].class()), Some( MathClass::Normal | MathClass::Alphabetic @@ -62,22 +62,43 @@ impl MathRow { | MathClass::Fence | MathClass::Relation ) - ) { - fragment.set_class(MathClass::Binary); + ) + { + fragment.set_class(MathClass::Binary); + } + + // Insert spacing between the last and this item. + if let Some(i) = last { + if let Some(s) = spacing(&resolved[i], space.take(), &fragment) { + resolved.insert(i + 1, s); } } - let mut amount = Abs::zero(); - amount += spacing(prev, &fragment, style, space, space_width).at(font_size); - - if !amount.is_zero() { - self.0.insert(i + 1, MathFragment::Spacing(amount)); - } - - break; + last = Some(resolved.len()); + resolved.push(fragment); } - self.0.push(fragment); + Self(resolved) + } + + pub fn iter(&self) -> std::slice::Iter<'_, MathFragment> { + self.0.iter() + } + + pub fn width(&self) -> Abs { + self.iter().map(MathFragment::width).sum() + } + + pub fn height(&self) -> Abs { + self.ascent() + self.descent() + } + + pub fn ascent(&self) -> Abs { + self.iter().map(MathFragment::ascent).max().unwrap_or_default() + } + + pub fn descent(&self) -> Abs { + self.iter().map(MathFragment::descent).max().unwrap_or_default() } pub fn to_frame(self, ctx: &MathContext) -> Frame { @@ -86,14 +107,22 @@ impl MathRow { self.to_aligned_frame(ctx, &[], align) } + pub fn to_fragment(self, ctx: &MathContext) -> MathFragment { + if self.0.len() == 1 { + self.0.into_iter().next().unwrap() + } else { + FrameFragment::new(ctx, self.to_frame(ctx)).into() + } + } + pub fn to_aligned_frame( mut self, ctx: &MathContext, points: &[Abs], align: Align, ) -> Frame { - if self.0.iter().any(|frag| matches!(frag, MathFragment::Linebreak)) { - let fragments = std::mem::take(&mut self.0); + if self.iter().any(|frag| matches!(frag, MathFragment::Linebreak)) { + let fragments: Vec<_> = std::mem::take(&mut self.0); let leading = if ctx.style.size >= MathSize::Text { ctx.styles().get(ParNode::LEADING) } else { @@ -140,7 +169,7 @@ impl MathRow { if let (Some(&first), Align::Center) = (points.first(), align) { let mut offset = first; - for fragment in &self.0 { + for fragment in self.iter() { offset -= fragment.width(); if matches!(fragment, MathFragment::Align) { x = offset; diff --git a/library/src/math/spacing.rs b/library/src/math/spacing.rs index fad1c863a..172672388 100644 --- a/library/src/math/spacing.rs +++ b/library/src/math/spacing.rs @@ -1,6 +1,5 @@ use super::*; -pub(super) const ZERO: Em = Em::zero(); pub(super) const THIN: Em = Em::new(1.0 / 6.0); pub(super) const MEDIUM: Em = Em::new(2.0 / 9.0); pub(super) const THICK: Em = Em::new(5.0 / 18.0); @@ -14,49 +13,48 @@ pub(super) fn define(math: &mut Scope) { math.define("quad", HNode::strong(QUAD).pack()); } -/// Determine the spacing between two fragments in a given style. +/// Create the spacing between two fragments in a given style. pub(super) fn spacing( - left: &MathFragment, - right: &MathFragment, - style: MathStyle, - space: bool, - space_width: Em, -) -> Em { + l: &MathFragment, + space: Option, + r: &MathFragment, +) -> Option { use MathClass::*; - let script = style.size <= MathSize::Script; - let class = |frag: &MathFragment| frag.class().unwrap_or(Special); - match (class(left), class(right)) { + + let class = |f: &MathFragment| f.class().unwrap_or(Special); + let resolve = |v: Em, f: &MathFragment| { + Some(MathFragment::Spacing(f.font_size().map_or(Abs::zero(), |size| v.at(size)))) + }; + let script = + |f: &MathFragment| f.style().map_or(false, |s| s.size <= MathSize::Script); + + match (class(l), class(r)) { // No spacing before punctuation; thin spacing after punctuation, unless // in script size. - (_, Punctuation) => ZERO, - (Punctuation, _) if !script => THIN, + (_, Punctuation) => None, + (Punctuation, _) if !script(l) => resolve(THIN, l), // No spacing after opening delimiters and before closing delimiters. - (Opening, _) | (_, Closing) => ZERO, + (Opening, _) | (_, Closing) => None, // Thick spacing around relations, unless followed by a another relation // or in script size. - (Relation, Relation) => ZERO, - (Relation, _) | (_, Relation) if !script => THICK, + (Relation, Relation) => None, + (Relation, _) if !script(l) => resolve(THICK, l), + (_, Relation) if !script(r) => resolve(THICK, r), // Medium spacing around binary operators, unless in script size. - (Binary, _) | (_, Binary) if !script => MEDIUM, + (Binary, _) if !script(l) => resolve(MEDIUM, l), + (_, Binary) if !script(r) => resolve(MEDIUM, r), // Thin spacing around large operators, unless next to a delimiter. - (Large, Opening | Fence) | (Closing | Fence, Large) => ZERO, - (Large, _) | (_, Large) => THIN, + (Large, Opening | Fence) | (Closing | Fence, Large) => None, + (Large, _) => resolve(THIN, l), + (_, Large) => resolve(THIN, r), // Spacing around spaced frames. - _ if space && (is_spaced(left) || is_spaced(right)) => space_width, + _ if (l.is_spaced() || r.is_spaced()) => space, - _ => ZERO, - } -} - -/// Whether this fragment should react to adjacent spaces. -fn is_spaced(fragment: &MathFragment) -> bool { - match fragment { - MathFragment::Frame(frame) => frame.spaced, - _ => fragment.class() == Some(MathClass::Fence), + _ => None, } } diff --git a/library/src/math/stretch.rs b/library/src/math/stretch.rs index aee226d11..d308975d0 100644 --- a/library/src/math/stretch.rs +++ b/library/src/math/stretch.rs @@ -177,6 +177,8 @@ fn assemble( c: base.c, id: None, frame, + style: base.style, + font_size: base.font_size, italics_correction: Abs::zero(), class: base.class, } diff --git a/library/src/math/style.rs b/library/src/math/style.rs index da2e2313e..9ae773467 100644 --- a/library/src/math/style.rs +++ b/library/src/math/style.rs @@ -313,7 +313,7 @@ impl LayoutMath for BbNode { } } -/// The style in a formula. +/// Text properties in a formula. #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub struct MathStyle { /// The style variant to select. diff --git a/library/src/math/underover.rs b/library/src/math/underover.rs index 3a47059c6..a1c27e595 100644 --- a/library/src/math/underover.rs +++ b/library/src/math/underover.rs @@ -270,7 +270,8 @@ fn layout( baseline = rows.len() - 1; } - ctx.push(stack(ctx, rows, Align::Center, gap, baseline)); + let frame = stack(ctx, rows, Align::Center, gap, baseline); + ctx.push(FrameFragment::new(ctx, frame)); Ok(()) } diff --git a/tests/ref/math/delimited.png b/tests/ref/math/delimited.png index ea3ab6c29..29a8c915a 100644 Binary files a/tests/ref/math/delimited.png and b/tests/ref/math/delimited.png differ diff --git a/tests/ref/math/multiline.png b/tests/ref/math/multiline.png index d8b1d8470..1433ba301 100644 Binary files a/tests/ref/math/multiline.png and b/tests/ref/math/multiline.png differ diff --git a/tests/ref/math/op.png b/tests/ref/math/op.png index 870250b7e..bb1916061 100644 Binary files a/tests/ref/math/op.png and b/tests/ref/math/op.png differ diff --git a/tests/ref/math/spacing.png b/tests/ref/math/spacing.png index fcd0d6608..38d210260 100644 Binary files a/tests/ref/math/spacing.png and b/tests/ref/math/spacing.png differ diff --git a/tests/ref/math/style.png b/tests/ref/math/style.png index 04a282088..78cfe9b1d 100644 Binary files a/tests/ref/math/style.png and b/tests/ref/math/style.png differ