diff --git a/crates/typst/src/math/matrix.rs b/crates/typst/src/math/matrix.rs index 88122a572..ca62846a2 100644 --- a/crates/typst/src/math/matrix.rs +++ b/crates/typst/src/math/matrix.rs @@ -10,7 +10,8 @@ use crate::layout::{ }; use crate::math::{ alignments, scaled_font_size, stack, style_for_denominator, AlignmentResult, - FrameFragment, GlyphFragment, LayoutMath, MathContext, Scaled, DELIM_SHORT_FALL, + FrameFragment, GlyphFragment, LayoutMath, LeftRightAlternator, MathContext, Scaled, + DELIM_SHORT_FALL, }; use crate::syntax::{Span, Spanned}; use crate::text::TextElem; @@ -67,6 +68,7 @@ impl LayoutMath for Packed { self.children(), FixedAlignment::Center, self.gap(styles), + LeftRightAlternator::Right, )?; layout_delimiters( @@ -324,6 +326,7 @@ impl LayoutMath for Packed { self.children(), FixedAlignment::Start, self.gap(styles), + LeftRightAlternator::None, )?; let (open, close) = if self.reverse(styles) { @@ -387,6 +390,7 @@ fn layout_vec_body( column: &[Content], align: FixedAlignment, row_gap: Rel, + alternator: LeftRightAlternator, ) -> SourceResult { let gap = row_gap.relative_to(ctx.regions.base().y); @@ -396,7 +400,7 @@ fn layout_vec_body( flat.push(ctx.layout_into_run(child, styles.chain(&denom_style))?); } - Ok(stack(flat, align, gap, 0)) + Ok(stack(flat, align, gap, 0, alternator)) } /// Layout the inner contents of a matrix. @@ -480,7 +484,7 @@ fn layout_mat_body( let mut y = Abs::zero(); for (cell, &(ascent, descent)) in col.into_iter().zip(&heights) { - let cell = cell.into_line_frame(&points, FixedAlignment::Center); + let cell = cell.into_line_frame(&points, LeftRightAlternator::Right); let pos = Point::new( if points.is_empty() { x + (rcol - cell.width()) / 2.0 } else { x }, y + ascent - cell.ascent(), diff --git a/crates/typst/src/math/row.rs b/crates/typst/src/math/row.rs index 0ce17ce55..59661f722 100644 --- a/crates/typst/src/math/row.rs +++ b/crates/typst/src/math/row.rs @@ -3,7 +3,7 @@ use std::iter::once; use unicode_math_class::MathClass; use crate::foundations::{Resolve, StyleChain}; -use crate::layout::{Abs, AlignElem, Em, FixedAlignment, Frame, FrameKind, Point, Size}; +use crate::layout::{Abs, AlignElem, Em, Frame, FrameKind, Point, Size}; use crate::math::{ alignments, scaled_font_size, spacing, EquationElem, FrameFragment, MathContext, MathFragment, MathParItem, MathSize, @@ -140,7 +140,7 @@ impl MathRun { pub fn into_frame(self, ctx: &MathContext, styles: StyleChain) -> Frame { if !self.is_multiline() { - self.into_line_frame(&[], AlignElem::alignment_in(styles).resolve(styles).x) + self.into_line_frame(&[], LeftRightAlternator::Right) } else { self.multiline_frame_builder(ctx, styles).build() } @@ -181,7 +181,7 @@ impl MathRun { continue; } - let sub = row.into_line_frame(&alignments.points, align); + let sub = row.into_line_frame(&alignments.points, LeftRightAlternator::Right); if i > 0 { size.y += leading; } @@ -200,43 +200,37 @@ impl MathRun { /// Lay out [`MathFragment`]s into a one-row [`Frame`], using the /// caller-provided alignment points. - pub fn into_line_frame(self, points: &[Abs], align: FixedAlignment) -> Frame { + 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 mut widths = Vec::new(); - if !points.is_empty() && align != FixedAlignment::Start { - 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 widths: Vec = 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); - let mut alternator = LeftRightAlternator::Right; - move || match align { - FixedAlignment::Start => prev_points.next(), - FixedAlignment::End => { - point_widths.next().map(|(point, width)| point - width) - } - _ => point_widths + move || { + 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, - }), + _ => prev_point, + }) } }; let mut x = next_x().unwrap_or_default(); @@ -352,8 +346,11 @@ impl> From for MathRun { } } +/// An iterator that alternates between the `Left` and `Right` values, if the +/// initial value is not `None`. #[derive(Debug, Copy, Clone, Eq, PartialEq)] -enum LeftRightAlternator { +pub enum LeftRightAlternator { + None, Left, Right, } @@ -364,6 +361,7 @@ impl Iterator for LeftRightAlternator { fn next(&mut self) -> Option { let r = Some(*self); match self { + Self::None => {} Self::Left => *self = Self::Right, Self::Right => *self = Self::Left, } diff --git a/crates/typst/src/math/underover.rs b/crates/typst/src/math/underover.rs index 4117342bd..6be86d9f5 100644 --- a/crates/typst/src/math/underover.rs +++ b/crates/typst/src/math/underover.rs @@ -3,7 +3,8 @@ use crate::foundations::{elem, Content, Packed, StyleChain}; use crate::layout::{Abs, Em, FixedAlignment, Frame, FrameItem, Point, Size}; use crate::math::{ alignments, scaled_font_size, style_cramped, style_for_subscript, AlignmentResult, - FrameFragment, GlyphFragment, LayoutMath, MathContext, MathRun, Scaled, + FrameFragment, GlyphFragment, LayoutMath, LeftRightAlternator, MathContext, MathRun, + Scaled, }; use crate::syntax::Span; use crate::text::TextElem; @@ -290,7 +291,8 @@ fn layout_underoverspreader( baseline = rows.len() - 1; } - let frame = stack(rows, FixedAlignment::Center, gap, baseline); + let frame = + stack(rows, FixedAlignment::Center, gap, baseline, LeftRightAlternator::Right); ctx.push(FrameFragment::new(ctx, styles, frame).with_class(body_class)); Ok(()) @@ -298,28 +300,30 @@ fn layout_underoverspreader( /// Stack rows on top of each other. /// -/// Add a `gap` between each row and uses the baseline of the `baseline`th -/// row for the whole frame. +/// Add a `gap` between each row and uses the baseline of the `baseline`-th +/// row for the whole frame. `alternator` controls the left/right alternating +/// alignment behavior of `AlignPointElem` in the rows. pub(super) fn stack( rows: Vec, align: FixedAlignment, gap: Abs, baseline: usize, + alternator: LeftRightAlternator, ) -> Frame { let rows: Vec<_> = rows.into_iter().flat_map(|r| r.rows()).collect(); let AlignmentResult { points, width } = alignments(&rows); let rows: Vec<_> = rows .into_iter() - .map(|row| row.into_line_frame(&points, align)) + .map(|row| row.into_line_frame(&points, alternator)) .collect(); - let mut y = Abs::zero(); let mut frame = Frame::soft(Size::new( width, rows.iter().map(|row| row.height()).sum::() + rows.len().saturating_sub(1) as f64 * gap, )); + let mut y = Abs::zero(); for (i, row) in rows.into_iter().enumerate() { let x = align.position(width - row.width()); let pos = Point::new(x, y); diff --git a/tests/ref/issue-3973-math-equation-align.png b/tests/ref/issue-3973-math-equation-align.png new file mode 100644 index 000000000..91b48849a Binary files /dev/null and b/tests/ref/issue-3973-math-equation-align.png differ diff --git a/tests/suite/math/alignment.typ b/tests/suite/math/alignment.typ index f110b139e..63033ef5c 100644 --- a/tests/suite/math/alignment.typ +++ b/tests/suite/math/alignment.typ @@ -32,3 +32,20 @@ $ a &=b & quad c&=d \ e &=f & g&=h $ + +--- issue-3973-math-equation-align --- +// In this bug, the alignment set with "show math.equation: set align(...)" +// overrides the left-right alternating behavior of alignment points. +#let equations = [ +$ a + b &= c \ + e &= f + g + h $ +$ a &= b + c \ + e + f + g &= h $ +] +#equations + +#show math.equation: set align(start) +#equations + +#show math.equation: set align(end) +#equations