diff options
| author | Laurenz <laurmaedje@gmail.com> | 2024-10-27 19:04:55 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-10-27 18:04:55 +0000 |
| commit | be7cfc85d08c545abfac08098b7b33b4bd71f37e (patch) | |
| tree | f4137fa2aaa57babae1f7603a9b2ed7e688f43d8 /crates/typst-layout/src/math/run.rs | |
| parent | b8034a343831e8609aec2ec81eb7eeda57aa5d81 (diff) | |
Split out four new crates (#5302)
Diffstat (limited to 'crates/typst-layout/src/math/run.rs')
| -rw-r--r-- | crates/typst-layout/src/math/run.rs | 469 |
1 files changed, 469 insertions, 0 deletions
diff --git a/crates/typst-layout/src/math/run.rs b/crates/typst-layout/src/math/run.rs new file mode 100644 index 00000000..8f12c509 --- /dev/null +++ b/crates/typst-layout/src/math/run.rs @@ -0,0 +1,469 @@ +use std::iter::once; + +use typst_library::foundations::{Resolve, StyleChain}; +use typst_library::layout::{Abs, AlignElem, Em, Frame, InlineItem, Point, Size}; +use typst_library::math::{EquationElem, MathSize, MEDIUM, THICK, THIN}; +use typst_library::model::ParElem; +use unicode_math_class::MathClass; + +use super::{alignments, scaled_font_size, FrameFragment, MathContext, MathFragment}; + +const TIGHT_LEADING: Em = Em::new(0.25); + +/// A linear collection of [`MathFragment`]s. +#[derive(Debug, Default, Clone)] +pub struct MathRun(Vec<MathFragment>); + +impl MathRun { + /// Takes the given [`MathFragment`]s and do some basic processing. + pub fn new(fragments: Vec<MathFragment>) -> Self { + let iter = fragments.into_iter().peekable(); + let mut last: Option<usize> = None; + let mut space: Option<MathFragment> = None; + let mut resolved: Vec<MathFragment> = vec![]; + + for mut fragment in iter { + match fragment { + // Keep space only if supported by spaced fragments. + MathFragment::Space(_) => { + if last.is_some() { + space = Some(fragment); + } + continue; + } + + // Explicit spacing disables automatic spacing. + MathFragment::Spacing(width, weak) => { + last = None; + space = None; + + if weak { + match resolved.last_mut() { + None => continue, + Some(MathFragment::Spacing(prev, true)) => { + *prev = (*prev).max(width); + continue; + } + Some(_) => {} + } + } + + 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; + } + + _ => {} + } + + // Convert variable operators into binary operators if something + // precedes them and they are not preceded by a operator or comparator. + if fragment.class() == MathClass::Vary + && matches!( + last.map(|i| resolved[i].class()), + Some( + MathClass::Normal + | MathClass::Alphabetic + | MathClass::Closing + | MathClass::Fence + ) + ) + { + fragment.set_class(MathClass::Binary); + } + + // Insert spacing between the last and this non-ignorant item. + if !fragment.is_ignorant() { + if let Some(i) = last { + if let Some(s) = spacing(&resolved[i], space.take(), &fragment) { + resolved.insert(i + 1, s); + } + } + + last = Some(resolved.len()); + } + + resolved.push(fragment); + } + + if let Some(MathFragment::Spacing(_, true)) = resolved.last() { + resolved.pop(); + } + + Self(resolved) + } + + pub fn iter(&self) -> std::slice::Iter<'_, MathFragment> { + self.0.iter() + } + + /// Split by linebreaks, and copy [`MathFragment`]s into rows. + pub fn rows(&self) -> Vec<Self> { + self.0 + .split(|frag| matches!(frag, MathFragment::Linebreak)) + .map(|slice| Self(slice.to_vec())) + .collect() + } + + pub fn row_count(&self) -> usize { + let mut count = + 1 + self.0.iter().filter(|f| matches!(f, MathFragment::Linebreak)).count(); + + // A linebreak at the very end does not introduce an extra row. + if let Some(f) = self.0.last() { + if matches!(f, MathFragment::Linebreak) { + count -= 1 + } + } + count + } + + pub fn ascent(&self) -> Abs { + self.iter() + .filter(|e| affects_row_height(e)) + .map(|e| e.ascent()) + .max() + .unwrap_or_default() + } + + pub fn descent(&self) -> Abs { + self.iter() + .filter(|e| affects_row_height(e)) + .map(|e| e.descent()) + .max() + .unwrap_or_default() + } + + pub fn class(&self) -> MathClass { + // Predict the class of the output of 'into_fragment' + if self.0.len() == 1 { + self.0 + .first() + .map(|fragment| fragment.class()) + .unwrap_or(MathClass::Normal) + } else { + // FrameFragment::new() (inside 'into_fragment' in this branch) defaults + // to MathClass::Normal for its class. + MathClass::Normal + } + } + + pub fn into_frame(self, ctx: &MathContext, styles: StyleChain) -> Frame { + if !self.is_multiline() { + self.into_line_frame(&[], LeftRightAlternator::Right) + } else { + self.multiline_frame_builder(ctx, styles).build() + } + } + + pub fn into_fragment(self, ctx: &MathContext, styles: StyleChain) -> MathFragment { + if self.0.len() == 1 { + return self.0.into_iter().next().unwrap(); + } + + // Fragments without a math_size are ignored: the notion of size do not + // apply to them, so their text-likeness is meaningless. + let text_like = self + .iter() + .filter(|e| e.math_size().is_some()) + .all(|e| e.is_text_like()); + + FrameFragment::new(ctx, styles, self.into_frame(ctx, styles)) + .with_text_like(text_like) + .into() + } + + /// Returns a builder that lays out the [`MathFragment`]s into a possibly + /// multi-row [`Frame`]. The rows are aligned using the same set of alignment + /// points computed from them as a whole. + pub fn multiline_frame_builder( + self, + ctx: &MathContext, + styles: StyleChain, + ) -> MathRunFrameBuilder { + let rows: Vec<_> = self.rows(); + let row_count = rows.len(); + let alignments = alignments(&rows); + + let leading = if EquationElem::size_in(styles) >= MathSize::Text { + ParElem::leading_in(styles) + } else { + let font_size = scaled_font_size(ctx, styles); + TIGHT_LEADING.at(font_size) + }; + + let align = AlignElem::alignment_in(styles).resolve(styles).x; + let mut frames: Vec<(Frame, Point)> = vec![]; + let mut size = Size::zero(); + for (i, row) in rows.into_iter().enumerate() { + if i == row_count - 1 && row.0.is_empty() { + continue; + } + + let sub = row.into_line_frame(&alignments.points, LeftRightAlternator::Right); + if i > 0 { + size.y += leading; + } + + let mut pos = Point::with_y(size.y); + if alignments.points.is_empty() { + pos.x = align.position(alignments.width - sub.width()); + } + size.x.set_max(sub.width()); + size.y += sub.height(); + frames.push((sub, pos)); + } + + MathRunFrameBuilder { size, frames } + } + + /// Lay out [`MathFragment`]s into a one-row [`Frame`], using the + /// caller-provided alignment points. + pub fn into_line_frame( + self, + points: &[Abs], + mut alternator: LeftRightAlternator, + ) -> Frame { + let ascent = self.ascent(); + let mut frame = Frame::soft(Size::new(Abs::zero(), ascent + self.descent())); + frame.set_baseline(ascent); + + let mut next_x = { + let widths: Vec<Abs> = if points.is_empty() { + vec![] + } else { + self.iter() + .as_slice() + .split(|e| matches!(e, MathFragment::Align)) + .map(|chunk| chunk.iter().map(|e| e.width()).sum()) + .collect() + }; + + let mut prev_points = once(Abs::zero()).chain(points.iter().copied()); + let mut point_widths = points.iter().copied().zip(widths); + move || { + point_widths + .next() + .zip(prev_points.next()) + .zip(alternator.next()) + .map(|(((point, width), prev_point), alternator)| match alternator { + LeftRightAlternator::Right => point - width, + _ => prev_point, + }) + } + }; + let mut x = next_x().unwrap_or_default(); + + for fragment in self.0.into_iter() { + if matches!(fragment, MathFragment::Align) { + x = next_x().unwrap_or(x); + continue; + } + + let y = ascent - fragment.ascent(); + let pos = Point::new(x, y); + x += fragment.width(); + frame.push_frame(pos, fragment.into_frame()); + } + + frame.size_mut().x = x; + frame + } + + pub fn into_par_items(self) -> Vec<InlineItem> { + let mut items = vec![]; + + let mut x = Abs::zero(); + let mut ascent = Abs::zero(); + let mut descent = Abs::zero(); + let mut frame = Frame::soft(Size::zero()); + let mut empty = true; + + let finalize_frame = |frame: &mut Frame, x, ascent, descent| { + frame.set_size(Size::new(x, ascent + descent)); + frame.set_baseline(Abs::zero()); + frame.translate(Point::with_y(ascent)); + }; + + let mut space_is_visible = false; + + let is_relation = |f: &MathFragment| matches!(f.class(), MathClass::Relation); + let is_space = |f: &MathFragment| { + matches!(f, MathFragment::Space(_) | MathFragment::Spacing(_, _)) + }; + + let mut iter = self.0.into_iter().peekable(); + while let Some(fragment) = iter.next() { + if space_is_visible { + match fragment { + MathFragment::Space(width) | MathFragment::Spacing(width, _) => { + items.push(InlineItem::Space(width, true)); + continue; + } + _ => {} + } + } + + let class = fragment.class(); + let y = fragment.ascent(); + + ascent.set_max(y); + descent.set_max(fragment.descent()); + + let pos = Point::new(x, -y); + x += fragment.width(); + frame.push_frame(pos, fragment.into_frame()); + empty = false; + + if class == MathClass::Binary + || (class == MathClass::Relation + && !iter.peek().map(is_relation).unwrap_or_default()) + { + let mut frame_prev = + std::mem::replace(&mut frame, Frame::soft(Size::zero())); + + finalize_frame(&mut frame_prev, x, ascent, descent); + items.push(InlineItem::Frame(frame_prev)); + empty = true; + + x = Abs::zero(); + ascent = Abs::zero(); + descent = Abs::zero(); + + space_is_visible = true; + if let Some(f_next) = iter.peek() { + if !is_space(f_next) { + items.push(InlineItem::Space(Abs::zero(), true)); + } + } + } else { + space_is_visible = false; + } + } + + // Don't use `frame.is_empty()` because even an empty frame can + // contribute width (if it had hidden content). + if !empty { + finalize_frame(&mut frame, x, ascent, descent); + items.push(InlineItem::Frame(frame)); + } + + items + } + + pub fn is_multiline(&self) -> bool { + self.iter().any(|frag| matches!(frag, MathFragment::Linebreak)) + } +} + +impl<T: Into<MathFragment>> From<T> for MathRun { + fn from(fragment: T) -> Self { + Self(vec![fragment.into()]) + } +} + +/// An iterator that alternates between the `Left` and `Right` values, if the +/// initial value is not `None`. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum LeftRightAlternator { + None, + Left, + Right, +} + +impl Iterator for LeftRightAlternator { + type Item = LeftRightAlternator; + + fn next(&mut self) -> Option<Self::Item> { + let r = Some(*self); + match self { + Self::None => {} + Self::Left => *self = Self::Right, + Self::Right => *self = Self::Left, + } + r + } +} + +/// How the rows from the [`MathRun`] should be aligned and merged into a [`Frame`]. +pub struct MathRunFrameBuilder { + /// The size of the resulting frame. + pub size: Size, + /// Each row's frame, and the position where the frame should + /// be pushed into the resulting frame. + pub frames: Vec<(Frame, Point)>, +} + +impl MathRunFrameBuilder { + /// Consumes the builder and returns a [`Frame`]. + pub fn build(self) -> Frame { + let mut frame = Frame::soft(self.size); + for (sub, pos) in self.frames.into_iter() { + frame.push_frame(pos, sub); + } + frame + } +} + +fn affects_row_height(fragment: &MathFragment) -> bool { + !matches!(fragment, MathFragment::Align | MathFragment::Linebreak) +} + +/// Create the spacing between two fragments in a given style. +fn spacing( + l: &MathFragment, + space: Option<MathFragment>, + r: &MathFragment, +) -> Option<MathFragment> { + use MathClass::*; + + let resolve = |v: Em, size_ref: &MathFragment| -> Option<MathFragment> { + let width = size_ref.font_size().map_or(Abs::zero(), |size| v.at(size)); + Some(MathFragment::Spacing(width, false)) + }; + let script = |f: &MathFragment| f.math_size().is_some_and(|s| s <= MathSize::Script); + + match (l.class(), r.class()) { + // No spacing before punctuation; thin spacing after punctuation, unless + // in script size. + (_, Punctuation) => None, + (Punctuation, _) if !script(l) => resolve(THIN, l), + + // No spacing after opening delimiters and before closing delimiters. + (Opening, _) | (_, Closing) => None, + + // Thick spacing around relations, unless followed by a another relation + // or in script size. + (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, _) if !script(l) => resolve(MEDIUM, l), + (_, Binary) if !script(r) => resolve(MEDIUM, r), + + // Thin spacing around large operators, unless to the left of + // an opening delimiter. TeXBook, p170 + (Large, Opening | Fence) => None, + (Large, _) => resolve(THIN, l), + (_, Large) => resolve(THIN, r), + + // Spacing around spaced frames. + _ if (l.is_spaced() || r.is_spaced()) => space, + + _ => None, + } +} |
