From 012e14d40cb44997630cf6469a446f217f2e9057 Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 31 Mar 2025 09:38:04 +0000 Subject: [PATCH] Unify layout of `vec` and `cases` with `mat` (#5934) --- crates/typst-layout/src/math/mat.rs | 195 +++++++++++----------- crates/typst-layout/src/math/shared.rs | 16 +- crates/typst-layout/src/math/underover.rs | 10 +- tests/ref/math-cases-linebreaks.png | Bin 570 -> 506 bytes tests/ref/math-equation-font.png | Bin 984 -> 1032 bytes tests/ref/math-mat-vec-cases-unity.png | Bin 0 -> 1202 bytes tests/ref/math-vec-linebreaks.png | Bin 856 -> 651 bytes tests/suite/math/cases.typ | 4 +- tests/suite/math/mat.typ | 11 +- tests/suite/math/vec.typ | 4 +- 10 files changed, 118 insertions(+), 122 deletions(-) create mode 100644 tests/ref/math-mat-vec-cases-unity.png diff --git a/crates/typst-layout/src/math/mat.rs b/crates/typst-layout/src/math/mat.rs index bf4929026..d678f8658 100644 --- a/crates/typst-layout/src/math/mat.rs +++ b/crates/typst-layout/src/math/mat.rs @@ -1,4 +1,4 @@ -use typst_library::diag::{bail, SourceResult}; +use typst_library::diag::{bail, warning, SourceResult}; use typst_library::foundations::{Content, Packed, Resolve, StyleChain}; use typst_library::layout::{ Abs, Axes, Em, FixedAlignment, Frame, FrameItem, Point, Ratio, Rel, Size, @@ -9,7 +9,7 @@ use typst_library::visualize::{FillRule, FixedStroke, Geometry, LineCap, Shape}; use typst_syntax::Span; use super::{ - alignments, delimiter_alignment, stack, style_for_denominator, AlignmentResult, + alignments, delimiter_alignment, style_for_denominator, AlignmentResult, FrameFragment, GlyphFragment, LeftRightAlternator, MathContext, DELIM_SHORT_FALL, }; @@ -23,67 +23,23 @@ pub fn layout_vec( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - let delim = elem.delim(styles); - let frame = layout_vec_body( + let span = elem.span(); + + let column: Vec<&Content> = elem.children.iter().collect(); + let frame = layout_body( ctx, styles, - &elem.children, + &[column], elem.align(styles), - elem.gap(styles), LeftRightAlternator::Right, + None, + Axes::with_y(elem.gap(styles)), + span, + "elements", )?; - layout_delimiters(ctx, styles, frame, delim.open(), delim.close(), elem.span()) -} - -/// Lays out a [`MatElem`]. -#[typst_macros::time(name = "math.mat", span = elem.span())] -pub fn layout_mat( - elem: &Packed, - ctx: &mut MathContext, - styles: StyleChain, -) -> SourceResult<()> { - let augment = elem.augment(styles); - let rows = &elem.rows; - - if let Some(aug) = &augment { - for &offset in &aug.hline.0 { - if offset == 0 || offset.unsigned_abs() >= rows.len() { - bail!( - elem.span(), - "cannot draw a horizontal line after row {} of a matrix with {} rows", - if offset < 0 { rows.len() as isize + offset } else { offset }, - rows.len() - ); - } - } - - let ncols = rows.first().map_or(0, |row| row.len()); - - for &offset in &aug.vline.0 { - if offset == 0 || offset.unsigned_abs() >= ncols { - bail!( - elem.span(), - "cannot draw a vertical line after column {} of a matrix with {} columns", - if offset < 0 { ncols as isize + offset } else { offset }, - ncols - ); - } - } - } - let delim = elem.delim(styles); - let frame = layout_mat_body( - ctx, - styles, - rows, - elem.align(styles), - augment, - Axes::new(elem.column_gap(styles), elem.row_gap(styles)), - elem.span(), - )?; - - layout_delimiters(ctx, styles, frame, delim.open(), delim.close(), elem.span()) + layout_delimiters(ctx, styles, frame, delim.open(), delim.close(), span) } /// Lays out a [`CasesElem`]. @@ -93,60 +49,100 @@ pub fn layout_cases( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - let delim = elem.delim(styles); - let frame = layout_vec_body( + let span = elem.span(); + + let column: Vec<&Content> = elem.children.iter().collect(); + let frame = layout_body( ctx, styles, - &elem.children, + &[column], FixedAlignment::Start, - elem.gap(styles), LeftRightAlternator::None, + None, + Axes::with_y(elem.gap(styles)), + span, + "branches", )?; + let delim = elem.delim(styles); let (open, close) = if elem.reverse(styles) { (None, delim.close()) } else { (delim.open(), None) }; - - layout_delimiters(ctx, styles, frame, open, close, elem.span()) + layout_delimiters(ctx, styles, frame, open, close, span) } -/// Layout the inner contents of a vector. -fn layout_vec_body( +/// Lays out a [`MatElem`]. +#[typst_macros::time(name = "math.mat", span = elem.span())] +pub fn layout_mat( + elem: &Packed, ctx: &mut MathContext, styles: StyleChain, - column: &[Content], - align: FixedAlignment, - row_gap: Rel, - alternator: LeftRightAlternator, -) -> SourceResult { - let gap = row_gap.relative_to(ctx.region.size.y); +) -> SourceResult<()> { + let span = elem.span(); + let rows = &elem.rows; + let ncols = rows.first().map_or(0, |row| row.len()); - let denom_style = style_for_denominator(styles); - let mut flat = vec![]; - for child in column { - // We allow linebreaks in cases and vectors, which are functionally - // identical to commas. - flat.extend(ctx.layout_into_run(child, styles.chain(&denom_style))?.rows()); + let augment = elem.augment(styles); + if let Some(aug) = &augment { + for &offset in &aug.hline.0 { + if offset == 0 || offset.unsigned_abs() >= rows.len() { + bail!( + span, + "cannot draw a horizontal line after row {} of a matrix with {} rows", + if offset < 0 { rows.len() as isize + offset } else { offset }, + rows.len() + ); + } + } + + for &offset in &aug.vline.0 { + if offset == 0 || offset.unsigned_abs() >= ncols { + bail!( + span, + "cannot draw a vertical line after column {} of a matrix with {} columns", + if offset < 0 { ncols as isize + offset } else { offset }, + ncols + ); + } + } } - // We pad ascent and descent with the ascent and descent of the paren - // to ensure that normal vectors are aligned with others unless they are - // way too big. - let paren = - GlyphFragment::new(ctx, styles.chain(&denom_style), '(', Span::detached()); - Ok(stack(flat, align, gap, 0, alternator, Some((paren.ascent, paren.descent)))) + + // Transpose rows of the matrix into columns. + let mut row_iters: Vec<_> = rows.iter().map(|i| i.iter()).collect(); + let columns: Vec> = (0..ncols) + .map(|_| row_iters.iter_mut().map(|i| i.next().unwrap()).collect()) + .collect(); + + let frame = layout_body( + ctx, + styles, + &columns, + elem.align(styles), + LeftRightAlternator::Right, + augment, + Axes::new(elem.column_gap(styles), elem.row_gap(styles)), + span, + "cells", + )?; + + let delim = elem.delim(styles); + layout_delimiters(ctx, styles, frame, delim.open(), delim.close(), span) } -/// Layout the inner contents of a matrix. -fn layout_mat_body( +/// Layout the inner contents of a matrix, vector, or cases. +#[allow(clippy::too_many_arguments)] +fn layout_body( ctx: &mut MathContext, styles: StyleChain, - rows: &[Vec], + columns: &[Vec<&Content>], align: FixedAlignment, + alternator: LeftRightAlternator, augment: Option>, gap: Axes>, span: Span, + children: &str, ) -> SourceResult { - let ncols = rows.first().map_or(0, |row| row.len()); - let nrows = rows.len(); + let nrows = columns.first().map_or(0, |col| col.len()); + let ncols = columns.len(); if ncols == 0 || nrows == 0 { return Ok(Frame::soft(Size::zero())); } @@ -178,16 +174,11 @@ fn layout_mat_body( // Before the full matrix body can be laid out, the // individual cells must first be independently laid out // so we can ensure alignment across rows and columns. + let mut cols = vec![vec![]; ncols]; // This variable stores the maximum ascent and descent for each row. let mut heights = vec![(Abs::zero(), Abs::zero()); nrows]; - // We want to transpose our data layout to columns - // before final layout. For efficiency, the columns - // variable is set up here and newly generated - // individual cells are then added to it. - let mut cols = vec![vec![]; ncols]; - let denom_style = style_for_denominator(styles); // We pad ascent and descent with the ascent and descent of the paren // to ensure that normal matrices are aligned with others unless they are @@ -195,10 +186,22 @@ fn layout_mat_body( let paren = GlyphFragment::new(ctx, styles.chain(&denom_style), '(', Span::detached()); - for (row, (ascent, descent)) in rows.iter().zip(&mut heights) { - for (cell, col) in row.iter().zip(&mut cols) { + for (column, col) in columns.iter().zip(&mut cols) { + for (cell, (ascent, descent)) in column.iter().zip(&mut heights) { + let cell_span = cell.span(); let cell = ctx.layout_into_run(cell, styles.chain(&denom_style))?; + // We ignore linebreaks in the cells as we can't differentiate + // alignment points for the whole body from ones for a specific + // cell, and multiline cells don't quite make sense at the moment. + if cell.is_multiline() { + ctx.engine.sink.warn(warning!( + cell_span, + "linebreaks are ignored in {}", children; + hint: "use commas instead to separate each line" + )); + } + ascent.set_max(cell.ascent().max(paren.ascent)); descent.set_max(cell.descent().max(paren.descent)); @@ -222,7 +225,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, LeftRightAlternator::Right); + let cell = cell.into_line_frame(&points, alternator); let pos = Point::new( if points.is_empty() { x + align.position(rcol - cell.width()) diff --git a/crates/typst-layout/src/math/shared.rs b/crates/typst-layout/src/math/shared.rs index 5aebdacac..600c130d4 100644 --- a/crates/typst-layout/src/math/shared.rs +++ b/crates/typst-layout/src/math/shared.rs @@ -117,7 +117,6 @@ pub fn stack( gap: Abs, baseline: usize, alternator: LeftRightAlternator, - minimum_ascent_descent: Option<(Abs, Abs)>, ) -> Frame { let AlignmentResult { points, width } = alignments(&rows); let rows: Vec<_> = rows @@ -125,13 +124,9 @@ pub fn stack( .map(|row| row.into_line_frame(&points, alternator)) .collect(); - let padded_height = |height: Abs| { - height.max(minimum_ascent_descent.map_or(Abs::zero(), |(a, d)| a + d)) - }; - let mut frame = Frame::soft(Size::new( width, - rows.iter().map(|row| padded_height(row.height())).sum::() + rows.iter().map(|row| row.height()).sum::() + rows.len().saturating_sub(1) as f64 * gap, )); @@ -142,14 +137,11 @@ pub fn stack( } else { Abs::zero() }; - let ascent_padded_part = minimum_ascent_descent - .map_or(Abs::zero(), |(a, _)| (a - row.ascent())) - .max(Abs::zero()); - let pos = Point::new(x, y + ascent_padded_part); + let pos = Point::new(x, y); if i == baseline { - frame.set_baseline(y + row.baseline() + ascent_padded_part); + frame.set_baseline(y + row.baseline()); } - y += padded_height(row.height()) + gap; + y += row.height() + gap; frame.push_frame(pos, row); } diff --git a/crates/typst-layout/src/math/underover.rs b/crates/typst-layout/src/math/underover.rs index 7b3617c3e..5b6bd40eb 100644 --- a/crates/typst-layout/src/math/underover.rs +++ b/crates/typst-layout/src/math/underover.rs @@ -312,14 +312,8 @@ fn layout_underoverspreader( } }; - let frame = stack( - rows, - FixedAlignment::Center, - gap, - baseline, - LeftRightAlternator::Right, - None, - ); + let frame = + stack(rows, FixedAlignment::Center, gap, baseline, LeftRightAlternator::Right); ctx.push(FrameFragment::new(styles, frame).with_class(body_class)); Ok(()) diff --git a/tests/ref/math-cases-linebreaks.png b/tests/ref/math-cases-linebreaks.png index 543d5384c11a270a8a56f95e91e4f5ec7ac64d3f..eb4971c46fb2d2a36a8b95324d3d1e08b7d99319 100644 GIT binary patch delta 481 zcmV<70UrLk1o{JzBYy$GNklVP4Bt@3Ph~{FOY>~-6-Ym~#Fk@(%=gn7d8s##_|N9-h|Ka!jzuw_< z_^dgPGQ4DaHy6DPTURhgFj~rC+X$d z*Zcb|6!U@Jf`%y^^#b7U_EF#qbX=t@;hh41RPS2=ydL}DDms+_!Qf*H+cWR{nR?pA zq`(??MII3`luOhtmxVHZX&+S@xjHmcBS8ICMZZCNQe*yTvW{Iao?K$2M~*ie0O?xd zfm<&i8Ue;I67~?JUDnVX5>L}krf~T9e>)`Nb!69DT3Yx)FDVSiZpn3kTjVGv*L@b; z9);nE831eTfQMp?-OV?nMup+DZMjN3pfFt9L$sWP6f+Hsn<5lQAABqA!TMHEz9cRe8q zCP#{hjKH8aMcJl}i-<6+$P9wogh3C29$-Qlq}1S~m52`p2z#sqtf$p4{Ie*W^1g3T14$a%T?k;imS}Xu*AxThl5vuxwIhJd)kH^6!2pL7V0t2{+Kd2Rt|zK1 zybSY`$6yGPRKJY2V+%JoeQ>bv>DrQZjCWjjc|X8&ESNzC7Y7YIRV(jmr_iptIA~YB z-6rqaw$XMR?0-%H7M)9VA)QztpfLkw!h6e6LH9V$hchRamc!{O9Vd7bc z>fVN>mR(XB%vvNLd=(mCZyh=!Qns)PgX2aQ^RQ|gV6G-xUS(@I1Tf56%Ny$~&Kro0$&|(nW5as&2^UIJ kA*{Z0sT*Jwtb$SJ2Wbl+-x_NR>i_@%07*qoM6N<$f>qD?{Qv*} diff --git a/tests/ref/math-equation-font.png b/tests/ref/math-equation-font.png index eb84634e5a8f2c5e42a606d12505d505d63c3294..ec3c72311b4a40abad4b53074e7870c2042087db 100644 GIT binary patch delta 1023 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 N002ovPDHLkV1gW(A;bUx delta 834 zcmV-I1HJr<1=t3VBYy)QNklcHB<5?*i@o@c^IbymBXCA^i@1L`WLh=a zgU^jTb#dE4$bXZqC~OqQm3$Ew-hkXWuaCnxN=03KIU7<@`y!?cMnklSqpBe@HF;?- zVO-0hEiSIuro|u97R=L`pIQG*hV{y#uvIaNeZ9SP|Y{X{S2<~uHaTO2m z637y7d<>?bw0amLGBfQbmKmYOl2KwS2L4!7?~jkF2PnhD2f9Yi+RdowDKo;`>7%O*OBzw zDnwhnrIR@4Jm#Qgm)i;4vX#0xqJ}sJ{KQ`Uo4}>}sEg0s5nJ2LK`BGTGxVd_eQmgM zQ>lxq;&3T$H_D+)vP7I!fGfKuzZp8Iin@3@0DsaHgt%!Khu(>}