diff options
Diffstat (limited to 'crates/typst-library/src/math/row.rs')
| -rw-r--r-- | crates/typst-library/src/math/row.rs | 258 |
1 files changed, 258 insertions, 0 deletions
diff --git a/crates/typst-library/src/math/row.rs b/crates/typst-library/src/math/row.rs new file mode 100644 index 00000000..687f82b8 --- /dev/null +++ b/crates/typst-library/src/math/row.rs @@ -0,0 +1,258 @@ +use std::iter::once; + +use crate::layout::AlignElem; + +use super::*; + +pub const TIGHT_LEADING: Em = Em::new(0.25); + +#[derive(Debug, Default, Clone)] +pub struct MathRow(Vec<MathFragment>); + +impl MathRow { + 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(_) => { + 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; + } + + _ => {} + } + + // Convert variable operators into binary operators if something + // precedes them and they are not preceded by a operator or comparator. + if fragment.class() == Some(MathClass::Vary) + && matches!( + last.and_then(|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 item. + 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); + } + + Self(resolved) + } + + pub fn iter(&self) -> std::slice::Iter<'_, MathFragment> { + self.0.iter() + } + + /// Extract the sublines of the row. + /// + /// It is very unintuitive, but in current state of things, a `MathRow` can + /// contain several actual rows. That function deconstructs it to "single" + /// rows. Hopefully this is only a temporary hack. + pub fn rows(&self) -> Vec<Self> { + self.0 + .split(|frag| matches!(frag, MathFragment::Linebreak)) + .map(|slice| Self(slice.to_vec())) + .collect() + } + + 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 class(&self) -> MathClass { + // Predict the class of the output of 'into_fragment' + if self.0.len() == 1 { + self.0 + .first() + .and_then(|fragment| fragment.class()) + .unwrap_or(MathClass::Special) + } 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) -> Frame { + let styles = ctx.styles(); + let align = AlignElem::alignment_in(styles).x.resolve(styles); + self.into_aligned_frame(ctx, &[], align) + } + + pub fn into_fragment(self, ctx: &MathContext) -> MathFragment { + if self.0.len() == 1 { + self.0.into_iter().next().unwrap() + } else { + FrameFragment::new(ctx, self.into_frame(ctx)).into() + } + } + + pub fn into_aligned_frame( + self, + ctx: &MathContext, + points: &[Abs], + align: Align, + ) -> Frame { + if self.iter().any(|frag| matches!(frag, MathFragment::Linebreak)) { + let leading = if ctx.style.size >= MathSize::Text { + ParElem::leading_in(ctx.styles()) + } else { + TIGHT_LEADING.scaled(ctx) + }; + + let mut rows: Vec<_> = self.rows(); + + if matches!(rows.last(), Some(row) if row.0.is_empty()) { + rows.pop(); + } + + let AlignmentResult { points, width } = alignments(&rows); + let mut frame = Frame::new(Size::zero()); + + for (i, row) in rows.into_iter().enumerate() { + let sub = row.into_line_frame(&points, align); + let size = frame.size_mut(); + if i > 0 { + size.y += leading; + } + + let mut pos = Point::with_y(size.y); + if points.is_empty() { + pos.x = align.position(width - sub.width()); + } + size.y += sub.height(); + size.x.set_max(sub.width()); + frame.push_frame(pos, sub); + } + frame + } else { + self.into_line_frame(points, align) + } + } + + fn into_line_frame(self, points: &[Abs], align: Align) -> Frame { + let ascent = self.ascent(); + let mut frame = Frame::new(Size::new(Abs::zero(), ascent + self.descent())); + frame.set_baseline(ascent); + + let mut next_x = { + let mut widths = Vec::new(); + if !points.is_empty() && align != Align::Left { + let mut width = Abs::zero(); + for fragment in self.iter() { + if matches!(fragment, MathFragment::Align) { + widths.push(width); + width = Abs::zero(); + } else { + width += fragment.width(); + } + } + widths.push(width); + } + let widths = widths; + + let mut prev_points = once(Abs::zero()).chain(points.iter().copied()); + let mut point_widths = points.iter().copied().zip(widths); + let mut alternator = LeftRightAlternator::Right; + move || match align { + Align::Left => prev_points.next(), + Align::Right => point_widths.next().map(|(point, width)| point - width), + _ => point_widths + .next() + .zip(prev_points.next()) + .zip(alternator.next()) + .map(|(((point, width), prev_point), alternator)| match alternator { + LeftRightAlternator::Left => prev_point, + LeftRightAlternator::Right => point - width, + }), + } + }; + 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 + } +} + +impl<T: Into<MathFragment>> From<T> for MathRow { + fn from(fragment: T) -> Self { + Self(vec![fragment.into()]) + } +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +enum LeftRightAlternator { + Left, + Right, +} + +impl Iterator for LeftRightAlternator { + type Item = LeftRightAlternator; + + fn next(&mut self) -> Option<Self::Item> { + let r = Some(*self); + match self { + Self::Left => *self = Self::Right, + Self::Right => *self = Self::Left, + } + r + } +} |
