diff options
| author | Laurenz <laurmaedje@gmail.com> | 2023-11-23 16:25:49 +0100 |
|---|---|---|
| committer | Laurenz <laurmaedje@gmail.com> | 2023-11-24 12:30:02 +0100 |
| commit | 7eebafa7837ec173a7b2064ae60fd45b5413d17c (patch) | |
| tree | b63b302b6d7747bcbb28571713745b9ca1aa83a4 /crates/typst-library/src/math | |
| parent | 76e173b78b511b506b928c27ac360f75fa455747 (diff) | |
Merge `typst` and `typst-library`
Diffstat (limited to 'crates/typst-library/src/math')
| -rw-r--r-- | crates/typst-library/src/math/accent.rs | 137 | ||||
| -rw-r--r-- | crates/typst-library/src/math/align.rs | 60 | ||||
| -rw-r--r-- | crates/typst-library/src/math/attach.rs | 440 | ||||
| -rw-r--r-- | crates/typst-library/src/math/cancel.rs | 230 | ||||
| -rw-r--r-- | crates/typst-library/src/math/class.rs | 38 | ||||
| -rw-r--r-- | crates/typst-library/src/math/ctx.rs | 335 | ||||
| -rw-r--r-- | crates/typst-library/src/math/frac.rs | 157 | ||||
| -rw-r--r-- | crates/typst-library/src/math/fragment.rs | 513 | ||||
| -rw-r--r-- | crates/typst-library/src/math/lr.rs | 195 | ||||
| -rw-r--r-- | crates/typst-library/src/math/matrix.rs | 655 | ||||
| -rw-r--r-- | crates/typst-library/src/math/mod.rs | 500 | ||||
| -rw-r--r-- | crates/typst-library/src/math/op.rs | 115 | ||||
| -rw-r--r-- | crates/typst-library/src/math/root.rs | 137 | ||||
| -rw-r--r-- | crates/typst-library/src/math/row.rs | 261 | ||||
| -rw-r--r-- | crates/typst-library/src/math/spacing.rs | 63 | ||||
| -rw-r--r-- | crates/typst-library/src/math/stretch.rs | 199 | ||||
| -rw-r--r-- | crates/typst-library/src/math/style.rs | 574 | ||||
| -rw-r--r-- | crates/typst-library/src/math/underover.rs | 315 |
18 files changed, 0 insertions, 4924 deletions
diff --git a/crates/typst-library/src/math/accent.rs b/crates/typst-library/src/math/accent.rs deleted file mode 100644 index 1b2d4793..00000000 --- a/crates/typst-library/src/math/accent.rs +++ /dev/null @@ -1,137 +0,0 @@ -use crate::math::*; - -/// How much the accent can be shorter than the base. -const ACCENT_SHORT_FALL: Em = Em::new(0.5); - -/// Attaches an accent to a base. -/// -/// # Example -/// ```example -/// $grave(a) = accent(a, `)$ \ -/// $arrow(a) = accent(a, arrow)$ \ -/// $tilde(a) = accent(a, \u{0303})$ -/// ``` -#[elem(LayoutMath)] -pub struct AccentElem { - /// The base to which the accent is applied. - /// May consist of multiple letters. - /// - /// ```example - /// $arrow(A B C)$ - /// ``` - #[required] - pub base: Content, - - /// The accent to apply to the base. - /// - /// Supported accents include: - /// - /// | Accent | Name | Codepoint | - /// | ------------- | --------------- | --------- | - /// | Grave | `grave` | <code>`</code> | - /// | Acute | `acute` | `´` | - /// | Circumflex | `hat` | `^` | - /// | Tilde | `tilde` | `~` | - /// | Macron | `macron` | `¯` | - /// | Breve | `breve` | `˘` | - /// | Dot | `dot` | `.` | - /// | Double dot | `dot.double` | `¨` | - /// | Triple dot | `dot.triple` | <code>⃛</code> | - /// | Quadruple dot | `dot.quad` | <code>⃜</code> | - /// | Diaeresis | `diaer` | `¨` | - /// | Circle | `circle` | `∘` | - /// | Double acute | `acute.double` | `˝` | - /// | Caron | `caron` | `ˇ` | - /// | Right arrow | `arrow`, `->` | `→` | - /// | Left arrow | `arrow.l`, `<-` | `←` | - #[required] - pub accent: Accent, -} - -impl LayoutMath for AccentElem { - #[tracing::instrument(skip(ctx))] - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - ctx.style(ctx.style.with_cramped(true)); - let base = ctx.layout_fragment(self.base())?; - ctx.unstyle(); - - // Preserve class to preserve automatic spacing. - let base_class = base.class().unwrap_or(MathClass::Normal); - let base_attach = match &base { - MathFragment::Glyph(base) => { - attachment(ctx, base.id, base.italics_correction) - } - _ => (base.width() + base.italics_correction()) / 2.0, - }; - - // Forcing the accent to be at least as large as the base makes it too - // wide in many case. - let Accent(c) = self.accent(); - let glyph = GlyphFragment::new(ctx, *c, self.span()); - let short_fall = ACCENT_SHORT_FALL.scaled(ctx); - let variant = glyph.stretch_horizontal(ctx, base.width(), short_fall); - let accent = variant.frame; - let accent_attach = match variant.id { - Some(id) => attachment(ctx, id, variant.italics_correction), - None => accent.width() / 2.0, - }; - - // Descent is negative because the accent's ink bottom is above the - // baseline. Therefore, the default gap is the accent's negated descent - // minus the accent base height. Only if the base is very small, we need - // a larger gap so that the accent doesn't move too low. - let accent_base_height = scaled!(ctx, accent_base_height); - let gap = -accent.descent() - base.height().min(accent_base_height); - let size = Size::new(base.width(), accent.height() + gap + base.height()); - let accent_pos = Point::with_x(base_attach - accent_attach); - let base_pos = Point::with_y(accent.height() + gap); - let base_ascent = base.ascent(); - let baseline = base_pos.y + base.ascent(); - - let mut frame = Frame::soft(size); - frame.set_baseline(baseline); - frame.push_frame(accent_pos, accent); - frame.push_frame(base_pos, base.into_frame()); - ctx.push( - FrameFragment::new(ctx, frame) - .with_class(base_class) - .with_base_ascent(base_ascent), - ); - - Ok(()) - } -} - -/// The horizontal attachment position for the given glyph. -fn attachment(ctx: &MathContext, id: GlyphId, italics_correction: Abs) -> Abs { - ctx.table - .glyph_info - .and_then(|info| info.top_accent_attachments) - .and_then(|attachments| attachments.get(id)) - .map(|record| record.value.scaled(ctx)) - .unwrap_or_else(|| { - let advance = ctx.ttf.glyph_hor_advance(id).unwrap_or_default(); - (advance.scaled(ctx) + italics_correction) / 2.0 - }) -} - -/// An accent character. -#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] -pub struct Accent(char); - -impl Accent { - /// Normalize a character into an accent. - pub fn new(c: char) -> Self { - Self(Symbol::combining_accent(c).unwrap_or(c)) - } -} - -cast! { - Accent, - self => self.0.into_value(), - v: char => Self::new(v), - v: Content => match v.to::<TextElem>() { - Some(elem) => Value::Str(elem.text().clone().into()).cast()?, - None => bail!("expected text"), - }, -} diff --git a/crates/typst-library/src/math/align.rs b/crates/typst-library/src/math/align.rs deleted file mode 100644 index 4192e97b..00000000 --- a/crates/typst-library/src/math/align.rs +++ /dev/null @@ -1,60 +0,0 @@ -use crate::math::*; - -/// A math alignment point: `&`, `&&`. -#[elem(title = "Alignment Point", LayoutMath)] -pub struct AlignPointElem {} - -impl LayoutMath for AlignPointElem { - #[tracing::instrument(skip(ctx))] - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - ctx.push(MathFragment::Align); - Ok(()) - } -} - -pub(super) struct AlignmentResult { - pub points: Vec<Abs>, - pub width: Abs, -} - -/// Determine the position of the alignment points. -pub(super) fn alignments(rows: &[MathRow]) -> AlignmentResult { - let mut widths = Vec::<Abs>::new(); - - let mut pending_width = Abs::zero(); - for row in rows { - let mut width = Abs::zero(); - let mut alignment_index = 0; - - for fragment in row.iter() { - if matches!(fragment, MathFragment::Align) { - if alignment_index < widths.len() { - widths[alignment_index].set_max(width); - } else { - widths.push(width.max(pending_width)); - } - width = Abs::zero(); - alignment_index += 1; - } else { - width += fragment.width(); - } - } - if widths.is_empty() { - pending_width.set_max(width); - } else if alignment_index < widths.len() { - widths[alignment_index].set_max(width); - } else { - widths.push(width.max(pending_width)); - } - } - - let mut points = widths; - for i in 1..points.len() { - let prev = points[i - 1]; - points[i] += prev; - } - AlignmentResult { - width: points.last().copied().unwrap_or(pending_width), - points, - } -} diff --git a/crates/typst-library/src/math/attach.rs b/crates/typst-library/src/math/attach.rs deleted file mode 100644 index 3e6b69f2..00000000 --- a/crates/typst-library/src/math/attach.rs +++ /dev/null @@ -1,440 +0,0 @@ -use super::*; - -/// A base with optional attachments. -/// -/// ```example -/// $ attach( -/// Pi, t: alpha, b: beta, -/// tl: 1, tr: 2+3, bl: 4+5, br: 6, -/// ) $ -/// ``` -#[elem(LayoutMath)] -pub struct AttachElem { - /// The base to which things are attached. - #[required] - pub base: Content, - - /// The top attachment, smartly positioned at top-right or above the base. - /// - /// You can wrap the base in `{limits()}` or `{scripts()}` to override the - /// smart positioning. - pub t: Option<Content>, - - /// The bottom attachment, smartly positioned at the bottom-right or below - /// the base. - /// - /// You can wrap the base in `{limits()}` or `{scripts()}` to override the - /// smart positioning. - pub b: Option<Content>, - - /// The top-left attachment (before the base). - pub tl: Option<Content>, - - /// The bottom-left attachment (before base). - pub bl: Option<Content>, - - /// The top-right attachment (after the base). - pub tr: Option<Content>, - - /// The bottom-right attachment (after the base). - pub br: Option<Content>, -} - -impl LayoutMath for AttachElem { - #[tracing::instrument(skip(ctx))] - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - type GetAttachment = fn(&AttachElem, styles: StyleChain) -> Option<Content>; - let layout_attachment = |ctx: &mut MathContext, getter: GetAttachment| { - getter(self, ctx.styles()) - .map(|elem| ctx.layout_fragment(&elem)) - .transpose() - }; - - let base = ctx.layout_fragment(self.base())?; - - ctx.style(ctx.style.for_superscript()); - let tl = layout_attachment(ctx, Self::tl)?; - let tr = layout_attachment(ctx, Self::tr)?; - let t = layout_attachment(ctx, Self::t)?; - ctx.unstyle(); - - ctx.style(ctx.style.for_subscript()); - let bl = layout_attachment(ctx, Self::bl)?; - let br = layout_attachment(ctx, Self::br)?; - let b = layout_attachment(ctx, Self::b)?; - ctx.unstyle(); - - let limits = base.limits().active(ctx); - let (t, tr) = if limits || tr.is_some() { (t, tr) } else { (None, t) }; - let (b, br) = if limits || br.is_some() { (b, br) } else { (None, b) }; - layout_attachments(ctx, base, [tl, t, tr, bl, b, br]) - } -} - -/// Grouped primes. -/// -/// ```example -/// $ a'''_b = a^'''_b $ -/// ``` -/// -/// # Syntax -/// This function has dedicated syntax: use apostrophes instead of primes. They -/// will automatically attach to the previous element, moving superscripts to -/// the next level. -#[elem(LayoutMath)] -pub struct PrimesElem { - /// The number of grouped primes. - #[required] - pub count: usize, -} - -impl LayoutMath for PrimesElem { - #[tracing::instrument(skip(ctx))] - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - match *self.count() { - count @ 1..=4 => { - let f = ctx.layout_fragment(&TextElem::packed(match count { - 1 => '′', - 2 => '″', - 3 => '‴', - 4 => '⁗', - _ => unreachable!(), - }))?; - ctx.push(f); - } - count => { - // Custom amount of primes - let prime = ctx.layout_fragment(&TextElem::packed('′'))?.into_frame(); - let width = prime.width() * (count + 1) as f64 / 2.0; - let mut frame = Frame::soft(Size::new(width, prime.height())); - frame.set_baseline(prime.ascent()); - - for i in 0..count { - frame.push_frame( - Point::new(prime.width() * (i as f64 / 2.0), Abs::zero()), - prime.clone(), - ) - } - ctx.push(FrameFragment::new(ctx, frame)); - } - } - Ok(()) - } -} - -/// Forces a base to display attachments as scripts. -/// -/// ```example -/// $ scripts(sum)_1^2 != sum_1^2 $ -/// ``` -#[elem(LayoutMath)] -pub struct ScriptsElem { - /// The base to attach the scripts to. - #[required] - pub body: Content, -} - -impl LayoutMath for ScriptsElem { - #[tracing::instrument(skip(ctx))] - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - let mut fragment = ctx.layout_fragment(self.body())?; - fragment.set_limits(Limits::Never); - ctx.push(fragment); - Ok(()) - } -} - -/// Forces a base to display attachments as limits. -/// -/// ```example -/// $ limits(A)_1^2 != A_1^2 $ -/// ``` -#[elem(LayoutMath)] -pub struct LimitsElem { - /// The base to attach the limits to. - #[required] - pub body: Content, - - /// Whether to also force limits in inline equations. - /// - /// When applying limits globally (e.g., through a show rule), it is - /// typically a good idea to disable this. - #[default(true)] - pub inline: bool, -} - -impl LayoutMath for LimitsElem { - #[tracing::instrument(skip(ctx))] - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - let mut fragment = ctx.layout_fragment(self.body())?; - fragment.set_limits(if self.inline(ctx.styles()) { - Limits::Always - } else { - Limits::Display - }); - ctx.push(fragment); - Ok(()) - } -} - -/// Describes in which situation a frame should use limits for attachments. -#[derive(Debug, Copy, Clone)] -pub enum Limits { - /// Always scripts. - Never, - /// Display limits only in `display` math. - Display, - /// Always limits. - Always, -} - -impl Limits { - /// The default limit configuration if the given character is the base. - pub fn for_char(c: char) -> Self { - match unicode_math_class::class(c) { - Some(MathClass::Large) => { - if is_integral_char(c) { - Limits::Never - } else { - Limits::Display - } - } - Some(MathClass::Relation) => Limits::Always, - _ => Limits::Never, - } - } - - /// Whether limits should be displayed in this context - pub fn active(&self, ctx: &MathContext) -> bool { - match self { - Self::Always => true, - Self::Display => ctx.style.size == MathSize::Display, - Self::Never => false, - } - } -} - -macro_rules! measure { - ($e: ident, $attr: ident) => { - $e.as_ref().map(|e| e.$attr()).unwrap_or_default() - }; -} - -/// Layout the attachments. -fn layout_attachments( - ctx: &mut MathContext, - base: MathFragment, - [tl, t, tr, bl, b, br]: [Option<MathFragment>; 6], -) -> SourceResult<()> { - let (shift_up, shift_down) = - compute_shifts_up_and_down(ctx, &base, [&tl, &tr, &bl, &br]); - - let sup_delta = Abs::zero(); - let sub_delta = -base.italics_correction(); - let (base_width, base_ascent, base_descent) = - (base.width(), base.ascent(), base.descent()); - let base_class = base.class().unwrap_or(MathClass::Normal); - - let ascent = base_ascent - .max(shift_up + measure!(tr, ascent)) - .max(shift_up + measure!(tl, ascent)) - .max(shift_up + measure!(t, height)); - - let descent = base_descent - .max(shift_down + measure!(br, descent)) - .max(shift_down + measure!(bl, descent)) - .max(shift_down + measure!(b, height)); - - let pre_sup_width = measure!(tl, width); - let pre_sub_width = measure!(bl, width); - let pre_width_dif = pre_sup_width - pre_sub_width; // Could be negative. - let pre_width_max = pre_sup_width.max(pre_sub_width); - let post_max_width = - (sup_delta + measure!(tr, width)).max(sub_delta + measure!(br, width)); - - let (center_frame, base_offset) = attach_top_and_bottom(ctx, base, t, b); - let base_pos = - Point::new(sup_delta + pre_width_max, ascent - base_ascent - base_offset); - if [&tl, &bl, &tr, &br].iter().all(|&e| e.is_none()) { - ctx.push(FrameFragment::new(ctx, center_frame).with_class(base_class)); - return Ok(()); - } - - let mut frame = Frame::soft(Size::new( - pre_width_max + base_width + post_max_width + scaled!(ctx, space_after_script), - ascent + descent, - )); - frame.set_baseline(ascent); - frame.push_frame(base_pos, center_frame); - - if let Some(tl) = tl { - let pos = - Point::new(-pre_width_dif.min(Abs::zero()), ascent - shift_up - tl.ascent()); - frame.push_frame(pos, tl.into_frame()); - } - - if let Some(bl) = bl { - let pos = - Point::new(pre_width_dif.max(Abs::zero()), ascent + shift_down - bl.ascent()); - frame.push_frame(pos, bl.into_frame()); - } - - if let Some(tr) = tr { - let pos = Point::new( - sup_delta + pre_width_max + base_width, - ascent - shift_up - tr.ascent(), - ); - frame.push_frame(pos, tr.into_frame()); - } - - if let Some(br) = br { - let pos = Point::new( - sub_delta + pre_width_max + base_width, - ascent + shift_down - br.ascent(), - ); - frame.push_frame(pos, br.into_frame()); - } - - ctx.push(FrameFragment::new(ctx, frame).with_class(base_class)); - - Ok(()) -} - -fn attach_top_and_bottom( - ctx: &mut MathContext, - base: MathFragment, - t: Option<MathFragment>, - b: Option<MathFragment>, -) -> (Frame, Abs) { - let upper_gap_min = scaled!(ctx, upper_limit_gap_min); - let upper_rise_min = scaled!(ctx, upper_limit_baseline_rise_min); - let lower_gap_min = scaled!(ctx, lower_limit_gap_min); - let lower_drop_min = scaled!(ctx, lower_limit_baseline_drop_min); - - let mut base_offset = Abs::zero(); - let mut width = base.width(); - let mut height = base.height(); - - if let Some(t) = &t { - let top_gap = upper_gap_min.max(upper_rise_min - t.descent()); - width.set_max(t.width()); - height += t.height() + top_gap; - base_offset = top_gap + t.height(); - } - - if let Some(b) = &b { - let bottom_gap = lower_gap_min.max(lower_drop_min - b.ascent()); - width.set_max(b.width()); - height += b.height() + bottom_gap; - } - - let base_pos = Point::new((width - base.width()) / 2.0, base_offset); - let delta = base.italics_correction() / 2.0; - - let mut frame = Frame::soft(Size::new(width, height)); - frame.set_baseline(base_pos.y + base.ascent()); - frame.push_frame(base_pos, base.into_frame()); - - if let Some(t) = t { - let top_pos = Point::with_x((width - t.width()) / 2.0 + delta); - frame.push_frame(top_pos, t.into_frame()); - } - - if let Some(b) = b { - let bottom_pos = - Point::new((width - b.width()) / 2.0 - delta, height - b.height()); - frame.push_frame(bottom_pos, b.into_frame()); - } - - (frame, base_offset) -} - -fn compute_shifts_up_and_down( - ctx: &MathContext, - base: &MathFragment, - [tl, tr, bl, br]: [&Option<MathFragment>; 4], -) -> (Abs, Abs) { - let sup_shift_up = if ctx.style.cramped { - scaled!(ctx, superscript_shift_up_cramped) - } else { - scaled!(ctx, superscript_shift_up) - }; - - let sup_bottom_min = scaled!(ctx, superscript_bottom_min); - let sup_bottom_max_with_sub = scaled!(ctx, superscript_bottom_max_with_subscript); - let sup_drop_max = scaled!(ctx, superscript_baseline_drop_max); - let gap_min = scaled!(ctx, sub_superscript_gap_min); - let sub_shift_down = scaled!(ctx, subscript_shift_down); - let sub_top_max = scaled!(ctx, subscript_top_max); - let sub_drop_min = scaled!(ctx, subscript_baseline_drop_min); - - let mut shift_up = Abs::zero(); - let mut shift_down = Abs::zero(); - let is_char_box = is_character_box(base); - - if tl.is_some() || tr.is_some() { - let ascent = match &base { - MathFragment::Frame(frame) => frame.base_ascent, - _ => base.ascent(), - }; - shift_up = shift_up - .max(sup_shift_up) - .max(if is_char_box { Abs::zero() } else { ascent - sup_drop_max }) - .max(sup_bottom_min + measure!(tl, descent)) - .max(sup_bottom_min + measure!(tr, descent)); - } - - if bl.is_some() || br.is_some() { - shift_down = shift_down - .max(sub_shift_down) - .max(if is_char_box { Abs::zero() } else { base.descent() + sub_drop_min }) - .max(measure!(bl, ascent) - sub_top_max) - .max(measure!(br, ascent) - sub_top_max); - } - - for (sup, sub) in [(tl, bl), (tr, br)] { - if let (Some(sup), Some(sub)) = (&sup, &sub) { - let sup_bottom = shift_up - sup.descent(); - let sub_top = sub.ascent() - shift_down; - let gap = sup_bottom - sub_top; - if gap >= gap_min { - continue; - } - - let increase = gap_min - gap; - let sup_only = - (sup_bottom_max_with_sub - sup_bottom).clamp(Abs::zero(), increase); - let rest = (increase - sup_only) / 2.0; - shift_up += sup_only + rest; - shift_down += rest; - } - } - - (shift_up, shift_down) -} - -/// Determines if the character is one of a variety of integral signs -fn is_integral_char(c: char) -> bool { - ('∫'..='∳').contains(&c) || ('⨋'..='⨜').contains(&c) -} - -/// Whether the fragment consists of a single character or atomic piece of text. -fn is_character_box(fragment: &MathFragment) -> bool { - match fragment { - MathFragment::Glyph(_) | MathFragment::Variant(_) => { - fragment.class() != Some(MathClass::Large) - } - MathFragment::Frame(fragment) => is_atomic_text_frame(&fragment.frame), - _ => false, - } -} - -/// Handles e.g. "sin", "log", "exp", "CustomOperator". -fn is_atomic_text_frame(frame: &Frame) -> bool { - // Meta information isn't visible or renderable, so we exclude it. - let mut iter = frame - .items() - .map(|(_, item)| item) - .filter(|item| !matches!(item, FrameItem::Meta(_, _))); - matches!(iter.next(), Some(FrameItem::Text(_))) && iter.next().is_none() -} diff --git a/crates/typst-library/src/math/cancel.rs b/crates/typst-library/src/math/cancel.rs deleted file mode 100644 index 455750f7..00000000 --- a/crates/typst-library/src/math/cancel.rs +++ /dev/null @@ -1,230 +0,0 @@ -use super::*; - -/// Displays a diagonal line over a part of an equation. -/// -/// This is commonly used to show the elimination of a term. -/// -/// # Example -/// ```example -/// >>> #set page(width: 140pt) -/// Here, we can simplify: -/// $ (a dot b dot cancel(x)) / -/// cancel(x) $ -/// ``` -#[elem(LayoutMath)] -pub struct CancelElem { - /// The content over which the line should be placed. - #[required] - pub body: Content, - - /// The length of the line, relative to the length of the diagonal spanning - /// the whole element being "cancelled". A value of `{100%}` would then have - /// the line span precisely the element's diagonal. - /// - /// ```example - /// >>> #set page(width: 140pt) - /// $ a + cancel(x, length: #200%) - /// - cancel(x, length: #200%) $ - /// ``` - #[default(Rel::new(Ratio::one(), Abs::pt(3.0).into()))] - pub length: Rel<Length>, - - /// Whether the cancel line should be inverted (flipped along the y-axis). - /// For the default angle setting, inverted means the cancel line - /// points to the top left instead of top right. - /// - /// ```example - /// >>> #set page(width: 140pt) - /// $ (a cancel((b + c), inverted: #true)) / - /// cancel(b + c, inverted: #true) $ - /// ``` - #[default(false)] - pub inverted: bool, - - /// Whether two opposing cancel lines should be drawn, forming a cross over - /// the element. Overrides `inverted`. - /// - /// ```example - /// >>> #set page(width: 140pt) - /// $ cancel(Pi, cross: #true) $ - /// ``` - #[default(false)] - pub cross: bool, - - /// How much to rotate the cancel line. - /// - /// - If `{auto}`, the line assumes the default angle; that is, along the - /// diagonal line of the content box. - /// - If given an angle, the line is rotated by that angle clockwise w.r.t - /// the y-axis. - /// - If given a function `angle => angle`, the line is rotated by the angle - /// returned by that function. The function receives the default angle as - /// its input. - /// - /// ```example - /// >>> #set page(width: 140pt) - /// $ cancel(Pi) - /// cancel(Pi, angle: #0deg) - /// cancel(Pi, angle: #45deg) - /// cancel(Pi, angle: #90deg) - /// cancel(1/(1+x), angle: #(a => a + 45deg)) - /// cancel(1/(1+x), angle: #(a => a + 90deg)) $ - /// ``` - pub angle: Smart<CancelAngle>, - - /// How to [stroke]($stroke) the cancel line. - /// - /// ```example - /// >>> #set page(width: 140pt) - /// $ cancel( - /// sum x, - /// stroke: #( - /// paint: red, - /// thickness: 1.5pt, - /// dash: "dashed", - /// ), - /// ) $ - /// ``` - #[resolve] - #[fold] - #[default(Stroke { - // Default stroke has 0.5pt for better visuals. - thickness: Smart::Custom(Abs::pt(0.5)), - ..Default::default() - })] - pub stroke: Stroke, -} - -impl LayoutMath for CancelElem { - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - let body = ctx.layout_fragment(self.body())?; - // Use the same math class as the body, in order to preserve automatic spacing around it. - let body_class = body.class().unwrap_or(MathClass::Special); - let mut body = body.into_frame(); - - let styles = ctx.styles(); - let body_size = body.size(); - let span = self.span(); - let length = self.length(styles).resolve(styles); - - let stroke = self.stroke(styles).unwrap_or(FixedStroke { - paint: TextElem::fill_in(styles).as_decoration(), - ..Default::default() - }); - - let invert = self.inverted(styles); - let cross = self.cross(styles); - let angle = self.angle(styles); - - let invert_first_line = !cross && invert; - let first_line = draw_cancel_line( - ctx, - length, - stroke.clone(), - invert_first_line, - &angle, - body_size, - span, - )?; - - // The origin of our line is the very middle of the element. - let center = body_size.to_point() / 2.0; - body.push_frame(center, first_line); - - if cross { - // Draw the second line. - let second_line = - draw_cancel_line(ctx, length, stroke, true, &angle, body_size, span)?; - - body.push_frame(center, second_line); - } - - ctx.push(FrameFragment::new(ctx, body).with_class(body_class)); - - Ok(()) - } -} - -/// Defines the cancel line. -#[derive(Debug, Clone, PartialEq, Hash)] -pub enum CancelAngle { - Angle(Angle), - Func(Func), -} - -cast! { - CancelAngle, - self => match self { - Self::Angle(v) => v.into_value(), - Self::Func(v) => v.into_value() - }, - v: Angle => CancelAngle::Angle(v), - v: Func => CancelAngle::Func(v), -} - -/// Draws a cancel line. -fn draw_cancel_line( - ctx: &mut MathContext, - length_scale: Rel<Abs>, - stroke: FixedStroke, - invert: bool, - angle: &Smart<CancelAngle>, - body_size: Size, - span: Span, -) -> SourceResult<Frame> { - let default = default_angle(body_size); - let mut angle = match angle { - // Non specified angle defaults to the diagonal - Smart::Auto => default, - Smart::Custom(angle) => match angle { - // This specifies the absolute angle w.r.t y-axis clockwise. - CancelAngle::Angle(v) => *v, - // This specifies a function that takes the default angle as input. - CancelAngle::Func(func) => { - func.call_vt(ctx.vt, [default])?.cast().at(span)? - } - }, - }; - - // invert means flipping along the y-axis - if invert { - angle *= -1.0; - } - - // same as above, the default length is the diagonal of the body box. - let default_length = body_size.to_point().hypot(); - let length = length_scale.relative_to(default_length); - - // Draw a vertical line of length and rotate it by angle - let start = Point::new(Abs::zero(), length / 2.0); - let delta = Point::new(Abs::zero(), -length); - - let mut frame = Frame::soft(body_size); - frame.push(start, FrameItem::Shape(Geometry::Line(delta).stroked(stroke), span)); - - // Having the middle of the line at the origin is convenient here. - frame.transform(Transform::rotate(angle)); - Ok(frame) -} - -/// The default line angle for a body of the given size. -fn default_angle(body: Size) -> Angle { - // The default cancel line is the diagonal. - // We infer the default angle from - // the diagonal w.r.t to the body box. - // - // The returned angle is in the range of [0, Pi/2] - // - // Note that the angle is computed w.r.t to the y-axis - // - // B - // /| - // diagonal / | height - // / | - // / | - // O ---- - // width - let (width, height) = (body.x, body.y); - let default_angle = (width / height).atan(); // arctangent (in the range [0, Pi/2]) - Angle::rad(default_angle) -} diff --git a/crates/typst-library/src/math/class.rs b/crates/typst-library/src/math/class.rs deleted file mode 100644 index d2c5192d..00000000 --- a/crates/typst-library/src/math/class.rs +++ /dev/null @@ -1,38 +0,0 @@ -use super::*; - -/// Forced use of a certain math class. -/// -/// This is useful to treat certain symbols as if they were of a different -/// class, e.g. to make a symbol behave like a relation. -/// -/// # Example -/// ```example -/// #let loves = math.class( -/// "relation", -/// sym.suit.heart, -/// ) -/// -/// $x loves y and y loves 5$ -/// ``` -#[elem(LayoutMath)] -pub struct ClassElem { - /// The class to apply to the content. - #[required] - pub class: MathClass, - - /// The content to which the class is applied. - #[required] - pub body: Content, -} - -impl LayoutMath for ClassElem { - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - ctx.style(ctx.style.with_class(*self.class())); - let mut fragment = ctx.layout_fragment(self.body())?; - ctx.unstyle(); - - fragment.set_class(*self.class()); - ctx.push(fragment); - Ok(()) - } -} diff --git a/crates/typst-library/src/math/ctx.rs b/crates/typst-library/src/math/ctx.rs deleted file mode 100644 index 789bd332..00000000 --- a/crates/typst-library/src/math/ctx.rs +++ /dev/null @@ -1,335 +0,0 @@ -use comemo::Prehashed; -use ttf_parser::gsub::SubstitutionSubtable; -use ttf_parser::math::MathValue; -use typst::font::{FontStyle, FontWeight}; -use typst::model::realize; -use typst::syntax::is_newline; -use unicode_segmentation::UnicodeSegmentation; - -use super::*; -use crate::text::{tags, BottomEdge, BottomEdgeMetric, TopEdge, TopEdgeMetric}; - -macro_rules! scaled { - ($ctx:expr, text: $text:ident, display: $display:ident $(,)?) => { - match $ctx.style.size { - MathSize::Display => scaled!($ctx, $display), - _ => scaled!($ctx, $text), - } - }; - ($ctx:expr, $name:ident) => { - $ctx.constants.$name().scaled($ctx) - }; -} - -macro_rules! percent { - ($ctx:expr, $name:ident) => { - $ctx.constants.$name() as f64 / 100.0 - }; -} - -/// The context for math layout. -pub struct MathContext<'a, 'b, 'v> { - pub vt: &'v mut Vt<'b>, - pub regions: Regions<'static>, - pub font: &'a Font, - pub ttf: &'a ttf_parser::Face<'a>, - pub table: ttf_parser::math::Table<'a>, - pub constants: ttf_parser::math::Constants<'a>, - pub ssty_table: Option<ttf_parser::gsub::AlternateSubstitution<'a>>, - pub glyphwise_tables: Option<Vec<GlyphwiseSubsts<'a>>>, - pub space_width: Em, - pub fragments: Vec<MathFragment>, - pub local: Styles, - pub style: MathStyle, - pub size: Abs, - outer: StyleChain<'a>, - style_stack: Vec<(MathStyle, Abs)>, -} - -impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> { - pub fn new( - vt: &'v mut Vt<'b>, - styles: StyleChain<'a>, - regions: Regions, - font: &'a Font, - block: bool, - ) -> Self { - let math_table = font.ttf().tables().math.unwrap(); - let gsub_table = font.ttf().tables().gsub; - let constants = math_table.constants.unwrap(); - - let ssty_table = gsub_table - .and_then(|gsub| { - gsub.features - .find(ttf_parser::Tag::from_bytes(b"ssty")) - .and_then(|feature| feature.lookup_indices.get(0)) - .and_then(|index| gsub.lookups.get(index)) - }) - .and_then(|ssty| ssty.subtables.get::<SubstitutionSubtable>(0)) - .and_then(|ssty| match ssty { - SubstitutionSubtable::Alternate(alt_glyphs) => Some(alt_glyphs), - _ => None, - }); - - let features = tags(styles); - let glyphwise_tables = gsub_table.map(|gsub| { - features - .into_iter() - .filter_map(|feature| GlyphwiseSubsts::new(gsub, feature)) - .collect() - }); - - let size = TextElem::size_in(styles); - let ttf = font.ttf(); - let space_width = ttf - .glyph_index(' ') - .and_then(|id| ttf.glyph_hor_advance(id)) - .map(|advance| font.to_em(advance)) - .unwrap_or(THICK); - - let variant = variant(styles); - Self { - vt, - regions: Regions::one(regions.base(), Axes::splat(false)), - font, - ttf: font.ttf(), - table: math_table, - constants, - ssty_table, - glyphwise_tables, - space_width, - fragments: vec![], - local: Styles::new(), - style: MathStyle { - variant: MathVariant::Serif, - size: if block { MathSize::Display } else { MathSize::Text }, - class: Smart::Auto, - cramped: false, - bold: variant.weight >= FontWeight::BOLD, - italic: match variant.style { - FontStyle::Normal => Smart::Auto, - FontStyle::Italic | FontStyle::Oblique => Smart::Custom(true), - }, - }, - size, - outer: styles, - style_stack: vec![], - } - } - - pub fn push(&mut self, fragment: impl Into<MathFragment>) { - self.fragments.push(fragment.into()); - } - - pub fn extend(&mut self, fragments: Vec<MathFragment>) { - self.fragments.extend(fragments); - } - - pub fn layout_fragment( - &mut self, - elem: &dyn LayoutMath, - ) -> SourceResult<MathFragment> { - let row = self.layout_fragments(elem)?; - Ok(MathRow::new(row).into_fragment(self)) - } - - pub fn layout_fragments( - &mut self, - elem: &dyn LayoutMath, - ) -> SourceResult<Vec<MathFragment>> { - let prev = std::mem::take(&mut self.fragments); - elem.layout_math(self)?; - Ok(std::mem::replace(&mut self.fragments, prev)) - } - - pub fn layout_row(&mut self, elem: &dyn LayoutMath) -> SourceResult<MathRow> { - let fragments = self.layout_fragments(elem)?; - Ok(MathRow::new(fragments)) - } - - pub fn layout_frame(&mut self, elem: &dyn LayoutMath) -> SourceResult<Frame> { - Ok(self.layout_fragment(elem)?.into_frame()) - } - - pub fn layout_box(&mut self, boxed: &BoxElem) -> SourceResult<Frame> { - Ok(boxed - .layout(self.vt, self.outer.chain(&self.local), self.regions)? - .into_frame()) - } - - pub fn layout_content(&mut self, content: &Content) -> SourceResult<Frame> { - Ok(content - .layout(self.vt, self.outer.chain(&self.local), self.regions)? - .into_frame()) - } - - pub fn layout_text(&mut self, elem: &TextElem) -> SourceResult<MathFragment> { - let text = elem.text(); - let span = elem.span(); - let mut chars = text.chars(); - let fragment = if let Some(mut glyph) = chars - .next() - .filter(|_| chars.next().is_none()) - .map(|c| self.style.styled_char(c)) - .and_then(|c| GlyphFragment::try_new(self, c, span)) - { - // A single letter that is available in the math font. - match self.style.size { - MathSize::Script => { - glyph.make_scriptsize(self); - } - MathSize::ScriptScript => { - glyph.make_scriptscriptsize(self); - } - _ => (), - } - - let class = self.style.class.as_custom().or(glyph.class); - if class == Some(MathClass::Large) { - let mut variant = if self.style.size == MathSize::Display { - let height = scaled!(self, display_operator_min_height); - glyph.stretch_vertical(self, height, Abs::zero()) - } else { - glyph.into_variant() - }; - // TeXbook p 155. Large operators are always vertically centered on the axis. - variant.center_on_axis(self); - variant.into() - } else { - glyph.into() - } - } else if text.chars().all(|c| c.is_ascii_digit() || c == '.') { - // Numbers aren't that difficult. - let mut fragments = vec![]; - for c in text.chars() { - let c = self.style.styled_char(c); - fragments.push(GlyphFragment::new(self, c, span).into()); - } - let frame = MathRow::new(fragments).into_frame(self); - FrameFragment::new(self, frame).into() - } else { - // Anything else is handled by Typst's standard text layout. - let mut style = self.style; - if self.style.italic == Smart::Auto { - style = style.with_italic(false); - } - let text: EcoString = text.chars().map(|c| style.styled_char(c)).collect(); - if text.contains(is_newline) { - let mut fragments = vec![]; - for (i, piece) in text.split(is_newline).enumerate() { - if i != 0 { - fragments.push(MathFragment::Linebreak); - } - if !piece.is_empty() { - fragments.push(self.layout_complex_text(piece, span)?.into()); - } - } - let mut frame = MathRow::new(fragments).into_frame(self); - let axis = scaled!(self, axis_height); - frame.set_baseline(frame.height() / 2.0 + axis); - FrameFragment::new(self, frame).into() - } else { - self.layout_complex_text(&text, span)?.into() - } - }; - Ok(fragment) - } - - pub fn layout_complex_text( - &mut self, - text: &str, - span: Span, - ) -> SourceResult<FrameFragment> { - let spaced = text.graphemes(true).nth(1).is_some(); - let elem = TextElem::packed(text) - .styled(TextElem::set_top_edge(TopEdge::Metric(TopEdgeMetric::Bounds))) - .styled(TextElem::set_bottom_edge(BottomEdge::Metric( - BottomEdgeMetric::Bounds, - ))) - .spanned(span); - - // There isn't a natural width for a paragraph in a math environment; - // because it will be placed somewhere probably not at the left margin - // it will overflow. So emulate an `hbox` instead and allow the paragraph - // to extend as far as needed. - let span = elem.span(); - let frame = ParElem::new(vec![Prehashed::new(elem)]) - .spanned(span) - .layout( - self.vt, - self.outer.chain(&self.local), - false, - Size::splat(Abs::inf()), - false, - )? - .into_frame(); - - Ok(FrameFragment::new(self, frame) - .with_class(MathClass::Alphabetic) - .with_spaced(spaced)) - } - - pub fn styles(&self) -> StyleChain { - self.outer.chain(&self.local) - } - - pub fn realize(&mut self, content: &Content) -> SourceResult<Option<Content>> { - realize(self.vt, content, self.outer.chain(&self.local)) - } - - pub fn style(&mut self, style: MathStyle) { - self.style_stack.push((self.style, self.size)); - let base_size = TextElem::size_in(self.styles()) / self.style.size.factor(self); - self.size = base_size * style.size.factor(self); - self.local.set(TextElem::set_size(TextSize(self.size.into()))); - self.local - .set(TextElem::set_style(if style.italic == Smart::Custom(true) { - FontStyle::Italic - } else { - FontStyle::Normal - })); - self.local.set(TextElem::set_weight(if style.bold { - FontWeight::BOLD - } else { - // The normal weight is what we started with. - // It's 400 for CM Regular, 450 for CM Book. - self.font.info().variant.weight - })); - self.style = style; - } - - pub fn unstyle(&mut self) { - (self.style, self.size) = self.style_stack.pop().unwrap(); - self.local.unset(); - self.local.unset(); - self.local.unset(); - } -} - -pub(super) trait Scaled { - fn scaled(self, ctx: &MathContext) -> Abs; -} - -impl Scaled for i16 { - fn scaled(self, ctx: &MathContext) -> Abs { - ctx.font.to_em(self).scaled(ctx) - } -} - -impl Scaled for u16 { - fn scaled(self, ctx: &MathContext) -> Abs { - ctx.font.to_em(self).scaled(ctx) - } -} - -impl Scaled for Em { - fn scaled(self, ctx: &MathContext) -> Abs { - self.at(ctx.size) - } -} - -impl Scaled for MathValue<'_> { - fn scaled(self, ctx: &MathContext) -> Abs { - self.value.scaled(ctx) - } -} diff --git a/crates/typst-library/src/math/frac.rs b/crates/typst-library/src/math/frac.rs deleted file mode 100644 index c3014178..00000000 --- a/crates/typst-library/src/math/frac.rs +++ /dev/null @@ -1,157 +0,0 @@ -use super::*; - -const FRAC_AROUND: Em = Em::new(0.1); - -/// A mathematical fraction. -/// -/// # Example -/// ```example -/// $ 1/2 < (x+1)/2 $ -/// $ ((x+1)) / 2 = frac(a, b) $ -/// ``` -/// -/// # Syntax -/// This function also has dedicated syntax: Use a slash to turn neighbouring -/// expressions into a fraction. Multiple atoms can be grouped into a single -/// expression using round grouping parenthesis. Such parentheses are removed -/// from the output, but you can nest multiple to force them. -#[elem(title = "Fraction", LayoutMath)] -pub struct FracElem { - /// The fraction's numerator. - #[required] - pub num: Content, - - /// The fraction's denominator. - #[required] - pub denom: Content, -} - -impl LayoutMath for FracElem { - #[tracing::instrument(skip(ctx))] - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - layout(ctx, self.num(), std::slice::from_ref(self.denom()), false, self.span()) - } -} - -/// A binomial expression. -/// -/// # Example -/// ```example -/// $ binom(n, k) $ -/// $ binom(n, k_1, k_2, k_3, ..., k_m) $ -/// ``` -#[elem(title = "Binomial", LayoutMath)] -pub struct BinomElem { - /// The binomial's upper index. - #[required] - pub upper: Content, - - /// The binomial's lower index. - #[required] - #[variadic] - #[parse( - let values = args.all::<Spanned<Value>>()?; - if values.is_empty() { - // Prevents one element binomials - bail!(args.span, "missing argument: lower"); - } - values.into_iter().map(|spanned| spanned.v.display()).collect() - )] - pub lower: Vec<Content>, -} - -impl LayoutMath for BinomElem { - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - layout(ctx, self.upper(), self.lower(), true, self.span()) - } -} - -/// Layout a fraction or binomial. -fn layout( - ctx: &mut MathContext, - num: &Content, - denom: &[Content], - binom: bool, - span: Span, -) -> SourceResult<()> { - let short_fall = DELIM_SHORT_FALL.scaled(ctx); - let axis = scaled!(ctx, axis_height); - let thickness = scaled!(ctx, fraction_rule_thickness); - let shift_up = scaled!( - ctx, - text: fraction_numerator_shift_up, - display: fraction_numerator_display_style_shift_up, - ); - let shift_down = scaled!( - ctx, - text: fraction_denominator_shift_down, - display: fraction_denominator_display_style_shift_down, - ); - let num_min = scaled!( - ctx, - text: fraction_numerator_gap_min, - display: fraction_num_display_style_gap_min, - ); - let denom_min = scaled!( - ctx, - text: fraction_denominator_gap_min, - display: fraction_denom_display_style_gap_min, - ); - - ctx.style(ctx.style.for_numerator()); - let num = ctx.layout_frame(num)?; - ctx.unstyle(); - - ctx.style(ctx.style.for_denominator()); - let denom = ctx.layout_frame(&Content::sequence( - // Add a comma between each element. - denom.iter().flat_map(|a| [TextElem::packed(','), a.clone()]).skip(1), - ))?; - ctx.unstyle(); - - let around = FRAC_AROUND.scaled(ctx); - let num_gap = (shift_up - axis - num.descent()).max(num_min + thickness / 2.0); - let denom_gap = (shift_down + axis - denom.ascent()).max(denom_min + thickness / 2.0); - - let line_width = num.width().max(denom.width()); - let width = line_width + 2.0 * around; - let height = num.height() + num_gap + thickness + denom_gap + denom.height(); - let size = Size::new(width, height); - let num_pos = Point::with_x((width - num.width()) / 2.0); - let line_pos = - Point::new((width - line_width) / 2.0, num.height() + num_gap + thickness / 2.0); - let denom_pos = Point::new((width - denom.width()) / 2.0, height - denom.height()); - let baseline = line_pos.y + axis; - - let mut frame = Frame::soft(size); - frame.set_baseline(baseline); - frame.push_frame(num_pos, num); - frame.push_frame(denom_pos, denom); - - if binom { - let mut left = - GlyphFragment::new(ctx, '(', span).stretch_vertical(ctx, height, short_fall); - left.center_on_axis(ctx); - ctx.push(left); - ctx.push(FrameFragment::new(ctx, frame)); - let mut right = - GlyphFragment::new(ctx, ')', span).stretch_vertical(ctx, height, short_fall); - right.center_on_axis(ctx); - ctx.push(right); - } else { - frame.push( - line_pos, - FrameItem::Shape( - Geometry::Line(Point::with_x(line_width)).stroked(FixedStroke { - paint: TextElem::fill_in(ctx.styles()).as_decoration(), - thickness, - ..FixedStroke::default() - }), - span, - ), - ); - ctx.push(FrameFragment::new(ctx, frame)); - } - - Ok(()) -} diff --git a/crates/typst-library/src/math/fragment.rs b/crates/typst-library/src/math/fragment.rs deleted file mode 100644 index 76ee2512..00000000 --- a/crates/typst-library/src/math/fragment.rs +++ /dev/null @@ -1,513 +0,0 @@ -use rustybuzz::Feature; -use ttf_parser::gsub::{ - AlternateSet, AlternateSubstitution, SingleSubstitution, SubstitutionSubtable, -}; -use ttf_parser::opentype_layout::LayoutTable; - -use super::*; - -#[derive(Debug, Clone)] -pub enum MathFragment { - Glyph(GlyphFragment), - Variant(VariantFragment), - Frame(FrameFragment), - Spacing(Abs), - Space(Abs), - Linebreak, - Align, -} - -impl MathFragment { - pub fn size(&self) -> Size { - Size::new(self.width(), self.height()) - } - - pub fn width(&self) -> Abs { - match self { - Self::Glyph(glyph) => glyph.width, - Self::Variant(variant) => variant.frame.width(), - Self::Frame(fragment) => fragment.frame.width(), - Self::Spacing(amount) => *amount, - Self::Space(amount) => *amount, - _ => Abs::zero(), - } - } - - pub fn height(&self) -> Abs { - match self { - Self::Glyph(glyph) => glyph.height(), - Self::Variant(variant) => variant.frame.height(), - Self::Frame(fragment) => fragment.frame.height(), - _ => Abs::zero(), - } - } - - pub fn ascent(&self) -> Abs { - match self { - Self::Glyph(glyph) => glyph.ascent, - Self::Variant(variant) => variant.frame.ascent(), - Self::Frame(fragment) => fragment.frame.baseline(), - _ => Abs::zero(), - } - } - - pub fn descent(&self) -> Abs { - match self { - Self::Glyph(glyph) => glyph.descent, - Self::Variant(variant) => variant.frame.descent(), - Self::Frame(fragment) => fragment.frame.descent(), - _ => Abs::zero(), - } - } - - pub fn class(&self) -> Option<MathClass> { - self.style().and_then(|style| style.class.as_custom()).or(match self { - Self::Glyph(glyph) => glyph.class, - Self::Variant(variant) => variant.class, - Self::Frame(fragment) => Some(fragment.class), - _ => None, - }) - } - - pub fn style(&self) -> Option<MathStyle> { - match self { - Self::Glyph(glyph) => Some(glyph.style), - Self::Variant(variant) => Some(variant.style), - Self::Frame(fragment) => Some(fragment.style), - _ => None, - } - } - - pub fn font_size(&self) -> Option<Abs> { - match self { - Self::Glyph(glyph) => Some(glyph.font_size), - Self::Variant(variant) => Some(variant.font_size), - Self::Frame(fragment) => Some(fragment.font_size), - _ => None, - } - } - - pub fn set_class(&mut self, class: MathClass) { - macro_rules! set_style_class { - ($fragment:ident) => { - if $fragment.style.class.is_custom() { - $fragment.style.class = Smart::Custom(class); - } - }; - } - - match self { - Self::Glyph(glyph) => { - glyph.class = Some(class); - set_style_class!(glyph); - } - Self::Variant(variant) => { - variant.class = Some(class); - set_style_class!(variant); - } - Self::Frame(fragment) => { - fragment.class = class; - set_style_class!(fragment); - } - _ => {} - } - } - - pub fn set_limits(&mut self, limits: Limits) { - match self { - Self::Glyph(glyph) => glyph.limits = limits, - Self::Variant(variant) => variant.limits = limits, - Self::Frame(fragment) => fragment.limits = limits, - _ => {} - } - } - - pub fn is_spaced(&self) -> bool { - match self { - MathFragment::Frame(frame) => { - match self.style().and_then(|style| style.class.as_custom()) { - Some(MathClass::Fence) => true, - Some(_) => false, - None => frame.spaced, - } - } - _ => self.class() == Some(MathClass::Fence), - } - } - - pub fn italics_correction(&self) -> Abs { - match self { - Self::Glyph(glyph) => glyph.italics_correction, - Self::Variant(variant) => variant.italics_correction, - _ => Abs::zero(), - } - } - - pub fn into_frame(self) -> Frame { - match self { - Self::Glyph(glyph) => glyph.into_frame(), - Self::Variant(variant) => variant.frame, - Self::Frame(fragment) => fragment.frame, - _ => Frame::soft(self.size()), - } - } - - pub fn limits(&self) -> Limits { - match self { - MathFragment::Glyph(glyph) => glyph.limits, - MathFragment::Variant(variant) => variant.limits, - MathFragment::Frame(fragment) => fragment.limits, - _ => Limits::Never, - } - } -} - -impl From<GlyphFragment> for MathFragment { - fn from(glyph: GlyphFragment) -> Self { - Self::Glyph(glyph) - } -} - -impl From<VariantFragment> for MathFragment { - fn from(variant: VariantFragment) -> Self { - Self::Variant(variant) - } -} - -impl From<FrameFragment> for MathFragment { - fn from(fragment: FrameFragment) -> Self { - Self::Frame(fragment) - } -} - -#[derive(Clone)] -pub struct GlyphFragment { - pub id: GlyphId, - pub c: char, - pub font: Font, - pub lang: Lang, - pub fill: Paint, - pub shift: Abs, - pub width: Abs, - pub ascent: Abs, - pub descent: Abs, - pub italics_correction: Abs, - pub style: MathStyle, - pub font_size: Abs, - pub class: Option<MathClass>, - pub span: Span, - pub meta: SmallVec<[Meta; 1]>, - pub limits: Limits, -} - -impl GlyphFragment { - pub fn new(ctx: &MathContext, c: char, span: Span) -> Self { - let id = ctx.ttf.glyph_index(c).unwrap_or_default(); - let id = Self::adjust_glyph_index(ctx, id); - Self::with_id(ctx, c, id, span) - } - - pub fn try_new(ctx: &MathContext, c: char, span: Span) -> Option<Self> { - let c = ctx.style.styled_char(c); - let id = ctx.ttf.glyph_index(c)?; - let id = Self::adjust_glyph_index(ctx, id); - Some(Self::with_id(ctx, c, id, span)) - } - - pub fn with_id(ctx: &MathContext, c: char, id: GlyphId, span: Span) -> Self { - let class = match c { - ':' => Some(MathClass::Relation), - '.' | '/' | '⋯' | '⋱' | '⋰' | '⋮' => Some(MathClass::Normal), - _ => unicode_math_class::class(c), - }; - let mut fragment = Self { - id, - c, - font: ctx.font.clone(), - lang: TextElem::lang_in(ctx.styles()), - fill: TextElem::fill_in(ctx.styles()).as_decoration(), - shift: TextElem::baseline_in(ctx.styles()), - style: ctx.style, - font_size: ctx.size, - width: Abs::zero(), - ascent: Abs::zero(), - descent: Abs::zero(), - limits: Limits::for_char(c), - italics_correction: Abs::zero(), - class, - span, - meta: MetaElem::data_in(ctx.styles()), - }; - fragment.set_id(ctx, id); - fragment - } - - /// Apply GSUB substitutions. - fn adjust_glyph_index(ctx: &MathContext, id: GlyphId) -> GlyphId { - if let Some(glyphwise_tables) = &ctx.glyphwise_tables { - glyphwise_tables.iter().fold(id, |id, table| table.apply(id)) - } else { - id - } - } - - /// Sets element id and boxes in appropriate way without changing other - /// styles. This is used to replace the glyph with a stretch variant. - pub fn set_id(&mut self, ctx: &MathContext, id: GlyphId) { - let advance = ctx.ttf.glyph_hor_advance(id).unwrap_or_default(); - let italics = italics_correction(ctx, id).unwrap_or_default(); - let bbox = ctx.ttf.glyph_bounding_box(id).unwrap_or(Rect { - x_min: 0, - y_min: 0, - x_max: 0, - y_max: 0, - }); - - let mut width = advance.scaled(ctx); - if !is_extended_shape(ctx, id) { - width += italics; - } - - self.id = id; - self.width = width; - self.ascent = bbox.y_max.scaled(ctx); - self.descent = -bbox.y_min.scaled(ctx); - self.italics_correction = italics; - } - - pub fn height(&self) -> Abs { - self.ascent + self.descent - } - - pub fn into_variant(self) -> VariantFragment { - VariantFragment { - c: self.c, - id: Some(self.id), - style: self.style, - font_size: self.font_size, - italics_correction: self.italics_correction, - class: self.class, - span: self.span, - limits: self.limits, - frame: self.into_frame(), - } - } - - pub fn into_frame(self) -> Frame { - let item = TextItem { - font: self.font.clone(), - size: self.font_size, - fill: self.fill, - lang: self.lang, - text: self.c.into(), - glyphs: vec![Glyph { - id: self.id.0, - x_advance: Em::from_length(self.width, self.font_size), - x_offset: Em::zero(), - range: 0..self.c.len_utf8() as u16, - span: (self.span, 0), - }], - }; - let size = Size::new(self.width, self.ascent + self.descent); - let mut frame = Frame::soft(size); - frame.set_baseline(self.ascent); - frame.push(Point::with_y(self.ascent + self.shift), FrameItem::Text(item)); - frame.meta_iter(self.meta); - frame - } - - pub fn make_scriptsize(&mut self, ctx: &MathContext) { - let alt_id = - script_alternatives(ctx, self.id).and_then(|alts| alts.alternates.get(0)); - - if let Some(alt_id) = alt_id { - self.set_id(ctx, alt_id); - } - } - - pub fn make_scriptscriptsize(&mut self, ctx: &MathContext) { - let alts = script_alternatives(ctx, self.id); - let alt_id = alts - .and_then(|alts| alts.alternates.get(1).or_else(|| alts.alternates.get(0))); - - if let Some(alt_id) = alt_id { - self.set_id(ctx, alt_id); - } - } -} - -impl Debug for GlyphFragment { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "GlyphFragment({:?})", self.c) - } -} - -#[derive(Clone)] -pub struct VariantFragment { - pub c: char, - pub id: Option<GlyphId>, - pub italics_correction: Abs, - pub frame: Frame, - pub style: MathStyle, - pub font_size: Abs, - pub class: Option<MathClass>, - pub span: Span, - pub limits: Limits, -} - -impl VariantFragment { - /// Vertically adjust the fragment's frame so that it is centered - /// on the axis. - pub fn center_on_axis(&mut self, ctx: &MathContext) { - let h = self.frame.height(); - self.frame.set_baseline(h / 2.0 + scaled!(ctx, axis_height)); - } -} - -impl Debug for VariantFragment { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "VariantFragment({:?})", self.c) - } -} - -#[derive(Debug, Clone)] -pub struct FrameFragment { - pub frame: Frame, - pub style: MathStyle, - pub font_size: Abs, - pub class: MathClass, - pub limits: Limits, - pub spaced: bool, - pub base_ascent: Abs, -} - -impl FrameFragment { - pub fn new(ctx: &MathContext, mut frame: Frame) -> Self { - let base_ascent = frame.ascent(); - frame.meta(ctx.styles(), false); - Self { - frame, - font_size: ctx.size, - style: ctx.style, - class: MathClass::Normal, - limits: Limits::Never, - spaced: false, - base_ascent, - } - } - - pub fn with_class(self, class: MathClass) -> Self { - Self { class, ..self } - } - - pub fn with_limits(self, limits: Limits) -> Self { - Self { limits, ..self } - } - - pub fn with_spaced(self, spaced: bool) -> Self { - Self { spaced, ..self } - } - - pub fn with_base_ascent(self, base_ascent: Abs) -> Self { - Self { base_ascent, ..self } - } -} - -/// Look up the italics correction for a glyph. -fn italics_correction(ctx: &MathContext, id: GlyphId) -> Option<Abs> { - Some(ctx.table.glyph_info?.italic_corrections?.get(id)?.scaled(ctx)) -} - -/// Look up the script/scriptscript alternates for a glyph -fn script_alternatives<'a>( - ctx: &MathContext<'a, '_, '_>, - id: GlyphId, -) -> Option<AlternateSet<'a>> { - ctx.ssty_table.and_then(|ssty| { - ssty.coverage.get(id).and_then(|index| ssty.alternate_sets.get(index)) - }) -} - -/// Look up the italics correction for a glyph. -fn is_extended_shape(ctx: &MathContext, id: GlyphId) -> bool { - ctx.table - .glyph_info - .and_then(|info| info.extended_shapes) - .and_then(|info| info.get(id)) - .is_some() -} - -/// Look up a kerning value at a specific corner and height. -/// -/// This can be integrated once we've found a font that actually provides this -/// data. -#[allow(unused)] -fn kern_at_height( - ctx: &MathContext, - id: GlyphId, - corner: Corner, - height: Abs, -) -> Option<Abs> { - let kerns = ctx.table.glyph_info?.kern_infos?.get(id)?; - let kern = match corner { - Corner::TopLeft => kerns.top_left, - Corner::TopRight => kerns.top_right, - Corner::BottomRight => kerns.bottom_right, - Corner::BottomLeft => kerns.bottom_left, - }?; - - let mut i = 0; - while i < kern.count() && height > kern.height(i)?.scaled(ctx) { - i += 1; - } - - Some(kern.kern(i)?.scaled(ctx)) -} - -/// An OpenType substitution table that is applicable to glyph-wise substitutions. -pub enum GlyphwiseSubsts<'a> { - Single(SingleSubstitution<'a>), - Alternate(AlternateSubstitution<'a>, u32), -} - -impl<'a> GlyphwiseSubsts<'a> { - pub fn new(gsub: LayoutTable<'a>, feature: Feature) -> Option<Self> { - let table = gsub - .features - .find(ttf_parser::Tag(feature.tag.0)) - .and_then(|feature| feature.lookup_indices.get(0)) - .and_then(|index| gsub.lookups.get(index))?; - let table = table.subtables.get::<SubstitutionSubtable>(0)?; - match table { - SubstitutionSubtable::Single(single_glyphs) => { - Some(Self::Single(single_glyphs)) - } - SubstitutionSubtable::Alternate(alt_glyphs) => { - Some(Self::Alternate(alt_glyphs, feature.value)) - } - _ => None, - } - } - - pub fn try_apply(&self, glyph_id: GlyphId) -> Option<GlyphId> { - match self { - Self::Single(single) => match single { - SingleSubstitution::Format1 { coverage, delta } => coverage - .get(glyph_id) - .map(|_| GlyphId(glyph_id.0.wrapping_add(*delta as u16))), - SingleSubstitution::Format2 { coverage, substitutes } => { - coverage.get(glyph_id).and_then(|idx| substitutes.get(idx)) - } - }, - Self::Alternate(alternate, value) => alternate - .coverage - .get(glyph_id) - .and_then(|idx| alternate.alternate_sets.get(idx)) - .and_then(|set| set.alternates.get(*value as u16)), - } - } - - pub fn apply(&self, glyph_id: GlyphId) -> GlyphId { - self.try_apply(glyph_id).unwrap_or(glyph_id) - } -} diff --git a/crates/typst-library/src/math/lr.rs b/crates/typst-library/src/math/lr.rs deleted file mode 100644 index 39143620..00000000 --- a/crates/typst-library/src/math/lr.rs +++ /dev/null @@ -1,195 +0,0 @@ -use super::*; - -/// How much less high scaled delimiters can be than what they wrap. -pub(super) const DELIM_SHORT_FALL: Em = Em::new(0.1); - -/// Scales delimiters. -/// -/// While matched delimiters scale by default, this can be used to scale -/// unmatched delimiters and to control the delimiter scaling more precisely. -#[elem(title = "Left/Right", LayoutMath)] -pub struct LrElem { - /// The size of the brackets, relative to the height of the wrapped content. - pub size: Smart<Rel<Length>>, - - /// The delimited content, including the delimiters. - #[required] - #[parse( - let mut body = Content::empty(); - for (i, arg) in args.all::<Content>()?.into_iter().enumerate() { - if i > 0 { - body += TextElem::packed(','); - } - body += arg; - } - body - )] - pub body: Content, -} - -impl LayoutMath for LrElem { - #[tracing::instrument(skip(ctx))] - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - let mut body = self.body(); - if let Some(elem) = body.to::<LrElem>() { - if elem.size(ctx.styles()).is_auto() { - body = elem.body(); - } - } - - let mut fragments = ctx.layout_fragments(body)?; - let axis = scaled!(ctx, axis_height); - let max_extent = fragments - .iter() - .map(|fragment| (fragment.ascent() - axis).max(fragment.descent() + axis)) - .max() - .unwrap_or_default(); - - let height = self - .size(ctx.styles()) - .unwrap_or(Rel::one()) - .resolve(ctx.styles()) - .relative_to(2.0 * max_extent); - - match fragments.as_mut_slice() { - [one] => scale(ctx, one, height, None), - [first, .., last] => { - scale(ctx, first, height, Some(MathClass::Opening)); - scale(ctx, last, height, Some(MathClass::Closing)); - } - _ => {} - } - - ctx.extend(fragments); - - Ok(()) - } -} - -/// Scale a math fragment to a height. -fn scale( - ctx: &mut MathContext, - fragment: &mut MathFragment, - height: Abs, - apply: Option<MathClass>, -) { - if matches!( - fragment.class(), - Some(MathClass::Opening | MathClass::Closing | MathClass::Fence) - ) { - let glyph = match fragment { - MathFragment::Glyph(glyph) => glyph.clone(), - MathFragment::Variant(variant) => { - GlyphFragment::new(ctx, variant.c, variant.span) - } - _ => return, - }; - - let short_fall = DELIM_SHORT_FALL.scaled(ctx); - let mut stretched = glyph.stretch_vertical(ctx, height, short_fall); - stretched.center_on_axis(ctx); - - *fragment = MathFragment::Variant(stretched); - if let Some(class) = apply { - fragment.set_class(class); - } - } -} - -/// Floors an expression. -/// -/// ```example -/// $ floor(x/2) $ -/// ``` -#[func] -pub fn floor( - /// The size of the brackets, relative to the height of the wrapped content. - #[named] - size: Option<Smart<Rel<Length>>>, - /// The expression to floor. - body: Content, -) -> Content { - delimited(body, '⌊', '⌋', size) -} - -/// Ceils an expression. -/// -/// ```example -/// $ ceil(x/2) $ -/// ``` -#[func] -pub fn ceil( - /// The size of the brackets, relative to the height of the wrapped content. - #[named] - size: Option<Smart<Rel<Length>>>, - /// The expression to ceil. - body: Content, -) -> Content { - delimited(body, '⌈', '⌉', size) -} - -/// Rounds an expression. -/// -/// ```example -/// $ round(x/2) $ -/// ``` -#[func] -pub fn round( - /// The size of the brackets, relative to the height of the wrapped content. - #[named] - size: Option<Smart<Rel<Length>>>, - /// The expression to round. - body: Content, -) -> Content { - delimited(body, '⌊', '⌉', size) -} - -/// Takes the absolute value of an expression. -/// -/// ```example -/// $ abs(x/2) $ -/// ``` -#[func] -pub fn abs( - /// The size of the brackets, relative to the height of the wrapped content. - #[named] - size: Option<Smart<Rel<Length>>>, - /// The expression to take the absolute value of. - body: Content, -) -> Content { - delimited(body, '|', '|', size) -} - -/// Takes the norm of an expression. -/// -/// ```example -/// $ norm(x/2) $ -/// ``` -#[func] -pub fn norm( - /// The size of the brackets, relative to the height of the wrapped content. - #[named] - size: Option<Smart<Rel<Length>>>, - /// The expression to take the norm of. - body: Content, -) -> Content { - delimited(body, '‖', '‖', size) -} - -fn delimited( - body: Content, - left: char, - right: char, - size: Option<Smart<Rel<Length>>>, -) -> Content { - let mut elem = LrElem::new(Content::sequence([ - TextElem::packed(left), - body, - TextElem::packed(right), - ])); - // Push size only if size is provided - if let Some(size) = size { - elem.push_size(size); - } - elem.pack() -} diff --git a/crates/typst-library/src/math/matrix.rs b/crates/typst-library/src/math/matrix.rs deleted file mode 100644 index b5d21ed6..00000000 --- a/crates/typst-library/src/math/matrix.rs +++ /dev/null @@ -1,655 +0,0 @@ -use super::*; - -const DEFAULT_ROW_GAP: Em = Em::new(0.5); -const DEFAULT_COL_GAP: Em = Em::new(0.5); -const VERTICAL_PADDING: Ratio = Ratio::new(0.1); - -const DEFAULT_STROKE_THICKNESS: Em = Em::new(0.05); - -/// A column vector. -/// -/// Content in the vector's elements can be aligned with the `&` symbol. -/// -/// # Example -/// ```example -/// $ vec(a, b, c) dot vec(1, 2, 3) -/// = a + 2b + 3c $ -/// ``` -#[elem(title = "Vector", LayoutMath)] -pub struct VecElem { - /// The delimiter to use. - /// - /// ```example - /// #set math.vec(delim: "[") - /// $ vec(1, 2) $ - /// ``` - #[default(Some(Delimiter::Paren))] - pub delim: Option<Delimiter>, - - /// The gap between elements. - /// - /// ```example - /// #set math.vec(gap: 1em) - /// $ vec(1, 2) $ - /// ``` - #[resolve] - #[default(DEFAULT_ROW_GAP.into())] - pub gap: Rel<Length>, - - /// The elements of the vector. - #[variadic] - pub children: Vec<Content>, -} - -impl LayoutMath for VecElem { - #[tracing::instrument(skip(ctx))] - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - let delim = self.delim(ctx.styles()); - let frame = layout_vec_body( - ctx, - self.children(), - FixedAlign::Center, - self.gap(ctx.styles()), - )?; - layout_delimiters( - ctx, - frame, - delim.map(Delimiter::open), - delim.map(Delimiter::close), - self.span(), - ) - } -} - -/// A matrix. -/// -/// The elements of a row should be separated by commas, while the rows -/// themselves should be separated by semicolons. The semicolon syntax merges -/// preceding arguments separated by commas into an array. You can also use this -/// special syntax of math function calls to define custom functions that take -/// 2D data. -/// -/// Content in cells that are in the same row can be aligned with the `&` symbol. -/// -/// # Example -/// ```example -/// $ mat( -/// 1, 2, ..., 10; -/// 2, 2, ..., 10; -/// dots.v, dots.v, dots.down, dots.v; -/// 10, 10, ..., 10; -/// ) $ -/// ``` -#[elem(title = "Matrix", LayoutMath)] -pub struct MatElem { - /// The delimiter to use. - /// - /// ```example - /// #set math.mat(delim: "[") - /// $ mat(1, 2; 3, 4) $ - /// ``` - #[default(Some(Delimiter::Paren))] - pub delim: Option<Delimiter>, - - /// Draws augmentation lines in a matrix. - /// - /// - `{none}`: No lines are drawn. - /// - A single number: A vertical augmentation line is drawn - /// after the specified column number. Negative numbers start from the end. - /// - A dictionary: With a dictionary, multiple augmentation lines can be - /// drawn both horizontally and vertically. Additionally, the style of the - /// lines can be set. The dictionary can contain the following keys: - /// - `hline`: The offsets at which horizontal lines should be drawn. - /// For example, an offset of `2` would result in a horizontal line - /// being drawn after the second row of the matrix. Accepts either an - /// integer for a single line, or an array of integers - /// for multiple lines. Like for a single number, negative numbers start from the end. - /// - `vline`: The offsets at which vertical lines should be drawn. - /// For example, an offset of `2` would result in a vertical line being - /// drawn after the second column of the matrix. Accepts either an - /// integer for a single line, or an array of integers - /// for multiple lines. Like for a single number, negative numbers start from the end. - /// - `stroke`: How to [stroke]($stroke) the line. If set to `{auto}`, - /// takes on a thickness of 0.05em and square line caps. - /// - /// ```example - /// $ mat(1, 0, 1; 0, 1, 2; augment: #2) $ - /// // Equivalent to: - /// $ mat(1, 0, 1; 0, 1, 2; augment: #(-1)) $ - /// ``` - /// - /// ```example - /// $ mat(0, 0, 0; 1, 1, 1; augment: #(hline: 1, stroke: 2pt + green)) $ - /// ``` - #[resolve] - #[fold] - pub augment: Option<Augment>, - - /// The gap between rows and columns. - /// - /// ```example - /// #set math.mat(gap: 1em) - /// $ mat(1, 2; 3, 4) $ - /// ``` - #[external] - pub gap: Rel<Length>, - - /// The gap between rows. Takes precedence over `gap`. - /// - /// ```example - /// #set math.mat(row-gap: 1em) - /// $ mat(1, 2; 3, 4) $ - /// ``` - #[resolve] - #[parse( - let gap = args.named("gap")?; - args.named("row-gap")?.or(gap) - )] - #[default(DEFAULT_ROW_GAP.into())] - pub row_gap: Rel<Length>, - - /// The gap between columns. Takes precedence over `gap`. - /// - /// ```example - /// #set math.mat(column-gap: 1em) - /// $ mat(1, 2; 3, 4) $ - /// ``` - #[resolve] - #[parse(args.named("column-gap")?.or(gap))] - #[default(DEFAULT_COL_GAP.into())] - pub column_gap: Rel<Length>, - - /// An array of arrays with the rows of the matrix. - /// - /// ```example - /// #let data = ((1, 2, 3), (4, 5, 6)) - /// #let matrix = math.mat(..data) - /// $ v := matrix $ - /// ``` - #[variadic] - #[parse( - let mut rows = vec![]; - let mut width = 0; - - let values = args.all::<Spanned<Value>>()?; - if values.iter().any(|spanned| matches!(spanned.v, Value::Array(_))) { - for Spanned { v, span } in values { - let array = v.cast::<Array>().at(span)?; - let row: Vec<_> = array.into_iter().map(Value::display).collect(); - width = width.max(row.len()); - rows.push(row); - } - } else { - rows = vec![values.into_iter().map(|spanned| spanned.v.display()).collect()]; - } - - for row in &mut rows { - if row.len() < width { - row.resize(width, Content::empty()); - } - } - - rows - )] - pub rows: Vec<Vec<Content>>, -} - -impl LayoutMath for MatElem { - #[tracing::instrument(skip(ctx))] - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - // validate inputs - - let augment = self.augment(ctx.styles()); - let rows = self.rows(); - - if let Some(aug) = &augment { - for &offset in &aug.hline.0 { - if offset == 0 || offset.unsigned_abs() >= rows.len() { - bail!( - self.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 = self.rows().first().map_or(0, |row| row.len()); - - for &offset in &aug.vline.0 { - if offset == 0 || offset.unsigned_abs() >= ncols { - bail!( - self.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 = self.delim(ctx.styles()); - let frame = layout_mat_body( - ctx, - rows, - augment, - Axes::new(self.column_gap(ctx.styles()), self.row_gap(ctx.styles())), - self.span(), - )?; - - layout_delimiters( - ctx, - frame, - delim.map(Delimiter::open), - delim.map(Delimiter::close), - self.span(), - ) - } -} - -/// A case distinction. -/// -/// Content across different branches can be aligned with the `&` symbol. -/// -/// # Example -/// ```example -/// $ f(x, y) := cases( -/// 1 "if" (x dot y)/2 <= 0, -/// 2 "if" x "is even", -/// 3 "if" x in NN, -/// 4 "else", -/// ) $ -/// ``` -#[elem(LayoutMath)] -pub struct CasesElem { - /// The delimiter to use. - /// - /// ```example - /// #set math.cases(delim: "[") - /// $ x = cases(1, 2) $ - /// ``` - #[default(Delimiter::Brace)] - pub delim: Delimiter, - - /// Whether the direction of cases should be reversed. - /// - /// ```example - /// #set math.cases(reverse: true) - /// $ cases(1, 2) = x $ - /// ``` - #[default(false)] - pub reverse: bool, - - /// The gap between branches. - /// - /// ```example - /// #set math.cases(gap: 1em) - /// $ x = cases(1, 2) $ - /// ``` - #[resolve] - #[default(DEFAULT_ROW_GAP.into())] - pub gap: Rel<Length>, - - /// The branches of the case distinction. - #[variadic] - pub children: Vec<Content>, -} - -impl LayoutMath for CasesElem { - #[tracing::instrument(skip(ctx))] - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - let delim = self.delim(ctx.styles()); - let frame = layout_vec_body( - ctx, - self.children(), - FixedAlign::Start, - self.gap(ctx.styles()), - )?; - - let (open, close) = if self.reverse(ctx.styles()) { - (None, Some(delim.close())) - } else { - (Some(delim.open()), None) - }; - - layout_delimiters(ctx, frame, open, close, self.span()) - } -} - -/// A vector / matrix delimiter. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] -pub enum Delimiter { - /// Delimit with parentheses. - #[string("(")] - Paren, - /// Delimit with brackets. - #[string("[")] - Bracket, - /// Delimit with curly braces. - #[string("{")] - Brace, - /// Delimit with vertical bars. - #[string("|")] - Bar, - /// Delimit with double vertical bars. - #[string("||")] - DoubleBar, -} - -impl Delimiter { - /// The delimiter's opening character. - fn open(self) -> char { - match self { - Self::Paren => '(', - Self::Bracket => '[', - Self::Brace => '{', - Self::Bar => '|', - Self::DoubleBar => '‖', - } - } - - /// The delimiter's closing character. - fn close(self) -> char { - match self { - Self::Paren => ')', - Self::Bracket => ']', - Self::Brace => '}', - Self::Bar => '|', - Self::DoubleBar => '‖', - } - } -} - -/// Layout the inner contents of a vector. -fn layout_vec_body( - ctx: &mut MathContext, - column: &[Content], - align: FixedAlign, - row_gap: Rel<Abs>, -) -> SourceResult<Frame> { - let gap = row_gap.relative_to(ctx.regions.base().y); - ctx.style(ctx.style.for_denominator()); - let mut flat = vec![]; - for child in column { - flat.push(ctx.layout_row(child)?); - } - ctx.unstyle(); - Ok(stack(ctx, flat, align, gap, 0)) -} - -/// Layout the inner contents of a matrix. -fn layout_mat_body( - ctx: &mut MathContext, - rows: &[Vec<Content>], - augment: Option<Augment<Abs>>, - gap: Axes<Rel<Abs>>, - span: Span, -) -> SourceResult<Frame> { - let gap = gap.zip_map(ctx.regions.base(), Rel::relative_to); - let half_gap = gap * 0.5; - - // We provide a default stroke thickness that scales - // with font size to ensure that augmentation lines - // look correct by default at all matrix sizes. - // The line cap is also set to square because it looks more "correct". - let default_stroke_thickness = DEFAULT_STROKE_THICKNESS.scaled(ctx); - let default_stroke = FixedStroke { - thickness: default_stroke_thickness, - paint: TextElem::fill_in(ctx.styles()).as_decoration(), - line_cap: LineCap::Square, - ..Default::default() - }; - - let (hline, vline, stroke) = match augment { - Some(v) => { - // need to get stroke here for ownership - let stroke = v.stroke_or(default_stroke); - - (v.hline, v.vline, stroke) - } - _ => (Offsets::default(), Offsets::default(), default_stroke), - }; - - let ncols = rows.first().map_or(0, |row| row.len()); - let nrows = rows.len(); - if ncols == 0 || nrows == 0 { - return Ok(Frame::soft(Size::zero())); - } - - // 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. - - // 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]; - - ctx.style(ctx.style.for_denominator()); - for (row, (ascent, descent)) in rows.iter().zip(&mut heights) { - for (cell, col) in row.iter().zip(&mut cols) { - let cell = ctx.layout_row(cell)?; - - ascent.set_max(cell.ascent()); - descent.set_max(cell.descent()); - - col.push(cell); - } - } - ctx.unstyle(); - - // For each row, combine maximum ascent and descent into a row height. - // Sum the row heights, then add the total height of the gaps between rows. - let total_height = - heights.iter().map(|&(a, b)| a + b).sum::<Abs>() + gap.y * (nrows - 1) as f64; - - // Width starts at zero because it can't be calculated until later - let mut frame = Frame::soft(Size::new(Abs::zero(), total_height)); - - let mut x = Abs::zero(); - - for (index, col) in cols.into_iter().enumerate() { - let AlignmentResult { points, width: rcol } = alignments(&col); - - let mut y = Abs::zero(); - - for (cell, &(ascent, descent)) in col.into_iter().zip(&heights) { - let cell = cell.into_aligned_frame(ctx, &points, FixedAlign::Center); - let pos = Point::new( - if points.is_empty() { x + (rcol - cell.width()) / 2.0 } else { x }, - y + ascent - cell.ascent(), - ); - - frame.push_frame(pos, cell); - - y += ascent + descent + gap.y; - } - - // Advance to the end of the column - x += rcol; - - // If a vertical line should be inserted after this column - if vline.0.contains(&(index as isize + 1)) - || vline.0.contains(&(1 - ((ncols - index) as isize))) - { - frame.push( - Point::with_x(x + half_gap.x), - line_item(total_height, true, stroke.clone(), span), - ); - } - - // Advance to the start of the next column - x += gap.x; - } - - // Once all the columns are laid out, the total width can be calculated - let total_width = x - gap.x; - - // This allows the horizontal lines to be laid out - for line in hline.0 { - let real_line = - if line < 0 { nrows - line.unsigned_abs() } else { line as usize }; - let offset = (heights[0..real_line].iter().map(|&(a, b)| a + b).sum::<Abs>() - + gap.y * (real_line - 1) as f64) - + half_gap.y; - - frame.push( - Point::with_y(offset), - line_item(total_width, false, stroke.clone(), span), - ); - } - - frame.size_mut().x = total_width; - - Ok(frame) -} - -fn line_item(length: Abs, vertical: bool, stroke: FixedStroke, span: Span) -> FrameItem { - let line_geom = if vertical { - Geometry::Line(Point::with_y(length)) - } else { - Geometry::Line(Point::with_x(length)) - }; - - FrameItem::Shape( - Shape { - geometry: line_geom, - fill: None, - stroke: Some(stroke), - }, - span, - ) -} - -/// Layout the outer wrapper around the body of a vector or matrix. -fn layout_delimiters( - ctx: &mut MathContext, - mut frame: Frame, - left: Option<char>, - right: Option<char>, - span: Span, -) -> SourceResult<()> { - let axis = scaled!(ctx, axis_height); - let short_fall = DELIM_SHORT_FALL.scaled(ctx); - let height = frame.height(); - let target = height + VERTICAL_PADDING.of(height); - frame.set_baseline(height / 2.0 + axis); - - if let Some(left) = left { - let mut left = - GlyphFragment::new(ctx, left, span).stretch_vertical(ctx, target, short_fall); - left.center_on_axis(ctx); - ctx.push(left); - } - - ctx.push(FrameFragment::new(ctx, frame)); - - if let Some(right) = right { - let mut right = GlyphFragment::new(ctx, right, span) - .stretch_vertical(ctx, target, short_fall); - right.center_on_axis(ctx); - ctx.push(right); - } - - Ok(()) -} - -/// Parameters specifying how augmentation lines -/// should be drawn on a matrix. -#[derive(Debug, Default, Clone, PartialEq, Hash)] -pub struct Augment<T: Numeric = Length> { - pub hline: Offsets, - pub vline: Offsets, - pub stroke: Smart<Stroke<T>>, -} - -impl Augment<Abs> { - fn stroke_or(&self, fallback: FixedStroke) -> FixedStroke { - match &self.stroke { - Smart::Custom(v) => v.clone().unwrap_or(fallback), - Smart::Auto => fallback, - } - } -} - -impl Resolve for Augment { - type Output = Augment<Abs>; - - fn resolve(self, styles: StyleChain) -> Self::Output { - Augment { - hline: self.hline, - vline: self.vline, - stroke: self.stroke.resolve(styles), - } - } -} - -impl Fold for Augment<Abs> { - type Output = Augment<Abs>; - - fn fold(mut self, outer: Self::Output) -> Self::Output { - // Special case for handling `auto` strokes in subsequent `Augment`. - if self.stroke.is_auto() && outer.stroke.is_custom() { - self.stroke = outer.stroke; - } else { - self.stroke = self.stroke.fold(outer.stroke); - } - - self - } -} - -cast! { - Augment, - self => { - // if the stroke is auto and there is only one vertical line, - if self.stroke.is_auto() && self.hline.0.is_empty() && self.vline.0.len() == 1 { - return self.vline.0[0].into_value(); - } - - let d = dict! { - "hline" => self.hline.into_value(), - "vline" => self.vline.into_value(), - "stroke" => self.stroke.into_value() - }; - - d.into_value() - }, - v: isize => Augment { - hline: Offsets::default(), - vline: Offsets(smallvec![v]), - stroke: Smart::Auto, - }, - mut dict: Dict => { - // need the transpose for the defaults to work - let hline = dict.take("hline").ok().map(Offsets::from_value) - .transpose().unwrap_or_default().unwrap_or_default(); - let vline = dict.take("vline").ok().map(Offsets::from_value) - .transpose().unwrap_or_default().unwrap_or_default(); - - let stroke = dict.take("stroke").ok().map(Stroke::from_value) - .transpose()?.map(Smart::Custom).unwrap_or(Smart::Auto); - - Augment { hline, vline, stroke } - }, -} - -cast! { - Augment<Abs>, - self => self.into_value(), -} - -/// The offsets at which augmentation lines should be drawn on a matrix. -#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] -pub struct Offsets(SmallVec<[isize; 1]>); - -cast! { - Offsets, - self => self.0.into_value(), - v: isize => Self(smallvec![v]), - v: Array => Self(v.into_iter().map(Value::cast).collect::<StrResult<_>>()?), -} diff --git a/crates/typst-library/src/math/mod.rs b/crates/typst-library/src/math/mod.rs deleted file mode 100644 index 7ced638b..00000000 --- a/crates/typst-library/src/math/mod.rs +++ /dev/null @@ -1,500 +0,0 @@ -//! Mathematical formulas. - -#[macro_use] -mod ctx; -mod accent; -mod align; -mod attach; -mod cancel; -mod class; -mod frac; -mod fragment; -mod lr; -mod matrix; -mod op; -mod root; -mod row; -mod spacing; -mod stretch; -mod style; -mod underover; - -pub use self::accent::*; -pub use self::align::*; -pub use self::attach::*; -pub use self::cancel::*; -pub use self::class::*; -pub use self::frac::*; -pub use self::lr::*; -pub use self::matrix::*; -pub use self::op::*; -pub use self::root::*; -pub use self::style::*; -pub use self::underover::*; - -use std::borrow::Cow; - -use ttf_parser::{GlyphId, Rect}; -use typst::eval::{Module, Scope}; -use typst::font::{Font, FontWeight}; -use typst::model::Guard; -use typst::util::option_eq; -use unicode_math_class::MathClass; - -use self::ctx::*; -use self::fragment::*; -use self::row::*; -use self::spacing::*; -use crate::layout::{AlignElem, BoxElem, HElem, ParElem, Spacing}; -use crate::meta::{ - Count, Counter, CounterUpdate, LocalNameIn, Numbering, Outlinable, Refable, - Supplement, -}; -use crate::prelude::*; -use crate::shared::BehavedBuilder; -use crate::text::{ - families, variant, FontFamily, FontList, LinebreakElem, SpaceElem, TextElem, TextSize, -}; - -/// Create a module with all math definitions. -pub fn module() -> Module { - let mut math = Scope::deduplicating(); - math.category("math"); - math.define_elem::<EquationElem>(); - math.define_elem::<TextElem>(); - math.define_elem::<LrElem>(); - math.define_elem::<AttachElem>(); - math.define_elem::<ScriptsElem>(); - math.define_elem::<LimitsElem>(); - math.define_elem::<AccentElem>(); - math.define_elem::<UnderlineElem>(); - math.define_elem::<OverlineElem>(); - math.define_elem::<UnderbraceElem>(); - math.define_elem::<OverbraceElem>(); - math.define_elem::<UnderbracketElem>(); - math.define_elem::<OverbracketElem>(); - math.define_elem::<CancelElem>(); - math.define_elem::<FracElem>(); - math.define_elem::<BinomElem>(); - math.define_elem::<VecElem>(); - math.define_elem::<MatElem>(); - math.define_elem::<CasesElem>(); - math.define_elem::<RootElem>(); - math.define_elem::<ClassElem>(); - math.define_elem::<OpElem>(); - math.define_func::<abs>(); - math.define_func::<norm>(); - math.define_func::<floor>(); - math.define_func::<ceil>(); - math.define_func::<round>(); - math.define_func::<sqrt>(); - math.define_func::<upright>(); - math.define_func::<bold>(); - math.define_func::<italic>(); - math.define_func::<serif>(); - math.define_func::<sans>(); - math.define_func::<cal>(); - math.define_func::<frak>(); - math.define_func::<mono>(); - math.define_func::<bb>(); - math.define_func::<display>(); - math.define_func::<inline>(); - math.define_func::<script>(); - math.define_func::<sscript>(); - - // Text operators, spacings, and symbols. - op::define(&mut math); - spacing::define(&mut math); - for (name, symbol) in crate::symbols::SYM { - math.define(*name, symbol.clone()); - } - - Module::new("math", math) -} - -/// A mathematical equation. -/// -/// Can be displayed inline with text or as a separate block. -/// -/// # Example -/// ```example -/// #set text(font: "New Computer Modern") -/// -/// Let $a$, $b$, and $c$ be the side -/// lengths of right-angled triangle. -/// Then, we know that: -/// $ a^2 + b^2 = c^2 $ -/// -/// Prove by induction: -/// $ sum_(k=1)^n k = (n(n+1)) / 2 $ -/// ``` -/// -/// # Syntax -/// This function also has dedicated syntax: Write mathematical markup within -/// dollar signs to create an equation. Starting and ending the equation with at -/// least one space lifts it into a separate block that is centered -/// horizontally. For more details about math syntax, see the -/// [main math page]($category/math). -#[elem( - Locatable, Synthesize, Show, Finalize, Layout, LayoutMath, Count, LocalName, Refable, - Outlinable -)] -pub struct EquationElem { - /// Whether the equation is displayed as a separate block. - #[default(false)] - pub block: bool, - - /// How to [number]($numbering) block-level equations. - /// - /// ```example - /// #set math.equation(numbering: "(1)") - /// - /// We define: - /// $ phi.alt := (1 + sqrt(5)) / 2 $ <ratio> - /// - /// With @ratio, we get: - /// $ F_n = floor(1 / sqrt(5) phi.alt^n) $ - /// ``` - pub numbering: Option<Numbering>, - - /// A supplement for the equation. - /// - /// For references to equations, this is added before the referenced number. - /// - /// If a function is specified, it is passed the referenced equation and - /// should return content. - /// - /// ```example - /// #set math.equation(numbering: "(1)", supplement: [Eq.]) - /// - /// We define: - /// $ phi.alt := (1 + sqrt(5)) / 2 $ <ratio> - /// - /// With @ratio, we get: - /// $ F_n = floor(1 / sqrt(5) phi.alt^n) $ - /// ``` - pub supplement: Smart<Option<Supplement>>, - - /// The contents of the equation. - #[required] - pub body: Content, -} - -impl Synthesize for EquationElem { - fn synthesize(&mut self, vt: &mut Vt, styles: StyleChain) -> SourceResult<()> { - // Resolve the supplement. - let supplement = match self.supplement(styles) { - Smart::Auto => TextElem::packed(Self::local_name_in(styles)), - Smart::Custom(None) => Content::empty(), - Smart::Custom(Some(supplement)) => supplement.resolve(vt, [self.clone()])?, - }; - - self.push_block(self.block(styles)); - self.push_numbering(self.numbering(styles)); - self.push_supplement(Smart::Custom(Some(Supplement::Content(supplement)))); - - Ok(()) - } -} - -impl Show for EquationElem { - #[tracing::instrument(name = "EquationElem::show", skip_all)] - fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> { - let mut realized = self.clone().pack().guarded(Guard::Base(Self::elem())); - if self.block(styles) { - realized = AlignElem::new(realized).pack(); - } - Ok(realized) - } -} - -impl Finalize for EquationElem { - fn finalize(&self, realized: Content, style: StyleChain) -> Content { - let mut realized = realized; - if self.block(style) { - realized = realized.styled(AlignElem::set_alignment(Align::CENTER)); - } - realized - .styled(TextElem::set_weight(FontWeight::from_number(450))) - .styled(TextElem::set_font(FontList(vec![FontFamily::new( - "New Computer Modern Math", - )]))) - } -} - -impl Layout for EquationElem { - #[tracing::instrument(name = "EquationElem::layout", skip_all)] - fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment> { - const NUMBER_GUTTER: Em = Em::new(0.5); - - let block = self.block(styles); - - // Find a math font. - let variant = variant(styles); - let world = vt.world; - let Some(font) = families(styles).find_map(|family| { - let id = world.book().select(family, variant)?; - let font = world.font(id)?; - let _ = font.ttf().tables().math?.constants?; - Some(font) - }) else { - bail!(self.span(), "current font does not support math"); - }; - - let mut ctx = MathContext::new(vt, styles, regions, &font, block); - let mut frame = ctx.layout_frame(self)?; - - if block { - if let Some(numbering) = self.numbering(styles) { - let pod = Regions::one(regions.base(), Axes::splat(false)); - let counter = Counter::of(Self::elem()) - .display(Some(numbering), false) - .layout(vt, styles, pod)? - .into_frame(); - - let full_counter_width = counter.width() + NUMBER_GUTTER.resolve(styles); - let width = if regions.size.x.is_finite() { - regions.size.x - } else { - frame.width() + 2.0 * full_counter_width - }; - - let height = frame.height().max(counter.height()); - let align = AlignElem::alignment_in(styles).resolve(styles).x; - frame.resize(Size::new(width, height), Axes::splat(align)); - - let dir = TextElem::dir_in(styles); - let offset = match (align, dir) { - (FixedAlign::Start, Dir::RTL) => full_counter_width, - (FixedAlign::End, Dir::LTR) => -full_counter_width, - _ => Abs::zero(), - }; - frame.translate(Point::with_x(offset)); - - let x = if dir.is_positive() { - frame.width() - counter.width() - } else { - Abs::zero() - }; - let y = (frame.height() - counter.height()) / 2.0; - - frame.push_frame(Point::new(x, y), counter) - } - } else { - let font_size = TextElem::size_in(styles); - let slack = ParElem::leading_in(styles) * 0.7; - let top_edge = TextElem::top_edge_in(styles).resolve(font_size, &font, None); - let bottom_edge = - -TextElem::bottom_edge_in(styles).resolve(font_size, &font, None); - - let ascent = top_edge.max(frame.ascent() - slack); - let descent = bottom_edge.max(frame.descent() - slack); - frame.translate(Point::with_y(ascent - frame.baseline())); - frame.size_mut().y = ascent + descent; - } - - // Apply metadata. - frame.meta(styles, false); - - Ok(Fragment::frame(frame)) - } -} - -impl Count for EquationElem { - fn update(&self) -> Option<CounterUpdate> { - (self.block(StyleChain::default()) - && self.numbering(StyleChain::default()).is_some()) - .then(|| CounterUpdate::Step(NonZeroUsize::ONE)) - } -} - -impl LocalName for EquationElem { - fn local_name(lang: Lang, region: Option<Region>) -> &'static str { - match lang { - Lang::ALBANIAN => "Ekuacion", - Lang::ARABIC => "معادلة", - Lang::BOKMÅL => "Ligning", - Lang::CHINESE if option_eq(region, "TW") => "方程式", - Lang::CHINESE => "公式", - Lang::CZECH => "Rovnice", - Lang::DANISH => "Ligning", - Lang::DUTCH => "Vergelijking", - Lang::FILIPINO => "Ekwasyon", - Lang::FINNISH => "Yhtälö", - Lang::FRENCH => "Équation", - Lang::GERMAN => "Gleichung", - Lang::GREEK => "Εξίσωση", - Lang::HUNGARIAN => "Egyenlet", - Lang::ITALIAN => "Equazione", - Lang::NYNORSK => "Likning", - Lang::POLISH => "Równanie", - Lang::PORTUGUESE => "Equação", - Lang::ROMANIAN => "Ecuația", - Lang::RUSSIAN => "Уравнение", - Lang::SLOVENIAN => "Enačba", - Lang::SPANISH => "Ecuación", - Lang::SWEDISH => "Ekvation", - Lang::TURKISH => "Denklem", - Lang::UKRAINIAN => "Рівняння", - Lang::VIETNAMESE => "Phương trình", - Lang::JAPANESE => "式", - Lang::ENGLISH | _ => "Equation", - } - } -} - -impl Refable for EquationElem { - fn supplement(&self) -> Content { - // After synthesis, this should always be custom content. - match self.supplement(StyleChain::default()) { - Smart::Custom(Some(Supplement::Content(content))) => content, - _ => Content::empty(), - } - } - - fn counter(&self) -> Counter { - Counter::of(Self::elem()) - } - - fn numbering(&self) -> Option<Numbering> { - self.numbering(StyleChain::default()) - } -} - -impl Outlinable for EquationElem { - fn outline(&self, vt: &mut Vt) -> SourceResult<Option<Content>> { - if !self.block(StyleChain::default()) { - return Ok(None); - } - let Some(numbering) = self.numbering(StyleChain::default()) else { - return Ok(None); - }; - - // After synthesis, this should always be custom content. - let mut supplement = match self.supplement(StyleChain::default()) { - Smart::Custom(Some(Supplement::Content(content))) => content, - _ => Content::empty(), - }; - - if !supplement.is_empty() { - supplement += TextElem::packed("\u{a0}"); - } - - let numbers = self - .counter() - .at(vt, self.location().unwrap())? - .display(vt, &numbering)?; - - Ok(Some(supplement + numbers)) - } -} - -pub trait LayoutMath { - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()>; -} - -impl LayoutMath for EquationElem { - #[tracing::instrument(skip(ctx))] - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - self.body().layout_math(ctx) - } -} - -impl LayoutMath for Content { - #[tracing::instrument(skip(ctx))] - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - // Directly layout the body of nested equations instead of handling it - // like a normal equation so that things like this work: - // ``` - // #let my = $pi$ - // $ my r^2 $ - // ``` - if let Some(elem) = self.to::<EquationElem>() { - return elem.layout_math(ctx); - } - - if let Some(realized) = ctx.realize(self)? { - return realized.layout_math(ctx); - } - - if self.is_sequence() { - let mut bb = BehavedBuilder::new(); - self.sequence_recursive_for_each(&mut |child: &Content| { - bb.push(Cow::Owned(child.clone()), StyleChain::default()) - }); - - for (child, _) in bb.finish().0.iter() { - child.layout_math(ctx)?; - } - return Ok(()); - } - - if let Some((elem, styles)) = self.to_styled() { - if TextElem::font_in(ctx.styles().chain(styles)) - != TextElem::font_in(ctx.styles()) - { - let frame = ctx.layout_content(self)?; - ctx.push(FrameFragment::new(ctx, frame).with_spaced(true)); - return Ok(()); - } - - let prev_map = std::mem::replace(&mut ctx.local, styles.clone()); - let prev_size = ctx.size; - ctx.local.apply(prev_map.clone()); - ctx.size = TextElem::size_in(ctx.styles()); - elem.layout_math(ctx)?; - ctx.size = prev_size; - ctx.local = prev_map; - return Ok(()); - } - - if self.is::<SpaceElem>() { - ctx.push(MathFragment::Space(ctx.space_width.scaled(ctx))); - return Ok(()); - } - - if self.is::<LinebreakElem>() { - ctx.push(MathFragment::Linebreak); - return Ok(()); - } - - if let Some(elem) = self.to::<HElem>() { - if let Spacing::Rel(rel) = elem.amount() { - if rel.rel.is_zero() { - ctx.push(MathFragment::Spacing(rel.abs.resolve(ctx.styles()))); - } - } - return Ok(()); - } - - if let Some(elem) = self.to::<TextElem>() { - let fragment = ctx.layout_text(elem)?; - ctx.push(fragment); - return Ok(()); - } - - if let Some(boxed) = self.to::<BoxElem>() { - let frame = ctx.layout_box(boxed)?; - ctx.push(FrameFragment::new(ctx, frame).with_spaced(true)); - return Ok(()); - } - - if let Some(elem) = self.with::<dyn LayoutMath>() { - return elem.layout_math(ctx); - } - - let mut frame = ctx.layout_content(self)?; - if !frame.has_baseline() { - let axis = scaled!(ctx, axis_height); - frame.set_baseline(frame.height() / 2.0 + axis); - } - ctx.push(FrameFragment::new(ctx, frame).with_spaced(true)); - - Ok(()) - } -} diff --git a/crates/typst-library/src/math/op.rs b/crates/typst-library/src/math/op.rs deleted file mode 100644 index 9e35d207..00000000 --- a/crates/typst-library/src/math/op.rs +++ /dev/null @@ -1,115 +0,0 @@ -use super::*; - -/// A text operator in an equation. -/// -/// # Example -/// ```example -/// $ tan x = (sin x)/(cos x) $ -/// $ op("custom", -/// limits: #true)_(n->oo) n $ -/// ``` -/// -/// # Predefined Operators { #predefined } -/// Typst predefines the operators `arccos`, `arcsin`, `arctan`, `arg`, `cos`, -/// `cosh`, `cot`, `coth`, `csc`, `csch`, `ctg`, `deg`, `det`, `dim`, `exp`, -/// `gcd`, `hom`, `id`, `im`, `inf`, `ker`, `lg`, `lim`, `liminf`, `limsup`, -/// `ln`, `log`, `max`, `min`, `mod`, `Pr`, `sec`, `sech`, `sin`, `sinc`, -/// `sinh`, `sup`, `tan`, `tanh`, `tg` and `tr`. -#[elem(title = "Text Operator", LayoutMath)] -pub struct OpElem { - /// The operator's text. - #[required] - pub text: Content, - - /// Whether the operator should show attachments as limits in display mode. - #[default(false)] - pub limits: bool, -} - -impl LayoutMath for OpElem { - #[tracing::instrument(skip(ctx))] - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - let fragment = ctx.layout_fragment(self.text())?; - ctx.push( - FrameFragment::new(ctx, fragment.into_frame()) - .with_class(MathClass::Large) - .with_limits(if self.limits(ctx.styles()) { - Limits::Display - } else { - Limits::Never - }), - ); - Ok(()) - } -} - -macro_rules! ops { - ($($name:ident $(: $value:literal)? $(($tts:tt))?),* $(,)?) => { - pub(super) fn define(math: &mut Scope) { - $({ - let operator = EcoString::from(ops!(@name $name $(: $value)?)); - math.define( - stringify!($name), - OpElem::new(TextElem::new(operator).into()) - .with_limits(ops!(@limit $($tts)*)) - .pack() - ); - })* - - let dif = |d| { - HElem::new(THIN.into()).with_weak(true).pack() - + MathStyleElem::new(TextElem::packed(d)).with_italic(Some(false)).pack() - }; - math.define("dif", dif('d')); - math.define("Dif", dif('D')); - } - }; - (@name $name:ident) => { stringify!($name) }; - (@name $name:ident: $value:literal) => { $value }; - (@limit limits) => { true }; - (@limit) => { false }; -} - -ops! { - arccos, - arcsin, - arctan, - arg, - cos, - cosh, - cot, - coth, - csc, - csch, - ctg, - deg, - det (limits), - dim, - exp, - gcd (limits), - hom, - id, - im, - inf (limits), - ker, - lg, - lim (limits), - liminf: "lim inf" (limits), - limsup: "lim sup" (limits), - ln, - log, - max (limits), - min (limits), - mod, - Pr (limits), - sec, - sech, - sin, - sinc, - sinh, - sup (limits), - tan, - tanh, - tg, - tr, -} diff --git a/crates/typst-library/src/math/root.rs b/crates/typst-library/src/math/root.rs deleted file mode 100644 index ba918ea9..00000000 --- a/crates/typst-library/src/math/root.rs +++ /dev/null @@ -1,137 +0,0 @@ -use super::*; - -/// A square root. -/// -/// ```example -/// $ sqrt(3 - 2 sqrt(2)) = sqrt(2) - 1 $ -/// ``` -#[func(title = "Square Root")] -pub fn sqrt( - /// The expression to take the square root of. - radicand: Content, -) -> Content { - RootElem::new(radicand).pack() -} - -/// A general root. -/// -/// ```example -/// $ root(3, x) $ -/// ``` -#[elem(LayoutMath)] -pub struct RootElem { - /// Which root of the radicand to take. - #[positional] - pub index: Option<Content>, - - /// The expression to take the root of. - #[required] - pub radicand: Content, -} - -impl LayoutMath for RootElem { - #[tracing::instrument(skip(ctx))] - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - layout(ctx, self.index(ctx.styles()).as_ref(), self.radicand(), self.span()) - } -} - -/// Layout a root. -/// -/// TeXbook page 443, page 360 -/// See also: https://www.w3.org/TR/mathml-core/#radicals-msqrt-mroot -fn layout( - ctx: &mut MathContext, - index: Option<&Content>, - radicand: &Content, - span: Span, -) -> SourceResult<()> { - let gap = scaled!( - ctx, - text: radical_vertical_gap, - display: radical_display_style_vertical_gap, - ); - let thickness = scaled!(ctx, radical_rule_thickness); - let extra_ascender = scaled!(ctx, radical_extra_ascender); - let kern_before = scaled!(ctx, radical_kern_before_degree); - let kern_after = scaled!(ctx, radical_kern_after_degree); - let raise_factor = percent!(ctx, radical_degree_bottom_raise_percent); - - // Layout radicand. - ctx.style(ctx.style.with_cramped(true)); - let radicand = ctx.layout_frame(radicand)?; - ctx.unstyle(); - - // Layout root symbol. - let target = radicand.height() + thickness + gap; - let sqrt = GlyphFragment::new(ctx, '√', span) - .stretch_vertical(ctx, target, Abs::zero()) - .frame; - - // Layout the index. - ctx.style(ctx.style.with_size(MathSize::ScriptScript)); - let index = index.map(|elem| ctx.layout_frame(elem)).transpose()?; - ctx.unstyle(); - - // TeXbook, page 443, item 11 - // Keep original gap, and then distribute any remaining free space - // equally above and below. - let gap = gap.max((sqrt.height() - thickness - radicand.height() + gap) / 2.0); - - let sqrt_ascent = radicand.ascent() + gap + thickness; - let descent = sqrt.height() - sqrt_ascent; - let inner_ascent = sqrt_ascent + extra_ascender; - - let mut sqrt_offset = Abs::zero(); - let mut shift_up = Abs::zero(); - let mut ascent = inner_ascent; - - if let Some(index) = &index { - sqrt_offset = kern_before + index.width() + kern_after; - // The formula below for how much raise the index by comes from - // the TeXbook, page 360, in the definition of `\root`. - // However, the `+ index.descent()` part is different from TeX. - // Without it, descenders can collide with the surd, a rarity - // in practice, but possible. MS Word also adjusts index positions - // for descenders. - shift_up = raise_factor * (inner_ascent - descent) + index.descent(); - ascent.set_max(shift_up + index.ascent()); - } - - let radicand_x = sqrt_offset + sqrt.width(); - let radicand_y = ascent - radicand.ascent(); - let width = radicand_x + radicand.width(); - let size = Size::new(width, ascent + descent); - - // The extra "- thickness" comes from the fact that the sqrt is placed - // in `push_frame` with respect to its top, not its baseline. - let sqrt_pos = Point::new(sqrt_offset, radicand_y - gap - thickness); - let line_pos = Point::new(radicand_x, radicand_y - gap - (thickness / 2.0)); - let radicand_pos = Point::new(radicand_x, radicand_y); - - let mut frame = Frame::soft(size); - frame.set_baseline(ascent); - - if let Some(index) = index { - let index_pos = Point::new(kern_before, ascent - index.ascent() - shift_up); - frame.push_frame(index_pos, index); - } - - frame.push_frame(sqrt_pos, sqrt); - frame.push( - line_pos, - FrameItem::Shape( - Geometry::Line(Point::with_x(radicand.width())).stroked(FixedStroke { - paint: TextElem::fill_in(ctx.styles()).as_decoration(), - thickness, - ..FixedStroke::default() - }), - span, - ), - ); - - frame.push_frame(radicand_pos, radicand); - ctx.push(FrameFragment::new(ctx, frame)); - - Ok(()) -} diff --git a/crates/typst-library/src/math/row.rs b/crates/typst-library/src/math/row.rs deleted file mode 100644 index 70813598..00000000 --- a/crates/typst-library/src/math/row.rs +++ /dev/null @@ -1,261 +0,0 @@ -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).resolve(styles).x; - 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: FixedAlign, - ) -> Frame { - if !self.iter().any(|frag| matches!(frag, MathFragment::Linebreak)) { - return self.into_line_frame(points, align); - } - - 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::soft(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 - } - - fn into_line_frame(self, points: &[Abs], align: FixedAlign) -> 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 != FixedAlign::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 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 { - FixedAlign::Start => prev_points.next(), - FixedAlign::End => { - 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 - } -} diff --git a/crates/typst-library/src/math/spacing.rs b/crates/typst-library/src/math/spacing.rs deleted file mode 100644 index 3dfce024..00000000 --- a/crates/typst-library/src/math/spacing.rs +++ /dev/null @@ -1,63 +0,0 @@ -use super::*; - -pub(super) const THIN: Em = Em::new(1.0 / 6.0); -pub(super) const MEDIUM: Em = Em::new(2.0 / 9.0); -pub(super) const THICK: Em = Em::new(5.0 / 18.0); -pub(super) const QUAD: Em = Em::new(1.0); -pub(super) const WIDE: Em = Em::new(2.0); - -/// Hook up all spacings. -pub(super) fn define(math: &mut Scope) { - math.define("thin", HElem::new(THIN.into()).pack()); - math.define("med", HElem::new(MEDIUM.into()).pack()); - math.define("thick", HElem::new(THICK.into()).pack()); - math.define("quad", HElem::new(QUAD.into()).pack()); - math.define("wide", HElem::new(WIDE.into()).pack()); -} - -/// Create the spacing between two fragments in a given style. -pub(super) fn spacing( - l: &MathFragment, - space: Option<MathFragment>, - r: &MathFragment, -) -> Option<MathFragment> { - use MathClass::*; - - let class = |f: &MathFragment| f.class().unwrap_or(Special); - let resolve = |v: Em, f: &MathFragment| { - Some(MathFragment::Spacing(f.font_size().map_or(Abs::zero(), |size| v.at(size)))) - }; - let script = - |f: &MathFragment| f.style().map_or(false, |s| s.size <= MathSize::Script); - - match (class(l), class(r)) { - // 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, - } -} diff --git a/crates/typst-library/src/math/stretch.rs b/crates/typst-library/src/math/stretch.rs deleted file mode 100644 index e9bf6890..00000000 --- a/crates/typst-library/src/math/stretch.rs +++ /dev/null @@ -1,199 +0,0 @@ -use ttf_parser::math::{GlyphAssembly, GlyphConstruction, GlyphPart}; -use ttf_parser::LazyArray16; - -use super::*; - -/// Maximum number of times extenders can be repeated. -const MAX_REPEATS: usize = 1024; - -impl GlyphFragment { - /// Try to stretch a glyph to a desired height. - pub fn stretch_vertical( - self, - ctx: &MathContext, - height: Abs, - short_fall: Abs, - ) -> VariantFragment { - stretch_glyph(ctx, self, height, short_fall, false) - } - - /// Try to stretch a glyph to a desired width. - pub fn stretch_horizontal( - self, - ctx: &MathContext, - width: Abs, - short_fall: Abs, - ) -> VariantFragment { - stretch_glyph(ctx, self, width, short_fall, true) - } -} - -/// Try to stretch a glyph to a desired width or height. -/// -/// The resulting frame may not have the exact desired width. -fn stretch_glyph( - ctx: &MathContext, - mut base: GlyphFragment, - target: Abs, - short_fall: Abs, - horizontal: bool, -) -> VariantFragment { - let short_target = target - short_fall; - let mut min_overlap = Abs::zero(); - let construction = ctx - .table - .variants - .and_then(|variants| { - min_overlap = variants.min_connector_overlap.scaled(ctx); - if horizontal { - variants.horizontal_constructions - } else { - variants.vertical_constructions - } - .get(base.id) - }) - .unwrap_or(GlyphConstruction { assembly: None, variants: LazyArray16::new(&[]) }); - - // If the base glyph is good enough, use it. - let advance = if horizontal { base.width } else { base.height() }; - if short_target <= advance { - return base.into_variant(); - } - - // Search for a pre-made variant with a good advance. - let mut best_id = base.id; - let mut best_advance = base.width; - for variant in construction.variants { - best_id = variant.variant_glyph; - best_advance = base.font.to_em(variant.advance_measurement).at(base.font_size); - if short_target <= best_advance { - break; - } - } - - // This is either good or the best we've got. - if short_target <= best_advance || construction.assembly.is_none() { - base.set_id(ctx, best_id); - return base.into_variant(); - } - - // Assemble from parts. - let assembly = construction.assembly.unwrap(); - assemble(ctx, base, assembly, min_overlap, target, horizontal) -} - -/// Assemble a glyph from parts. -fn assemble( - ctx: &MathContext, - base: GlyphFragment, - assembly: GlyphAssembly, - min_overlap: Abs, - target: Abs, - horizontal: bool, -) -> VariantFragment { - // Determine the number of times the extenders need to be repeated as well - // as a ratio specifying how much to spread the parts apart - // (0 = maximal overlap, 1 = minimal overlap). - let mut full; - let mut ratio; - let mut repeat = 0; - loop { - full = Abs::zero(); - ratio = 0.0; - - let mut parts = parts(assembly, repeat).peekable(); - let mut growable = Abs::zero(); - - while let Some(part) = parts.next() { - let mut advance = part.full_advance.scaled(ctx); - if let Some(next) = parts.peek() { - let max_overlap = part - .end_connector_length - .min(next.start_connector_length) - .scaled(ctx); - - advance -= max_overlap; - growable += max_overlap - min_overlap; - } - - full += advance; - } - - if full < target { - let delta = target - full; - ratio = (delta / growable).min(1.0); - full += ratio * growable; - } - - if target <= full || repeat >= MAX_REPEATS { - break; - } - - repeat += 1; - } - - let mut selected = vec![]; - let mut parts = parts(assembly, repeat).peekable(); - while let Some(part) = parts.next() { - let mut advance = part.full_advance.scaled(ctx); - if let Some(next) = parts.peek() { - let max_overlap = - part.end_connector_length.min(next.start_connector_length).scaled(ctx); - advance -= max_overlap; - advance += ratio * (max_overlap - min_overlap); - } - - let mut fragment = base.clone(); - fragment.set_id(ctx, part.glyph_id); - selected.push((fragment, advance)); - } - - let size; - let baseline; - if horizontal { - let height = base.ascent + base.descent; - size = Size::new(full, height); - baseline = base.ascent; - } else { - let axis = scaled!(ctx, axis_height); - let width = selected.iter().map(|(f, _)| f.width).max().unwrap_or_default(); - size = Size::new(width, full); - baseline = full / 2.0 + axis; - } - - let mut frame = Frame::soft(size); - let mut offset = Abs::zero(); - frame.set_baseline(baseline); - frame.meta_iter(base.meta); - - for (fragment, advance) in selected { - let pos = if horizontal { - Point::new(offset, frame.baseline() - fragment.ascent) - } else { - Point::with_y(full - offset - fragment.height()) - }; - frame.push_frame(pos, fragment.into_frame()); - offset += advance; - } - - VariantFragment { - c: base.c, - id: None, - frame, - style: base.style, - font_size: base.font_size, - italics_correction: Abs::zero(), - class: base.class, - span: base.span, - limits: base.limits, - } -} - -/// Return an iterator over the assembly's parts with extenders repeated the -/// specified number of times. -fn parts(assembly: GlyphAssembly, repeat: usize) -> impl Iterator<Item = GlyphPart> + '_ { - assembly.parts.into_iter().flat_map(move |part| { - let count = if part.part_flags.extender() { repeat } else { 1 }; - std::iter::repeat(part).take(count) - }) -} diff --git a/crates/typst-library/src/math/style.rs b/crates/typst-library/src/math/style.rs deleted file mode 100644 index 774fadac..00000000 --- a/crates/typst-library/src/math/style.rs +++ /dev/null @@ -1,574 +0,0 @@ -use super::*; - -/// Bold font style in math. -/// -/// ```example -/// $ bold(A) := B^+ $ -/// ``` -#[func] -pub fn bold( - /// The content to style. - body: Content, -) -> Content { - MathStyleElem::new(body).with_bold(Some(true)).pack() -} - -/// Upright (non-italic) font style in math. -/// -/// ```example -/// $ upright(A) != A $ -/// ``` -#[func] -pub fn upright( - /// The content to style. - body: Content, -) -> Content { - MathStyleElem::new(body).with_italic(Some(false)).pack() -} - -/// Italic font style in math. -/// -/// For roman letters and greek lowercase letters, this is already the default. -#[func] -pub fn italic( - /// The content to style. - body: Content, -) -> Content { - MathStyleElem::new(body).with_italic(Some(true)).pack() -} -/// Serif (roman) font style in math. -/// -/// This is already the default. -#[func] -pub fn serif( - /// The content to style. - body: Content, -) -> Content { - MathStyleElem::new(body).with_variant(Some(MathVariant::Serif)).pack() -} - -/// Sans-serif font style in math. -/// -/// ```example -/// $ sans(A B C) $ -/// ``` -#[func(title = "Sans Serif")] -pub fn sans( - /// The content to style. - body: Content, -) -> Content { - MathStyleElem::new(body).with_variant(Some(MathVariant::Sans)).pack() -} - -/// Calligraphic font style in math. -/// -/// ```example -/// Let $cal(P)$ be the set of ... -/// ``` -#[func(title = "Calligraphic")] -pub fn cal( - /// The content to style. - body: Content, -) -> Content { - MathStyleElem::new(body).with_variant(Some(MathVariant::Cal)).pack() -} - -/// Fraktur font style in math. -/// -/// ```example -/// $ frak(P) $ -/// ``` -#[func(title = "Fraktur")] -pub fn frak( - /// The content to style. - body: Content, -) -> Content { - MathStyleElem::new(body).with_variant(Some(MathVariant::Frak)).pack() -} - -/// Monospace font style in math. -/// -/// ```example -/// $ mono(x + y = z) $ -/// ``` -#[func(title = "Monospace")] -pub fn mono( - /// The content to style. - body: Content, -) -> Content { - MathStyleElem::new(body).with_variant(Some(MathVariant::Mono)).pack() -} - -/// Blackboard bold (double-struck) font style in math. -/// -/// For uppercase latin letters, blackboard bold is additionally available -/// through [symbols]($category/symbols/sym) of the form `NN` and `RR`. -/// -/// ```example -/// $ bb(b) $ -/// $ bb(N) = NN $ -/// $ f: NN -> RR $ -/// ``` -#[func(title = "Blackboard Bold")] -pub fn bb( - /// The content to style. - body: Content, -) -> Content { - MathStyleElem::new(body).with_variant(Some(MathVariant::Bb)).pack() -} - -/// Forced display style in math. -/// -/// This is the normal size for block equations. -/// -/// ```example -/// $sum_i x_i/2 = display(sum_i x_i/2)$ -/// ``` -#[func(title = "Display Size")] -pub fn display( - /// The content to size. - body: Content, - /// Whether to impose a height restriction for exponents, like regular sub- - /// and superscripts do. - #[named] - #[default(false)] - cramped: bool, -) -> Content { - MathStyleElem::new(body) - .with_size(Some(MathSize::Display)) - .with_cramped(Some(cramped)) - .pack() -} - -/// Forced inline (text) style in math. -/// -/// This is the normal size for inline equations. -/// -/// ```example -/// $ sum_i x_i/2 -/// = inline(sum_i x_i/2) $ -/// ``` -#[func(title = "Inline Size")] -pub fn inline( - /// The content to size. - body: Content, - /// Whether to impose a height restriction for exponents, like regular sub- - /// and superscripts do. - #[named] - #[default(false)] - cramped: bool, -) -> Content { - MathStyleElem::new(body) - .with_size(Some(MathSize::Text)) - .with_cramped(Some(cramped)) - .pack() -} - -/// Forced script style in math. -/// -/// This is the smaller size used in powers or sub- or superscripts. -/// -/// ```example -/// $sum_i x_i/2 = script(sum_i x_i/2)$ -/// ``` -#[func(title = "Script Size")] -pub fn script( - /// The content to size. - body: Content, - /// Whether to impose a height restriction for exponents, like regular sub- - /// and superscripts do. - #[named] - #[default(true)] - cramped: bool, -) -> Content { - MathStyleElem::new(body) - .with_size(Some(MathSize::Script)) - .with_cramped(Some(cramped)) - .pack() -} - -/// Forced second script style in math. -/// -/// This is the smallest size, used in second-level sub- and superscripts -/// (script of the script). -/// -/// ```example -/// $sum_i x_i/2 = sscript(sum_i x_i/2)$ -/// ``` -#[func(title = "Script-Script Size")] -pub fn sscript( - /// The content to size. - body: Content, - /// Whether to impose a height restriction for exponents, like regular sub- - /// and superscripts do. - #[named] - #[default(true)] - cramped: bool, -) -> Content { - MathStyleElem::new(body) - .with_size(Some(MathSize::ScriptScript)) - .with_cramped(Some(cramped)) - .pack() -} - -/// A font variant in math. -#[elem(LayoutMath)] -pub struct MathStyleElem { - /// The content to style. - #[required] - pub body: Content, - - /// The variant to select. - pub variant: Option<MathVariant>, - - /// Whether to use bold glyphs. - pub bold: Option<bool>, - - /// Whether to use italic glyphs. - pub italic: Option<bool>, - - /// Whether to use forced size - pub size: Option<MathSize>, - - /// Whether to limit height of exponents - pub cramped: Option<bool>, -} - -impl LayoutMath for MathStyleElem { - #[tracing::instrument(skip(ctx))] - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - let mut style = ctx.style; - if let Some(variant) = self.variant(StyleChain::default()) { - style = style.with_variant(variant); - } - if let Some(bold) = self.bold(StyleChain::default()) { - style = style.with_bold(bold); - } - if let Some(italic) = self.italic(StyleChain::default()) { - style = style.with_italic(italic); - } - if let Some(size) = self.size(StyleChain::default()) { - style = style.with_size(size); - } - if let Some(cramped) = self.cramped(StyleChain::default()) { - style = style.with_cramped(cramped); - } - ctx.style(style); - self.body().layout_math(ctx)?; - ctx.unstyle(); - Ok(()) - } -} - -/// Text properties in math. -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub struct MathStyle { - /// The style variant to select. - pub variant: MathVariant, - /// The size of the glyphs. - pub size: MathSize, - /// The class of the element. - pub class: Smart<MathClass>, - /// Affects the height of exponents. - pub cramped: bool, - /// Whether to use bold glyphs. - pub bold: bool, - /// Whether to use italic glyphs. - pub italic: Smart<bool>, -} - -impl MathStyle { - /// This style, with the given `variant`. - pub fn with_variant(self, variant: MathVariant) -> Self { - Self { variant, ..self } - } - - /// This style, with the given `size`. - pub fn with_size(self, size: MathSize) -> Self { - Self { size, ..self } - } - - // This style, with the given `class`. - pub fn with_class(self, class: MathClass) -> Self { - Self { class: Smart::Custom(class), ..self } - } - - /// This style, with `cramped` set to the given value. - pub fn with_cramped(self, cramped: bool) -> Self { - Self { cramped, ..self } - } - - /// This style, with `bold` set to the given value. - pub fn with_bold(self, bold: bool) -> Self { - Self { bold, ..self } - } - - /// This style, with `italic` set to the given value. - pub fn with_italic(self, italic: bool) -> Self { - Self { italic: Smart::Custom(italic), ..self } - } - - /// The style for subscripts in the current style. - pub fn for_subscript(self) -> Self { - self.for_superscript().with_cramped(true) - } - - /// The style for superscripts in the current style. - pub fn for_superscript(self) -> Self { - self.with_size(match self.size { - MathSize::Display | MathSize::Text => MathSize::Script, - MathSize::Script | MathSize::ScriptScript => MathSize::ScriptScript, - }) - } - - /// The style for numerators in the current style. - pub fn for_numerator(self) -> Self { - self.with_size(match self.size { - MathSize::Display => MathSize::Text, - MathSize::Text => MathSize::Script, - MathSize::Script | MathSize::ScriptScript => MathSize::ScriptScript, - }) - } - - /// The style for denominators in the current style. - pub fn for_denominator(self) -> Self { - self.for_numerator().with_cramped(true) - } - - /// Apply the style to a character. - pub fn styled_char(self, c: char) -> char { - styled_char(self, c) - } -} - -/// The size of elements in an equation. -/// -/// See the TeXbook p. 141. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Cast, Hash)] -pub enum MathSize { - /// Second-level sub- and superscripts. - ScriptScript, - /// Sub- and superscripts. - Script, - /// Math in text. - Text, - /// Math on its own line. - Display, -} - -impl MathSize { - pub(super) fn factor(self, ctx: &MathContext) -> f64 { - match self { - Self::Display | Self::Text => 1.0, - Self::Script => percent!(ctx, script_percent_scale_down), - Self::ScriptScript => percent!(ctx, script_script_percent_scale_down), - } - } -} - -/// A mathematical style variant, as defined by Unicode. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Cast, Hash)] -pub enum MathVariant { - Serif, - Sans, - Cal, - Frak, - Mono, - Bb, -} - -impl Default for MathVariant { - fn default() -> Self { - Self::Serif - } -} - -/// Select the correct styled math letter. -/// -/// https://www.w3.org/TR/mathml-core/#new-text-transform-mappings -/// https://en.wikipedia.org/wiki/Mathematical_Alphanumeric_Symbols -pub(super) fn styled_char(style: MathStyle, c: char) -> char { - use MathVariant::*; - - let MathStyle { variant, bold, .. } = style; - let italic = style.italic.unwrap_or(matches!( - c, - 'a'..='z' | 'ı' | 'ȷ' | 'A'..='Z' | 'α'..='ω' | - '∂' | 'ϵ' | 'ϑ' | 'ϰ' | 'ϕ' | 'ϱ' | 'ϖ' - )); - - if let Some(c) = basic_exception(c) { - return c; - } - - if let Some(c) = latin_exception(c, variant, bold, italic) { - return c; - } - - if let Some(c) = greek_exception(c, variant, bold, italic) { - return c; - } - - let base = match c { - 'A'..='Z' => 'A', - 'a'..='z' => 'a', - 'Α'..='Ω' => 'Α', - 'α'..='ω' => 'α', - '0'..='9' => '0', - _ => return c, - }; - - let tuple = (variant, bold, italic); - let start = match c { - // Latin upper. - 'A'..='Z' => match tuple { - (Serif, false, false) => 0x0041, - (Serif, true, false) => 0x1D400, - (Serif, false, true) => 0x1D434, - (Serif, true, true) => 0x1D468, - (Sans, false, false) => 0x1D5A0, - (Sans, true, false) => 0x1D5D4, - (Sans, false, true) => 0x1D608, - (Sans, true, true) => 0x1D63C, - (Cal, false, _) => 0x1D49C, - (Cal, true, _) => 0x1D4D0, - (Frak, false, _) => 0x1D504, - (Frak, true, _) => 0x1D56C, - (Mono, _, _) => 0x1D670, - (Bb, _, _) => 0x1D538, - }, - - // Latin lower. - 'a'..='z' => match tuple { - (Serif, false, false) => 0x0061, - (Serif, true, false) => 0x1D41A, - (Serif, false, true) => 0x1D44E, - (Serif, true, true) => 0x1D482, - (Sans, false, false) => 0x1D5BA, - (Sans, true, false) => 0x1D5EE, - (Sans, false, true) => 0x1D622, - (Sans, true, true) => 0x1D656, - (Cal, false, _) => 0x1D4B6, - (Cal, true, _) => 0x1D4EA, - (Frak, false, _) => 0x1D51E, - (Frak, true, _) => 0x1D586, - (Mono, _, _) => 0x1D68A, - (Bb, _, _) => 0x1D552, - }, - - // Greek upper. - 'Α'..='Ω' => match tuple { - (Serif, false, false) => 0x0391, - (Serif, true, false) => 0x1D6A8, - (Serif, false, true) => 0x1D6E2, - (Serif, true, true) => 0x1D71C, - (Sans, _, false) => 0x1D756, - (Sans, _, true) => 0x1D790, - (Cal | Frak | Mono | Bb, _, _) => return c, - }, - - // Greek lower. - 'α'..='ω' => match tuple { - (Serif, false, false) => 0x03B1, - (Serif, true, false) => 0x1D6C2, - (Serif, false, true) => 0x1D6FC, - (Serif, true, true) => 0x1D736, - (Sans, _, false) => 0x1D770, - (Sans, _, true) => 0x1D7AA, - (Cal | Frak | Mono | Bb, _, _) => return c, - }, - - // Numbers. - '0'..='9' => match tuple { - (Serif, false, _) => 0x0030, - (Serif, true, _) => 0x1D7CE, - (Bb, _, _) => 0x1D7D8, - (Sans, false, _) => 0x1D7E2, - (Sans, true, _) => 0x1D7EC, - (Mono, _, _) => 0x1D7F6, - (Cal | Frak, _, _) => return c, - }, - - _ => unreachable!(), - }; - - std::char::from_u32(start + (c as u32 - base as u32)).unwrap() -} - -fn basic_exception(c: char) -> Option<char> { - Some(match c { - '〈' => '⟨', - '〉' => '⟩', - '《' => '⟪', - '》' => '⟫', - _ => return None, - }) -} - -fn latin_exception( - c: char, - variant: MathVariant, - bold: bool, - italic: bool, -) -> Option<char> { - use MathVariant::*; - Some(match (c, variant, bold, italic) { - ('B', Cal, false, _) => 'ℬ', - ('E', Cal, false, _) => 'ℰ', - ('F', Cal, false, _) => 'ℱ', - ('H', Cal, false, _) => 'ℋ', - ('I', Cal, false, _) => 'ℐ', - ('L', Cal, false, _) => 'ℒ', - ('M', Cal, false, _) => 'ℳ', - ('R', Cal, false, _) => 'ℛ', - ('C', Frak, false, _) => 'ℭ', - ('H', Frak, false, _) => 'ℌ', - ('I', Frak, false, _) => 'ℑ', - ('R', Frak, false, _) => 'ℜ', - ('Z', Frak, false, _) => 'ℨ', - ('C', Bb, ..) => 'ℂ', - ('H', Bb, ..) => 'ℍ', - ('N', Bb, ..) => 'ℕ', - ('P', Bb, ..) => 'ℙ', - ('Q', Bb, ..) => 'ℚ', - ('R', Bb, ..) => 'ℝ', - ('Z', Bb, ..) => 'ℤ', - ('h', Serif, false, true) => 'ℎ', - ('e', Cal, false, _) => 'ℯ', - ('g', Cal, false, _) => 'ℊ', - ('o', Cal, false, _) => 'ℴ', - ('ı', Serif, .., true) => '𝚤', - ('ȷ', Serif, .., true) => '𝚥', - _ => return None, - }) -} - -fn greek_exception( - c: char, - variant: MathVariant, - bold: bool, - italic: bool, -) -> Option<char> { - use MathVariant::*; - let list = match c { - 'ϴ' => ['𝚹', '𝛳', '𝜭', '𝝧', '𝞡'], - '∇' => ['𝛁', '𝛻', '𝜵', '𝝯', '𝞩'], - '∂' => ['𝛛', '𝜕', '𝝏', '𝞉', '𝟃'], - 'ϵ' => ['𝛜', '𝜖', '𝝐', '𝞊', '𝟄'], - 'ϑ' => ['𝛝', '𝜗', '𝝑', '𝞋', '𝟅'], - 'ϰ' => ['𝛞', '𝜘', '𝝒', '𝞌', '𝟆'], - 'ϕ' => ['𝛟', '𝜙', '𝝓', '𝞍', '𝟇'], - 'ϱ' => ['𝛠', '𝜚', '𝝔', '𝞎', '𝟈'], - 'ϖ' => ['𝛡', '𝜛', '𝝕', '𝞏', '𝟉'], - _ => return None, - }; - - Some(match (variant, bold, italic) { - (Serif, true, false) => list[0], - (Serif, false, true) => list[1], - (Serif, true, true) => list[2], - (Sans, _, false) => list[3], - (Sans, _, true) => list[4], - _ => return None, - }) -} diff --git a/crates/typst-library/src/math/underover.rs b/crates/typst-library/src/math/underover.rs deleted file mode 100644 index 6fc76830..00000000 --- a/crates/typst-library/src/math/underover.rs +++ /dev/null @@ -1,315 +0,0 @@ -use super::*; - -const BRACE_GAP: Em = Em::new(0.25); -const BRACKET_GAP: Em = Em::new(0.25); - -/// A marker to distinguish under- vs. overlines. -enum LineKind { - Over, - Under, -} - -/// A horizontal line under content. -/// -/// ```example -/// $ underline(1 + 2 + ... + 5) $ -/// ``` -#[elem(LayoutMath)] -pub struct UnderlineElem { - /// The content above the line. - #[required] - pub body: Content, -} - -impl LayoutMath for UnderlineElem { - #[tracing::instrument(skip(ctx))] - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - layout_underoverline(ctx, self.body(), self.span(), LineKind::Under) - } -} - -/// A horizontal line over content. -/// -/// ```example -/// $ overline(1 + 2 + ... + 5) $ -/// ``` -#[elem(LayoutMath)] -pub struct OverlineElem { - /// The content below the line. - #[required] - pub body: Content, -} - -impl LayoutMath for OverlineElem { - #[tracing::instrument(skip(ctx))] - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - layout_underoverline(ctx, self.body(), self.span(), LineKind::Over) - } -} - -/// layout under- or overlined content -fn layout_underoverline( - ctx: &mut MathContext, - body: &Content, - span: Span, - line: LineKind, -) -> SourceResult<()> { - let (extra_height, content, line_pos, content_pos, baseline, bar_height); - match line { - LineKind::Under => { - let sep = scaled!(ctx, underbar_extra_descender); - bar_height = scaled!(ctx, underbar_rule_thickness); - let gap = scaled!(ctx, underbar_vertical_gap); - extra_height = sep + bar_height + gap; - - content = ctx.layout_fragment(body)?; - - line_pos = Point::with_y(content.height() + gap + bar_height / 2.0); - content_pos = Point::zero(); - baseline = content.ascent() - } - LineKind::Over => { - let sep = scaled!(ctx, overbar_extra_ascender); - bar_height = scaled!(ctx, overbar_rule_thickness); - let gap = scaled!(ctx, overbar_vertical_gap); - extra_height = sep + bar_height + gap; - - ctx.style(ctx.style.with_cramped(true)); - content = ctx.layout_fragment(body)?; - ctx.unstyle(); - - line_pos = Point::with_y(sep + bar_height / 2.0); - content_pos = Point::with_y(extra_height); - baseline = content.ascent() + extra_height; - } - } - - let width = content.width(); - let height = content.height() + extra_height; - let size = Size::new(width, height); - - let content_class = content.class().unwrap_or(MathClass::Normal); - let mut frame = Frame::soft(size); - frame.set_baseline(baseline); - frame.push_frame(content_pos, content.into_frame()); - frame.push( - line_pos, - FrameItem::Shape( - Geometry::Line(Point::with_x(width)).stroked(FixedStroke { - paint: TextElem::fill_in(ctx.styles()).as_decoration(), - thickness: bar_height, - ..FixedStroke::default() - }), - span, - ), - ); - - ctx.push(FrameFragment::new(ctx, frame).with_class(content_class)); - - Ok(()) -} - -/// A horizontal brace under content, with an optional annotation below. -/// -/// ```example -/// $ underbrace(1 + 2 + ... + 5, "numbers") $ -/// ``` -#[elem(LayoutMath)] -pub struct UnderbraceElem { - /// The content above the brace. - #[required] - pub body: Content, - - /// The optional content below the brace. - #[positional] - pub annotation: Option<Content>, -} - -impl LayoutMath for UnderbraceElem { - #[tracing::instrument(skip(ctx))] - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - layout_underoverspreader( - ctx, - self.body(), - &self.annotation(ctx.styles()), - '⏟', - BRACE_GAP, - false, - self.span(), - ) - } -} - -/// A horizontal brace over content, with an optional annotation above. -/// -/// ```example -/// $ overbrace(1 + 2 + ... + 5, "numbers") $ -/// ``` -#[elem(LayoutMath)] -pub struct OverbraceElem { - /// The content below the brace. - #[required] - pub body: Content, - - /// The optional content above the brace. - #[positional] - pub annotation: Option<Content>, -} - -impl LayoutMath for OverbraceElem { - #[tracing::instrument(skip(ctx))] - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - layout_underoverspreader( - ctx, - self.body(), - &self.annotation(ctx.styles()), - '⏞', - BRACE_GAP, - true, - self.span(), - ) - } -} - -/// A horizontal bracket under content, with an optional annotation below. -/// -/// ```example -/// $ underbracket(1 + 2 + ... + 5, "numbers") $ -/// ``` -#[elem(LayoutMath)] -pub struct UnderbracketElem { - /// The content above the bracket. - #[required] - pub body: Content, - - /// The optional content below the bracket. - #[positional] - pub annotation: Option<Content>, -} - -impl LayoutMath for UnderbracketElem { - #[tracing::instrument(skip(ctx))] - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - layout_underoverspreader( - ctx, - self.body(), - &self.annotation(ctx.styles()), - '⎵', - BRACKET_GAP, - false, - self.span(), - ) - } -} - -/// A horizontal bracket over content, with an optional annotation above. -/// -/// ```example -/// $ overbracket(1 + 2 + ... + 5, "numbers") $ -/// ``` -#[elem(LayoutMath)] -pub struct OverbracketElem { - /// The content below the bracket. - #[required] - pub body: Content, - - /// The optional content above the bracket. - #[positional] - pub annotation: Option<Content>, -} - -impl LayoutMath for OverbracketElem { - #[tracing::instrument(skip(ctx))] - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - layout_underoverspreader( - ctx, - self.body(), - &self.annotation(ctx.styles()), - '⎴', - BRACKET_GAP, - true, - self.span(), - ) - } -} - -/// Layout an over- or underbrace-like object. -fn layout_underoverspreader( - ctx: &mut MathContext, - body: &Content, - annotation: &Option<Content>, - c: char, - gap: Em, - reverse: bool, - span: Span, -) -> SourceResult<()> { - let gap = gap.scaled(ctx); - let body = ctx.layout_row(body)?; - let body_class = body.class(); - let body = body.into_fragment(ctx); - let glyph = GlyphFragment::new(ctx, c, span); - let stretched = glyph.stretch_horizontal(ctx, body.width(), Abs::zero()); - - let mut rows = vec![MathRow::new(vec![body]), stretched.into()]; - ctx.style(if reverse { - ctx.style.for_subscript() - } else { - ctx.style.for_superscript() - }); - rows.extend( - annotation - .as_ref() - .map(|annotation| ctx.layout_row(annotation)) - .transpose()?, - ); - ctx.unstyle(); - - let mut baseline = 0; - if reverse { - rows.reverse(); - baseline = rows.len() - 1; - } - - let frame = stack(ctx, rows, FixedAlign::Center, gap, baseline); - ctx.push(FrameFragment::new(ctx, frame).with_class(body_class)); - - Ok(()) -} - -/// 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. -pub(super) fn stack( - ctx: &MathContext, - rows: Vec<MathRow>, - align: FixedAlign, - gap: Abs, - baseline: usize, -) -> 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_aligned_frame(ctx, &points, align)) - .collect(); - - let mut y = Abs::zero(); - let mut frame = Frame::soft(Size::new( - width, - rows.iter().map(|row| row.height()).sum::<Abs>() - + rows.len().saturating_sub(1) as f64 * gap, - )); - - for (i, row) in rows.into_iter().enumerate() { - let x = align.position(width - row.width()); - let pos = Point::new(x, y); - if i == baseline { - frame.set_baseline(y + row.baseline()); - } - y += row.height() + gap; - frame.push_frame(pos, row); - } - - frame -} |
