This commit is contained in:
mkorje 2025-01-15 22:36:02 +11:00 committed by mkorje
parent f7bb5f72c5
commit 0a812963e3
No known key found for this signature in database
5 changed files with 223 additions and 34 deletions

View File

@ -220,7 +220,7 @@ fn layout_body(
let mut x = Abs::zero(); let mut x = Abs::zero();
for (index, col) in cols.into_iter().enumerate() { for (index, col) in cols.into_iter().enumerate() {
let AlignmentResult { points, width: rcol } = alignments(&col, Abs::zero()); let AlignmentResult { points, width: rcol, .. } = alignments(&col, None);
let mut y = Abs::zero(); let mut y = Abs::zero();

View File

@ -98,6 +98,12 @@ pub fn layout_equation_inline(
Ok(items) Ok(items)
} }
pub struct EquationSizings<'a> {
region_size_x: Abs,
gaps: &'a [GapSizing<Abs>],
padding: &'a [GapSizing<Abs>],
}
/// Layout a block-level equation (in a flow). /// Layout a block-level equation (in a flow).
#[typst_macros::time(span = elem.span())] #[typst_macros::time(span = elem.span())]
pub fn layout_equation_block( pub fn layout_equation_block(
@ -118,9 +124,17 @@ pub fn layout_equation_block(
let scale_style = style_for_script_scale(&ctx); let scale_style = style_for_script_scale(&ctx);
let styles = styles.chain(&scale_style); let styles = styles.chain(&scale_style);
let gaps = elem.column_gap(styles).resolve(styles);
let padding = elem.column_padding(styles).resolve(styles);
let sizings = EquationSizings {
region_size_x: regions.size.x,
gaps: gaps.0.as_slice(),
padding: padding.0.as_slice(),
};
let full_equation_builder = ctx let full_equation_builder = ctx
.layout_into_run(&elem.body, styles)? .layout_into_run(&elem.body, styles)?
.multiline_frame_builder(styles); .multiline_frame_builder(styles, Some(sizings));
let width = full_equation_builder.size.x; let width = full_equation_builder.size.x;
let equation_builders = if BlockElem::breakable_in(styles) { let equation_builders = if BlockElem::breakable_in(styles) {

View File

@ -6,7 +6,7 @@ use typst_library::math::{EquationElem, MathSize, MEDIUM, THICK, THIN};
use typst_library::model::ParElem; use typst_library::model::ParElem;
use unicode_math_class::MathClass; use unicode_math_class::MathClass;
use super::{alignments, FrameFragment, MathFragment}; use super::{alignments, EquationSizings, FrameFragment, MathFragment};
const TIGHT_LEADING: Em = Em::new(0.25); const TIGHT_LEADING: Em = Em::new(0.25);
@ -165,7 +165,7 @@ impl MathRun {
if !self.is_multiline() { if !self.is_multiline() {
self.into_line_frame(&[], LeftRightAlternator::Right) self.into_line_frame(&[], LeftRightAlternator::Right)
} else { } else {
self.multiline_frame_builder(styles).build() self.multiline_frame_builder(styles, None).build()
} }
} }
@ -189,12 +189,15 @@ impl MathRun {
/// Returns a builder that lays out the [`MathFragment`]s into a possibly /// 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 /// multi-row [`Frame`]. The rows are aligned using the same set of alignment
/// points computed from them as a whole. /// points computed from them as a whole.
pub fn multiline_frame_builder(self, styles: StyleChain) -> MathRunFrameBuilder { pub fn multiline_frame_builder(
self,
styles: StyleChain,
sizings: Option<EquationSizings>,
) -> MathRunFrameBuilder {
let rows: Vec<_> = self.rows(); let rows: Vec<_> = self.rows();
let row_count = rows.len(); let row_count = rows.len();
let column_gap = EquationElem::column_gap_in(styles).resolve(styles); let alignments = alignments(&rows, sizings);
let alignments = alignments(&rows, column_gap);
let leading = if EquationElem::size_in(styles) >= MathSize::Text { let leading = if EquationElem::size_in(styles) >= MathSize::Text {
ParElem::leading_in(styles) ParElem::leading_in(styles)
@ -215,15 +218,17 @@ impl MathRun {
size.y += leading; size.y += leading;
} }
let mut pos = Point::with_y(size.y); let mut pos = Point::new(alignments.padding.0, size.y);
if alignments.points.is_empty() { if alignments.points.is_empty() {
pos.x = align.position(alignments.width - sub.width()); pos.x += align.position(alignments.width - sub.width());
} }
size.x.set_max(sub.width()); size.x.set_max(sub.width() + alignments.padding.0);
size.y += sub.height(); size.y += sub.height();
frames.push((sub, pos)); frames.push((sub, pos));
} }
size.x += alignments.padding.1;
MathRunFrameBuilder { size, frames } MathRunFrameBuilder { size, frames }
} }

View File

@ -1,10 +1,12 @@
use ttf_parser::math::MathValue; use ttf_parser::math::MathValue;
use typst_library::foundations::{Style, StyleChain}; use typst_library::foundations::{Style, StyleChain};
use typst_library::layout::{Abs, Em, FixedAlignment, Frame, Point, Size, VAlignment}; use typst_library::layout::{
use typst_library::math::{EquationElem, MathSize}; Abs, Em, FixedAlignment, Fr, Frame, Point, Size, VAlignment,
};
use typst_library::math::{EquationElem, GapSizing, MathSize};
use typst_utils::LazyHash; use typst_utils::LazyHash;
use super::{LeftRightAlternator, MathContext, MathFragment, MathRun}; use super::{EquationSizings, LeftRightAlternator, MathContext, MathFragment, MathRun};
macro_rules! scaled { macro_rules! scaled {
($ctx:expr, $styles:expr, text: $text:ident, display: $display:ident $(,)?) => { ($ctx:expr, $styles:expr, text: $text:ident, display: $display:ident $(,)?) => {
@ -118,7 +120,7 @@ pub fn stack(
baseline: usize, baseline: usize,
alternator: LeftRightAlternator, alternator: LeftRightAlternator,
) -> Frame { ) -> Frame {
let AlignmentResult { points, width } = alignments(&rows, Abs::zero()); let AlignmentResult { points, width, .. } = alignments(&rows, None);
let rows: Vec<_> = rows let rows: Vec<_> = rows
.into_iter() .into_iter()
.map(|row| row.into_line_frame(&points, alternator)) .map(|row| row.into_line_frame(&points, alternator))
@ -148,8 +150,15 @@ pub fn stack(
frame frame
} }
/// Determine the positions of the alignment points, according to the input rows combined. pub struct AlignmentResult {
pub fn alignments(rows: &[MathRun], gap: Abs) -> AlignmentResult { pub points: Vec<Abs>,
pub width: Abs,
pub padding: (Abs, Abs),
}
/// Determine the positions of the alignment points, according to the input
/// rows combined.
pub fn alignments(rows: &[MathRun], sizings: Option<EquationSizings>) -> AlignmentResult {
let mut widths = Vec::<Abs>::new(); let mut widths = Vec::<Abs>::new();
let mut pending_width = Abs::zero(); let mut pending_width = Abs::zero();
@ -159,9 +168,6 @@ pub fn alignments(rows: &[MathRun], gap: Abs) -> AlignmentResult {
for fragment in row.iter() { for fragment in row.iter() {
if matches!(fragment, MathFragment::Align) { if matches!(fragment, MathFragment::Align) {
if alignment_index > 0 && alignment_index % 2 == 0 {
width += gap;
}
if alignment_index < widths.len() { if alignment_index < widths.len() {
widths[alignment_index].set_max(width); widths[alignment_index].set_max(width);
} else { } else {
@ -182,18 +188,80 @@ pub fn alignments(rows: &[MathRun], gap: Abs) -> AlignmentResult {
} }
} }
if widths.is_empty() {
widths.push(pending_width);
let padding = add_gaps(&mut widths, sizings);
return AlignmentResult { width: pending_width, points: vec![], padding };
}
let padding = add_gaps(&mut widths, sizings);
let mut points = widths; let mut points = widths;
for i in 1..points.len() { for i in 1..points.len() {
let prev = points[i - 1]; let prev = points[i - 1];
points[i] += prev; points[i] += prev;
} }
AlignmentResult { AlignmentResult {
width: points.last().copied().unwrap_or(pending_width), width: points.last().copied().unwrap(),
points, points,
padding,
} }
} }
pub struct AlignmentResult { /// Inserts gaps between columns given by the alignments.
pub points: Vec<Abs>, fn add_gaps(widths: &mut [Abs], sizings: Option<EquationSizings>) -> (Abs, Abs) {
pub width: Abs, let Some(sizings) = sizings else {
return (Abs::zero(), Abs::zero());
};
// Padding to be returned.
let mut padding = [Abs::zero(), Abs::zero()];
// Number of gaps between columns.
let len = widths.len();
let ngaps = len.div_ceil(2).saturating_sub(1);
// Discard excess gaps or repeat the last gap to match the number of gaps.
let mut gaps = sizings.gaps.to_vec();
gaps.truncate(ngaps);
if let Some(last_gap) = gaps.last().copied() {
gaps.extend(std::iter::repeat_n(last_gap, ngaps.saturating_sub(gaps.len())));
}
// Sum of fractions of all fractional gaps.
let mut fr = Fr::zero();
// Resolve the size of all relative gaps and compute the sum of all
// fractional gaps.
let region_width = sizings.region_size_x;
for (i, gap) in gaps.iter().enumerate() {
match gap {
GapSizing::Rel(v) => widths[1 + i * 2] += v.relative_to(region_width),
GapSizing::Fr(v) => fr += *v,
}
}
for (i, gap) in sizings.padding.iter().enumerate() {
match gap {
GapSizing::Rel(v) => padding[i] = v.relative_to(region_width),
GapSizing::Fr(v) => fr += *v,
}
}
// Size that is not used by fixed-size gaps.
let remaining = region_width - (widths.iter().sum::<Abs>() + padding.iter().sum());
// Distribute remaining space to fractional gaps.
if !remaining.approx_empty() {
for (i, gap) in gaps.iter().enumerate() {
if let GapSizing::Fr(v) = gap {
widths[1 + i * 2] += v.share(fr, remaining);
}
}
for (i, gap) in sizings.padding.iter().enumerate() {
if let GapSizing::Fr(v) = gap {
padding[i] = v.share(fr, remaining);
}
}
}
(padding[0], padding[1])
} }

View File

@ -1,25 +1,24 @@
use std::num::NonZeroUsize; use std::num::NonZeroUsize;
use typst_utils::NonZeroExt; use smallvec::{smallvec, SmallVec};
use typst_utils::{NonZeroExt, Numeric};
use unicode_math_class::MathClass; use unicode_math_class::MathClass;
use crate::diag::SourceResult; use crate::diag::{bail, HintedStrResult, SourceResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{
elem, Content, NativeElement, Packed, Show, ShowSet, Smart, StyleChain, Styles, cast, elem, Array, Content, NativeElement, Packed, Resolve, Show, ShowSet, Smart,
Synthesize, StyleChain, Styles, Synthesize, Value,
}; };
use crate::introspection::{Count, Counter, CounterUpdate, Locatable}; use crate::introspection::{Count, Counter, CounterUpdate, Locatable};
use crate::layout::{ use crate::layout::{
AlignElem, Alignment, BlockElem, Em, InlineElem, Length, OuterHAlignment, Abs, AlignElem, Alignment, BlockElem, Fr, InlineElem, Length, OuterHAlignment, Rel,
SpecificAlignment, VAlignment, Spacing, SpecificAlignment, VAlignment,
}; };
use crate::math::{MathSize, MathVariant}; use crate::math::{MathSize, MathVariant};
use crate::model::{Numbering, Outlinable, ParLine, Refable, Supplement}; use crate::model::{Numbering, Outlinable, ParLine, Refable, Supplement};
use crate::text::{FontFamily, FontList, FontWeight, LocalName, TextElem}; use crate::text::{FontFamily, FontList, FontWeight, LocalName, TextElem};
const DEFAULT_COL_GAP: Em = Em::new(1.0);
/// A mathematical equation. /// A mathematical equation.
/// ///
/// Can be displayed inline with text or as a separate block. An equation /// Can be displayed inline with text or as a separate block. An equation
@ -111,8 +110,14 @@ pub struct EquationElem {
/// 0 &= 0 & &"no" \ /// 0 &= 0 & &"no" \
/// 1+1 &= 2 & &"maybe" $ /// 1+1 &= 2 & &"maybe" $
/// ``` /// ```
#[default(DEFAULT_COL_GAP.into())] #[default(Fr::one().into())]
pub column_gap: Length, #[borrowed]
pub column_gap: GapSizings,
///
#[default(Fr::one().into())]
#[borrowed]
pub column_padding: PaddingSizings,
/// The contents of the equation. /// The contents of the equation.
#[required] #[required]
@ -203,7 +208,6 @@ impl ShowSet for Packed<EquationElem> {
out.set(BlockElem::set_breakable(false)); out.set(BlockElem::set_breakable(false));
out.set(ParLine::set_numbering(None)); out.set(ParLine::set_numbering(None));
out.set(EquationElem::set_size(MathSize::Display)); out.set(EquationElem::set_size(MathSize::Display));
out.set(EquationElem::set_column_gap(self.column_gap(styles)));
} else { } else {
out.set(EquationElem::set_size(MathSize::Text)); out.set(EquationElem::set_size(MathSize::Text));
} }
@ -262,3 +266,101 @@ impl Outlinable for Packed<EquationElem> {
Content::empty() Content::empty()
} }
} }
/// Gap sizing definitions.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct GapSizings<T: Numeric = Length>(pub SmallVec<[GapSizing<T>; 1]>);
impl<T: Into<Spacing>> From<T> for GapSizings {
fn from(spacing: T) -> Self {
Self(smallvec![GapSizing::from(spacing)])
}
}
impl Resolve for &GapSizings {
type Output = GapSizings<Abs>;
fn resolve(self, styles: StyleChain) -> Self::Output {
Self::Output {
0: self.0.iter().map(|v| v.resolve(styles)).collect(),
}
}
}
cast! {
GapSizings,
self => self.0.into_value(),
v: GapSizing => Self(smallvec![v]),
v: Array => Self(v.into_iter().map(Value::cast).collect::<HintedStrResult<_>>()?),
}
/// Padding sizing definitions.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct PaddingSizings<T: Numeric = Length>(pub SmallVec<[GapSizing<T>; 2]>);
impl<T: Into<Spacing>> From<T> for PaddingSizings {
fn from(spacing: T) -> Self {
let spacing = spacing.into();
Self(smallvec![GapSizing::from(spacing), GapSizing::from(spacing)])
}
}
impl Resolve for &PaddingSizings {
type Output = PaddingSizings<Abs>;
fn resolve(self, styles: StyleChain) -> Self::Output {
Self::Output {
0: self.0.iter().map(|v| v.resolve(styles)).collect(),
}
}
}
cast! {
PaddingSizings,
self => self.0.into_value(),
v: GapSizing => Self(smallvec![v, v]),
v: Array => match v.as_slice() {
[start, end] => Self(smallvec![start.clone().cast()?, end.clone().cast()?]),
_ => bail!("expected 2 sizings, found {}", v.len()),
},
}
/// Defines how to size a gap along an axis.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum GapSizing<T: Numeric = Length> {
/// A size specified in absolute terms and relative to the parent's size.
Rel(Rel<T>),
/// A size specified as a fraction of the remaining free space in the
/// parent.
Fr(Fr),
}
impl Resolve for GapSizing {
type Output = GapSizing<Abs>;
fn resolve(self, styles: StyleChain) -> Self::Output {
match self {
Self::Rel(rel) => Self::Output::Rel(rel.resolve(styles)),
Self::Fr(fr) => Self::Output::Fr(fr),
}
}
}
impl<T: Into<Spacing>> From<T> for GapSizing {
fn from(spacing: T) -> Self {
match spacing.into() {
Spacing::Rel(rel) => Self::Rel(rel),
Spacing::Fr(fr) => Self::Fr(fr),
}
}
}
cast! {
GapSizing,
self => match self {
Self::Rel(rel) => rel.into_value(),
Self::Fr(fr) => fr.into_value(),
},
v: Rel<Length> => Self::Rel(v),
v: Fr => Self::Fr(v),
}