diff options
| author | Laurenz <laurmaedje@gmail.com> | 2023-07-02 19:59:52 +0200 |
|---|---|---|
| committer | Laurenz <laurmaedje@gmail.com> | 2023-07-02 20:07:43 +0200 |
| commit | ebfdb1dafa430786db10dad2ef7d5467c1bdbed1 (patch) | |
| tree | 2bbc24ddb4124c4bb14dec0e536129d4de37b056 /crates/typst-library/src/math | |
| parent | 3ab19185093d7709f824b95b979060ce125389d8 (diff) | |
Move everything into `crates/` directory
Diffstat (limited to 'crates/typst-library/src/math')
| -rw-r--r-- | crates/typst-library/src/math/accent.rs | 139 | ||||
| -rw-r--r-- | crates/typst-library/src/math/align.rs | 63 | ||||
| -rw-r--r-- | crates/typst-library/src/math/attach.rs | 411 | ||||
| -rw-r--r-- | crates/typst-library/src/math/cancel.rs | 187 | ||||
| -rw-r--r-- | crates/typst-library/src/math/ctx.rs | 268 | ||||
| -rw-r--r-- | crates/typst-library/src/math/delimited.rs | 200 | ||||
| -rw-r--r-- | crates/typst-library/src/math/frac.rs | 148 | ||||
| -rw-r--r-- | crates/typst-library/src/math/fragment.rs | 414 | ||||
| -rw-r--r-- | crates/typst-library/src/math/matrix.rs | 313 | ||||
| -rw-r--r-- | crates/typst-library/src/math/mod.rs | 480 | ||||
| -rw-r--r-- | crates/typst-library/src/math/op.rs | 113 | ||||
| -rw-r--r-- | crates/typst-library/src/math/root.rs | 156 | ||||
| -rw-r--r-- | crates/typst-library/src/math/row.rs | 258 | ||||
| -rw-r--r-- | crates/typst-library/src/math/spacing.rs | 60 | ||||
| -rw-r--r-- | crates/typst-library/src/math/stretch.rs | 199 | ||||
| -rw-r--r-- | crates/typst-library/src/math/style.rs | 620 | ||||
| -rw-r--r-- | crates/typst-library/src/math/underover.rs | 339 |
17 files changed, 4368 insertions, 0 deletions
diff --git a/crates/typst-library/src/math/accent.rs b/crates/typst-library/src/math/accent.rs new file mode 100644 index 00000000..d1bee198 --- /dev/null +++ b/crates/typst-library/src/math/accent.rs @@ -0,0 +1,139 @@ +use super::*; + +/// 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 } +/// ```example +/// $grave(a) = accent(a, `)$ \ +/// $arrow(a) = accent(a, arrow)$ \ +/// $tilde(a) = accent(a, \u{0303})$ +/// ``` +/// +/// Display: Accent +/// Category: math +#[element(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::new(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. +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().into()).cast()?, + None => bail!("expected text"), + }, +} diff --git a/crates/typst-library/src/math/align.rs b/crates/typst-library/src/math/align.rs new file mode 100644 index 00000000..aee89a89 --- /dev/null +++ b/crates/typst-library/src/math/align.rs @@ -0,0 +1,63 @@ +use super::*; + +/// A math alignment point: `&`, `&&`. +/// +/// Display: Alignment Point +/// Category: math +#[element(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 new file mode 100644 index 00000000..fedeb908 --- /dev/null +++ b/crates/typst-library/src/math/attach.rs @@ -0,0 +1,411 @@ +use super::*; + +/// A base with optional attachments. +/// +/// ## Example { #example } +/// ```example +/// // With syntax. +/// $ sum_(i=0)^n a_i = 2^(1+i) $ +/// +/// // With function call. +/// $ attach( +/// Pi, t: alpha, b: beta, +/// tl: 1, tr: 2+3, bl: 4+5, br: 6, +/// ) $ +/// ``` +/// +/// ## Syntax { #syntax } +/// This function also has dedicated syntax for attachments after the base: Use +/// the underscore (`_`) to indicate a subscript i.e. bottom attachment and the +/// hat (`^`) to indicate a superscript i.e. top attachment. +/// +/// Display: Attachment +/// Category: math +#[element(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]) + } +} + +/// Forces a base to display attachments as scripts. +/// +/// ## Example { #example } +/// ```example +/// $ scripts(sum)_1^2 != sum_1^2 $ +/// ``` +/// +/// Display: Scripts +/// Category: math +#[element(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 { #example } +/// ```example +/// $ limits(A)_1^2 != A_1^2 $ +/// ``` +/// +/// Display: Limits +/// Category: math +#[element(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 { + if Self::DEFAULT_TO_LIMITS.contains(&c) { + Limits::Display + } else { + 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, + } + } + + /// Unicode codepoints that should show attachments as limits in display + /// mode. + #[rustfmt::skip] + const DEFAULT_TO_LIMITS: &[char] = &[ + /* ∏ */ '\u{220F}', /* ∐ */ '\u{2210}', /* ∑ */ '\u{2211}', + /* ⋀ */ '\u{22C0}', /* ⋁ */ '\u{22C1}', + /* ⋂ */ '\u{22C2}', /* ⋃ */ '\u{22C3}', + /* ⨀ */ '\u{2A00}', /* ⨁ */ '\u{2A01}', /* ⨂ */ '\u{2A02}', + /* ⨃ */ '\u{2A03}', /* ⨄ */ '\u{2A04}', + /* ⨅ */ '\u{2A05}', /* ⨆ */ '\u{2A06}', + ]; +} + +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::new(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::new(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) +} + +/// 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 new file mode 100644 index 00000000..f576a727 --- /dev/null +++ b/crates/typst-library/src/math/cancel.rs @@ -0,0 +1,187 @@ +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 } +/// ```example +/// >>> #set page(width: 140pt) +/// Here, we can simplify: +/// $ (a dot b dot cancel(x)) / +/// cancel(x) $ +/// ``` +/// +/// Display: Cancel +/// Category: math +#[element(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>, + + /// If the cancel line should be inverted (pointing 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, + + /// If 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 to rotate the cancel line. See the [line's + /// documentation]($func/line.angle) for more details. + /// + /// ```example + /// >>> #set page(width: 140pt) + /// $ cancel(Pi, rotation: #30deg) $ + /// ``` + #[default(Angle::zero())] + pub rotation: Angle, + + /// How to stroke the cancel line. See the + /// [line's documentation]($func/line.stroke) for more details. + /// + /// ```example + /// >>> #set page(width: 140pt) + /// $ cancel( + /// sum x, + /// stroke: #( + /// paint: red, + /// thickness: 1.5pt, + /// dash: "dashed", + /// ), + /// ) $ + /// ``` + #[resolve] + #[fold] + #[default(PartialStroke { + // Default stroke has 0.5pt for better visuals. + thickness: Smart::Custom(Abs::pt(0.5)), + ..Default::default() + })] + pub stroke: PartialStroke, +} + +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(Stroke { + paint: TextElem::fill_in(styles), + ..Default::default() + }); + + let invert = self.inverted(styles); + let cross = self.cross(styles); + let angle = self.rotation(styles); + + let invert_first_line = !cross && invert; + let first_line = draw_cancel_line( + 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(length, stroke, true, angle, body_size, span); + + body.push_frame(center, second_line); + } + + ctx.push(FrameFragment::new(ctx, body).with_class(body_class)); + + Ok(()) + } +} + +/// Draws a cancel line. +fn draw_cancel_line( + length: Rel<Abs>, + stroke: Stroke, + invert: bool, + angle: Angle, + body_size: Size, + span: Span, +) -> Frame { + // B + // /| + // diagonal / | height + // / | + // / | + // O ---- + // width + let diagonal = body_size.to_point().hypot(); + let length = length.relative_to(diagonal); + let (width, height) = (body_size.x, body_size.y); + let mid = body_size / 2.0; + + // Scale the amount needed such that the cancel line has the given 'length' + // (reference length, or 100%, is the whole diagonal). + // Scales from the center. + let scale = length.to_raw() / diagonal.to_raw(); + + // invert horizontally if 'invert' was given + let scale_x = scale * if invert { -1.0 } else { 1.0 }; + let scale_y = scale; + let scales = Axes::new(scale_x, scale_y); + + // Draw a line from bottom left to top right of the given element, where the + // origin represents the very middle of that element, that is, a line from + // (-width / 2, height / 2) with length components (width, -height) (sign is + // inverted in the y-axis). After applying the scale, the line will have the + // correct length and orientation (inverted if needed). + let start = Axes::new(-mid.x, mid.y).zip(scales).map(|(l, s)| l * s); + let delta = Axes::new(width, -height).zip(scales).map(|(l, s)| l * s); + + let mut frame = Frame::new(body_size); + frame.push( + start.to_point(), + FrameItem::Shape(Geometry::Line(delta.to_point()).stroked(stroke), span), + ); + + // Having the middle of the line at the origin is convenient here. + frame.transform(Transform::rotate(angle)); + frame +} diff --git a/crates/typst-library/src/math/ctx.rs b/crates/typst-library/src/math/ctx.rs new file mode 100644 index 00000000..a1dc6cf4 --- /dev/null +++ b/crates/typst-library/src/math/ctx.rs @@ -0,0 +1,268 @@ +use ttf_parser::math::MathValue; +use typst::font::{FontStyle, FontWeight}; +use typst::model::realize; +use unicode_segmentation::UnicodeSegmentation; + +use super::*; + +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 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 table = font.ttf().tables().math.unwrap(); + let constants = table.constants.unwrap(); + + let ssty_table = font + .ttf() + .tables() + .gsub + .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::<ttf_parser::gsub::SubstitutionSubtable>(0) + }) + .and_then(|ssty| match ssty { + ttf_parser::gsub::SubstitutionSubtable::Alternate(alt_glyphs) => { + Some(alt_glyphs) + } + _ => None, + }); + + 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, + constants, + ssty_table, + space_width, + fragments: vec![], + local: Styles::new(), + style: MathStyle { + variant: MathVariant::Serif, + size: if block { MathSize::Display } else { MathSize::Text }, + 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_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::Display => { + if glyph.class == Some(MathClass::Large) { + let height = scaled!(self, display_operator_min_height); + glyph.stretch_vertical(self, height, Abs::zero()).into() + } else { + glyph.into() + } + } + MathSize::Script => { + glyph.make_scriptsize(self); + glyph.into() + } + MathSize::ScriptScript => { + glyph.make_scriptscriptsize(self); + glyph.into() + } + _ => glyph.into(), + } + } else if text.chars().all(|c| c.is_ascii_digit()) { + // 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 spaced = text.graphemes(true).nth(1).is_some(); + 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(); + let frame = self.layout_content(&TextElem::packed(text).spanned(span))?; + FrameFragment::new(self, frame) + .with_class(MathClass::Alphabetic) + .with_spaced(spaced) + .into() + }; + Ok(fragment) + } + + 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 { + FontWeight::REGULAR + })); + 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/delimited.rs b/crates/typst-library/src/math/delimited.rs new file mode 100644 index 00000000..99cd6c33 --- /dev/null +++ b/crates/typst-library/src/math/delimited.rs @@ -0,0 +1,200 @@ +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. +/// +/// ## Example { #example } +/// ```example +/// $ lr(]a, b/2]) $ +/// $ lr(]sum_(x=1)^n] x, size: #50%) $ +/// ``` +/// +/// Display: Left/Right +/// Category: math +#[element(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); + *fragment = + MathFragment::Variant(glyph.stretch_vertical(ctx, height, short_fall)); + + if let Some(class) = apply { + fragment.set_class(class); + } + } +} + +/// Floors an expression. +/// +/// ## Example { #example } +/// ```example +/// $ floor(x/2) $ +/// ``` +/// +/// Display: Floor +/// Category: math +#[func] +pub fn floor( + /// The expression to floor. + body: Content, +) -> Content { + delimited(body, '⌊', '⌋') +} + +/// Ceils an expression. +/// +/// ## Example { #example } +/// ```example +/// $ ceil(x/2) $ +/// ``` +/// +/// Display: Ceil +/// Category: math +#[func] +pub fn ceil( + /// The expression to ceil. + body: Content, +) -> Content { + delimited(body, '⌈', '⌉') +} + +/// Rounds an expression. +/// +/// ## Example { #example } +/// ```example +/// $ round(x/2) $ +/// ``` +/// +/// Display: Round +/// Category: math +#[func] +pub fn round( + /// The expression to round. + body: Content, +) -> Content { + delimited(body, '⌊', '⌉') +} + +/// Takes the absolute value of an expression. +/// +/// ## Example { #example } +/// ```example +/// $ abs(x/2) $ +/// ``` +/// +/// +/// Display: Abs +/// Category: math +#[func] +pub fn abs( + /// The expression to take the absolute value of. + body: Content, +) -> Content { + delimited(body, '|', '|') +} + +/// Takes the norm of an expression. +/// +/// ## Example { #example } +/// ```example +/// $ norm(x/2) $ +/// ``` +/// +/// Display: Norm +/// Category: math +#[func] +pub fn norm( + /// The expression to take the norm of. + body: Content, +) -> Content { + delimited(body, '‖', '‖') +} + +fn delimited(body: Content, left: char, right: char) -> Content { + LrElem::new(Content::sequence([ + TextElem::packed(left), + body, + TextElem::packed(right), + ])) + .pack() +} diff --git a/crates/typst-library/src/math/frac.rs b/crates/typst-library/src/math/frac.rs new file mode 100644 index 00000000..0e1f78cc --- /dev/null +++ b/crates/typst-library/src/math/frac.rs @@ -0,0 +1,148 @@ +use super::*; + +const FRAC_AROUND: Em = Em::new(0.1); + +/// A mathematical fraction. +/// +/// ## Example { #example } +/// ```example +/// $ 1/2 < (x+1)/2 $ +/// $ ((x+1)) / 2 = frac(a, b) $ +/// ``` +/// +/// ## Syntax { #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. +/// +/// Display: Fraction +/// Category: math +#[element(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(), &self.denom(), false, self.span()) + } +} + +/// A binomial expression. +/// +/// ## Example { #example } +/// ```example +/// $ binom(n, k) $ +/// ``` +/// +/// Display: Binomial +/// Category: math +#[element(LayoutMath)] +pub struct BinomElem { + /// The binomial's upper index. + #[required] + pub upper: Content, + + /// The binomial's lower index. + #[required] + pub lower: 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(denom)?; + 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::new(size); + frame.set_baseline(baseline); + frame.push_frame(num_pos, num); + frame.push_frame(denom_pos, denom); + + if binom { + ctx.push( + GlyphFragment::new(ctx, '(', span).stretch_vertical(ctx, height, short_fall), + ); + ctx.push(FrameFragment::new(ctx, frame)); + ctx.push( + GlyphFragment::new(ctx, ')', span).stretch_vertical(ctx, height, short_fall), + ); + } else { + frame.push( + line_pos, + FrameItem::Shape( + Geometry::Line(Point::with_x(line_width)).stroked(Stroke { + paint: TextElem::fill_in(ctx.styles()), + thickness, + ..Stroke::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 new file mode 100644 index 00000000..139ce07b --- /dev/null +++ b/crates/typst-library/src/math/fragment.rs @@ -0,0 +1,414 @@ +use super::*; +use ttf_parser::gsub::AlternateSet; + +#[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> { + 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) { + match self { + Self::Glyph(glyph) => glyph.class = Some(class), + Self::Variant(variant) => variant.class = Some(class), + Self::Frame(fragment) => fragment.class = class, + _ => {} + } + } + + 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) => 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::new(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 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: Vec<Meta>, + 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(); + 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)?; + 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), + _ => 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()), + 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 + } + + /// 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::new(size); + frame.set_baseline(self.ascent); + frame.push(Point::with_y(self.ascent), 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 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)) +} diff --git a/crates/typst-library/src/math/matrix.rs b/crates/typst-library/src/math/matrix.rs new file mode 100644 index 00000000..aaccc332 --- /dev/null +++ b/crates/typst-library/src/math/matrix.rs @@ -0,0 +1,313 @@ +use super::*; + +const ROW_GAP: Em = Em::new(0.5); +const COL_GAP: Em = Em::new(0.5); +const VERTICAL_PADDING: Ratio = Ratio::new(0.1); + +/// A column vector. +/// +/// Content in the vector's elements can be aligned with the `&` symbol. +/// +/// ## Example { #example } +/// ```example +/// $ vec(a, b, c) dot vec(1, 2, 3) +/// = a + 2b + 3c $ +/// ``` +/// +/// Display: Vector +/// Category: math +#[element(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 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(), Align::Center)?; + 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 } +/// ```example +/// $ mat( +/// 1, 2, ..., 10; +/// 2, 2, ..., 10; +/// dots.v, dots.v, dots.down, dots.v; +/// 10, 10, ..., 10; +/// ) $ +/// ``` +/// +/// Display: Matrix +/// Category: math +#[element(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>, + + /// 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<()> { + let delim = self.delim(ctx.styles()); + let frame = layout_mat_body(ctx, &self.rows())?; + 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 } +/// ```example +/// $ f(x, y) := cases( +/// 1 "if" (x dot y)/2 <= 0, +/// 2 "if" x "is even", +/// 3 "if" x in NN, +/// 4 "else", +/// ) $ +/// ``` +/// +/// Display: Cases +/// Category: math +#[element(LayoutMath)] +pub struct CasesElem { + /// The delimiter to use. + /// + /// ```example + /// #set math.cases(delim: "[") + /// $ x = cases(1, 2) $ + /// ``` + #[default(Delimiter::Brace)] + pub delim: Delimiter, + + /// 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(), Align::Left)?; + layout_delimiters(ctx, frame, Some(delim.open()), None, 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: Align, +) -> SourceResult<Frame> { + let gap = ROW_GAP.scaled(ctx); + 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>]) -> SourceResult<Frame> { + let row_gap = ROW_GAP.scaled(ctx); + let col_gap = COL_GAP.scaled(ctx); + + let ncols = rows.first().map_or(0, |row| row.len()); + let nrows = rows.len(); + if ncols == 0 || nrows == 0 { + return Ok(Frame::new(Size::zero())); + } + + let mut heights = vec![(Abs::zero(), Abs::zero()); nrows]; + + ctx.style(ctx.style.for_denominator()); + let mut cols = vec![vec![]; ncols]; + 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(); + + let mut frame = Frame::new(Size::new( + Abs::zero(), + heights.iter().map(|&(a, b)| a + b).sum::<Abs>() + row_gap * (nrows - 1) as f64, + )); + let mut x = Abs::zero(); + for col in cols { + 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, Align::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 + row_gap; + } + x += rcol + col_gap; + } + frame.size_mut().x = x - col_gap; + + Ok(frame) +} + +/// Layout the outer wrapper around a vector's or matrices' body. +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 { + ctx.push( + GlyphFragment::new(ctx, left, span).stretch_vertical(ctx, target, short_fall), + ); + } + + ctx.push(FrameFragment::new(ctx, frame)); + + if let Some(right) = right { + ctx.push( + GlyphFragment::new(ctx, right, span) + .stretch_vertical(ctx, target, short_fall), + ); + } + + Ok(()) +} diff --git a/crates/typst-library/src/math/mod.rs b/crates/typst-library/src/math/mod.rs new file mode 100644 index 00000000..0429265f --- /dev/null +++ b/crates/typst-library/src/math/mod.rs @@ -0,0 +1,480 @@ +//! Mathematical formulas. + +#[macro_use] +mod ctx; +mod accent; +mod align; +mod attach; +mod cancel; +mod delimited; +mod frac; +mod fragment; +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::delimited::*; +pub use self::frac::*; +pub use self::matrix::*; +pub use self::op::*; +pub use self::root::*; +pub use self::style::*; +pub use self::underover::*; + +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::{HElem, ParElem, Spacing}; +use crate::meta::Supplement; +use crate::meta::{ + Count, Counter, CounterUpdate, LocalName, Numbering, Outlinable, Refable, +}; +use crate::prelude::*; +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.define("equation", EquationElem::func()); + math.define("text", TextElem::func()); + + // Grouping. + math.define("lr", LrElem::func()); + math.define("abs", abs_func()); + math.define("norm", norm_func()); + math.define("floor", floor_func()); + math.define("ceil", ceil_func()); + math.define("round", round_func()); + + // Attachments and accents. + math.define("attach", AttachElem::func()); + math.define("scripts", ScriptsElem::func()); + math.define("limits", LimitsElem::func()); + math.define("accent", AccentElem::func()); + math.define("underline", UnderlineElem::func()); + math.define("overline", OverlineElem::func()); + math.define("underbrace", UnderbraceElem::func()); + math.define("overbrace", OverbraceElem::func()); + math.define("underbracket", UnderbracketElem::func()); + math.define("overbracket", OverbracketElem::func()); + math.define("cancel", CancelElem::func()); + + // Fractions and matrix-likes. + math.define("frac", FracElem::func()); + math.define("binom", BinomElem::func()); + math.define("vec", VecElem::func()); + math.define("mat", MatElem::func()); + math.define("cases", CasesElem::func()); + + // Roots. + math.define("sqrt", sqrt_func()); + math.define("root", RootElem::func()); + + // Styles. + math.define("upright", upright_func()); + math.define("bold", bold_func()); + math.define("italic", italic_func()); + math.define("serif", serif_func()); + math.define("sans", sans_func()); + math.define("cal", cal_func()); + math.define("frak", frak_func()); + math.define("mono", mono_func()); + math.define("bb", bb_func()); + + math.define("display", display_func()); + math.define("inline", inline_func()); + math.define("script", script_func()); + math.define("sscript", sscript_func()); + + // Text operators. + math.define("op", OpElem::func()); + op::define(&mut math); + + // Spacings. + spacing::define(&mut math); + + // Symbols. + for (name, symbol) in crate::symbols::SYM { + math.define(*name, symbol.clone()); + } + + Module::new("math").with_scope(math) +} + +/// A mathematical equation. +/// +/// Can be displayed inline with text or as a separate block. +/// +/// ## Example { #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 { #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). +/// +/// Display: Equation +/// Category: math +#[element( + 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]($func/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::func())); + if self.block(styles) { + realized = realized.aligned(Axes::with_x(Some(Align::Center.into()))) + } + Ok(realized) + } +} + +impl Finalize for EquationElem { + fn finalize(&self, realized: Content, _: StyleChain) -> Content { + 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.as_str(), 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::func()) + .display(Some(numbering), false) + .layout(vt, styles, pod)? + .into_frame(); + + let width = if regions.size.x.is_finite() { + regions.size.x + } else { + frame.width() + + 2.0 * (counter.width() + NUMBER_GUTTER.resolve(styles)) + }; + + let height = frame.height().max(counter.height()); + frame.resize(Size::new(width, height), Align::CENTER_HORIZON); + + let x = if TextElem::dir_in(styles).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 slack = ParElem::leading_in(styles) * 0.7; + let top_edge = TextElem::top_edge_in(styles).resolve(styles, font.metrics()); + let bottom_edge = + -TextElem::bottom_edge_in(styles).resolve(styles, font.metrics()); + + 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(&self, 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::FRENCH => "Équation", + Lang::GERMAN => "Gleichung", + Lang::ITALIAN => "Equazione", + Lang::NYNORSK => "Likning", + Lang::POLISH => "Równanie", + Lang::PORTUGUESE => "Equação", + 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::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::func()) + } + + fn numbering(&self) -> Option<Numbering> { + self.numbering(StyleChain::default()) + } +} + +impl Outlinable for EquationElem { + fn outline(&self, vt: &mut Vt) -> SourceResult<Option<Content>> { + 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.0.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 let Some(children) = self.to_sequence() { + for child in children { + 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(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 new file mode 100644 index 00000000..8ad74c49 --- /dev/null +++ b/crates/typst-library/src/math/op.rs @@ -0,0 +1,113 @@ +use typst::eval::Scope; + +use super::*; + +/// A text operator in an equation. +/// +/// ## Example { #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`, `ctg`, `coth`, `csc`, `deg`, `det`, `dim`, +/// `exp`, `gcd`, `hom`, `mod`, `inf`, `ker`, `lg`, `lim`, `ln`, `log`, +/// `max`, `min`, `Pr`, `sec`, `sin`, `sinc`, `sinh`, `sup`, `tan`, `tg`, +/// `tanh`, `liminf`, and `limsup`. +/// +/// Display: Text Operator +/// Category: math +#[element(LayoutMath)] +pub struct OpElem { + /// The operator's text. + #[required] + pub text: EcoString, + + /// 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_text(&TextElem::new(self.text()).spanned(self.span()))?; + 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) { + $(math.define( + stringify!($name), + OpElem::new(ops!(@name $name $(: $value)?).into()) + .with_limits(ops!(@limit $($tts)*)) + .pack() + );)* + + let dif = |d| { + HElem::new(THIN.into()).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, + ctg, + coth, + csc, + deg, + det (limits), + dim, + exp, + gcd (limits), + hom, + mod, + inf (limits), + ker, + lg, + lim (limits), + ln, + log, + max (limits), + min (limits), + Pr (limits), + sec, + sin, + sinc, + sinh, + sup (limits), + tan, + tg, + tanh, + liminf: "lim inf" (limits), + limsup: "lim sup" (limits), +} diff --git a/crates/typst-library/src/math/root.rs b/crates/typst-library/src/math/root.rs new file mode 100644 index 00000000..d1c5f46a --- /dev/null +++ b/crates/typst-library/src/math/root.rs @@ -0,0 +1,156 @@ +use super::*; + +/// A square root. +/// +/// ## Example { #example } +/// ```example +/// $ sqrt(x^2) = x = sqrt(x)^2 $ +/// ``` +/// +/// Display: Square Root +/// Category: math +#[func] +pub fn sqrt( + /// The expression to take the square root of. + radicand: Content, +) -> Content { + RootElem::new(radicand).pack() +} + +/// A general root. +/// +/// ## Example { #example } +/// ```example +/// $ root(3, x) $ +/// ``` +/// +/// Display: Root +/// Category: math +#[element(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. +/// +/// https://www.w3.org/TR/mathml-core/#radicals-msqrt-mroot +fn layout( + ctx: &mut MathContext, + mut 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 = precomposed(ctx, index, target) + .map(|frame| { + index = None; + frame + }) + .unwrap_or_else(|| { + let glyph = GlyphFragment::new(ctx, '√', span); + glyph.stretch_vertical(ctx, target, Abs::zero()).frame + }); + + // Layout the index. + // Script-script style looks too small, we use Script style instead. + ctx.style(ctx.style.with_size(MathSize::Script)); + let index = index.map(|elem| ctx.layout_frame(elem)).transpose()?; + ctx.unstyle(); + + let gap = gap.max((sqrt.height() - radicand.height() - thickness) / 2.0); + let descent = radicand.descent() + gap; + let inner_ascent = extra_ascender + thickness + gap + radicand.ascent(); + + 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; + shift_up = raise_factor * sqrt.height() - descent + index.descent(); + ascent.set_max(shift_up + index.ascent()); + } + + let radicant_offset = sqrt_offset + sqrt.width(); + let width = radicant_offset + radicand.width(); + let size = Size::new(width, ascent + descent); + + let sqrt_pos = Point::new(sqrt_offset, ascent - inner_ascent); + let line_pos = Point::new(radicant_offset, ascent - inner_ascent + thickness / 2.0); + let radicand_pos = Point::new(radicant_offset, ascent - radicand.ascent()); + + let mut frame = Frame::new(size); + frame.set_baseline(ascent); + + if let Some(index) = index { + let index_pos = Point::new(kern_before, ascent - shift_up - index.ascent()); + 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(Stroke { + paint: TextElem::fill_in(ctx.styles()), + thickness, + ..Stroke::default() + }), + span, + ), + ); + + frame.push_frame(radicand_pos, radicand); + ctx.push(FrameFragment::new(ctx, frame)); + + Ok(()) +} + +/// Select a precomposed radical, if the font has it. +fn precomposed(ctx: &MathContext, index: Option<&Content>, target: Abs) -> Option<Frame> { + let elem = index?.to::<TextElem>()?; + let c = match elem.text().as_str() { + "3" => '∛', + "4" => '∜', + _ => return None, + }; + + ctx.ttf.glyph_index(c)?; + let glyph = GlyphFragment::new(ctx, c, elem.span()); + let variant = glyph.stretch_vertical(ctx, target, Abs::zero()).frame; + if variant.height() < target { + return None; + } + + Some(variant) +} diff --git a/crates/typst-library/src/math/row.rs b/crates/typst-library/src/math/row.rs new file mode 100644 index 00000000..687f82b8 --- /dev/null +++ b/crates/typst-library/src/math/row.rs @@ -0,0 +1,258 @@ +use std::iter::once; + +use crate::layout::AlignElem; + +use super::*; + +pub const TIGHT_LEADING: Em = Em::new(0.25); + +#[derive(Debug, Default, Clone)] +pub struct MathRow(Vec<MathFragment>); + +impl MathRow { + pub fn new(fragments: Vec<MathFragment>) -> Self { + let iter = fragments.into_iter().peekable(); + let mut last: Option<usize> = None; + let mut space: Option<MathFragment> = None; + let mut resolved: Vec<MathFragment> = vec![]; + + for mut fragment in iter { + match fragment { + // Keep space only if supported by spaced fragments. + MathFragment::Space(_) => { + if last.is_some() { + space = Some(fragment); + } + continue; + } + + // Explicit spacing disables automatic spacing. + MathFragment::Spacing(_) => { + last = None; + space = None; + resolved.push(fragment); + continue; + } + + // Alignment points are resolved later. + MathFragment::Align => { + resolved.push(fragment); + continue; + } + + // New line, new things. + MathFragment::Linebreak => { + resolved.push(fragment); + space = None; + last = None; + continue; + } + + _ => {} + } + + // Convert variable operators into binary operators if something + // precedes them and they are not preceded by a operator or comparator. + if fragment.class() == Some(MathClass::Vary) + && matches!( + last.and_then(|i| resolved[i].class()), + Some( + MathClass::Normal + | MathClass::Alphabetic + | MathClass::Closing + | MathClass::Fence + ) + ) + { + fragment.set_class(MathClass::Binary); + } + + // Insert spacing between the last and this item. + if let Some(i) = last { + if let Some(s) = spacing(&resolved[i], space.take(), &fragment) { + resolved.insert(i + 1, s); + } + } + + last = Some(resolved.len()); + resolved.push(fragment); + } + + Self(resolved) + } + + pub fn iter(&self) -> std::slice::Iter<'_, MathFragment> { + self.0.iter() + } + + /// Extract the sublines of the row. + /// + /// It is very unintuitive, but in current state of things, a `MathRow` can + /// contain several actual rows. That function deconstructs it to "single" + /// rows. Hopefully this is only a temporary hack. + pub fn rows(&self) -> Vec<Self> { + self.0 + .split(|frag| matches!(frag, MathFragment::Linebreak)) + .map(|slice| Self(slice.to_vec())) + .collect() + } + + pub fn ascent(&self) -> Abs { + self.iter().map(MathFragment::ascent).max().unwrap_or_default() + } + + pub fn descent(&self) -> Abs { + self.iter().map(MathFragment::descent).max().unwrap_or_default() + } + + pub fn class(&self) -> MathClass { + // Predict the class of the output of 'into_fragment' + if self.0.len() == 1 { + self.0 + .first() + .and_then(|fragment| fragment.class()) + .unwrap_or(MathClass::Special) + } else { + // FrameFragment::new() (inside 'into_fragment' in this branch) defaults + // to MathClass::Normal for its class. + MathClass::Normal + } + } + + pub fn into_frame(self, ctx: &MathContext) -> Frame { + let styles = ctx.styles(); + let align = AlignElem::alignment_in(styles).x.resolve(styles); + self.into_aligned_frame(ctx, &[], align) + } + + pub fn into_fragment(self, ctx: &MathContext) -> MathFragment { + if self.0.len() == 1 { + self.0.into_iter().next().unwrap() + } else { + FrameFragment::new(ctx, self.into_frame(ctx)).into() + } + } + + pub fn into_aligned_frame( + self, + ctx: &MathContext, + points: &[Abs], + align: Align, + ) -> Frame { + if self.iter().any(|frag| matches!(frag, MathFragment::Linebreak)) { + let leading = if ctx.style.size >= MathSize::Text { + ParElem::leading_in(ctx.styles()) + } else { + TIGHT_LEADING.scaled(ctx) + }; + + let mut rows: Vec<_> = self.rows(); + + if matches!(rows.last(), Some(row) if row.0.is_empty()) { + rows.pop(); + } + + let AlignmentResult { points, width } = alignments(&rows); + let mut frame = Frame::new(Size::zero()); + + for (i, row) in rows.into_iter().enumerate() { + let sub = row.into_line_frame(&points, align); + let size = frame.size_mut(); + if i > 0 { + size.y += leading; + } + + let mut pos = Point::with_y(size.y); + if points.is_empty() { + pos.x = align.position(width - sub.width()); + } + size.y += sub.height(); + size.x.set_max(sub.width()); + frame.push_frame(pos, sub); + } + frame + } else { + self.into_line_frame(points, align) + } + } + + fn into_line_frame(self, points: &[Abs], align: Align) -> Frame { + let ascent = self.ascent(); + let mut frame = Frame::new(Size::new(Abs::zero(), ascent + self.descent())); + frame.set_baseline(ascent); + + let mut next_x = { + let mut widths = Vec::new(); + if !points.is_empty() && align != Align::Left { + let mut width = Abs::zero(); + for fragment in self.iter() { + if matches!(fragment, MathFragment::Align) { + widths.push(width); + width = Abs::zero(); + } else { + width += fragment.width(); + } + } + widths.push(width); + } + let widths = widths; + + let mut prev_points = once(Abs::zero()).chain(points.iter().copied()); + let mut point_widths = points.iter().copied().zip(widths); + let mut alternator = LeftRightAlternator::Right; + move || match align { + Align::Left => prev_points.next(), + Align::Right => point_widths.next().map(|(point, width)| point - width), + _ => point_widths + .next() + .zip(prev_points.next()) + .zip(alternator.next()) + .map(|(((point, width), prev_point), alternator)| match alternator { + LeftRightAlternator::Left => prev_point, + LeftRightAlternator::Right => point - width, + }), + } + }; + let mut x = next_x().unwrap_or_default(); + + for fragment in self.0.into_iter() { + if matches!(fragment, MathFragment::Align) { + x = next_x().unwrap_or(x); + continue; + } + + let y = ascent - fragment.ascent(); + let pos = Point::new(x, y); + x += fragment.width(); + frame.push_frame(pos, fragment.into_frame()); + } + + frame.size_mut().x = x; + frame + } +} + +impl<T: Into<MathFragment>> From<T> for MathRow { + fn from(fragment: T) -> Self { + Self(vec![fragment.into()]) + } +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +enum LeftRightAlternator { + Left, + Right, +} + +impl Iterator for LeftRightAlternator { + type Item = LeftRightAlternator; + + fn next(&mut self) -> Option<Self::Item> { + let r = Some(*self); + match self { + Self::Left => *self = Self::Right, + Self::Right => *self = Self::Left, + } + r + } +} diff --git a/crates/typst-library/src/math/spacing.rs b/crates/typst-library/src/math/spacing.rs new file mode 100644 index 00000000..848aca78 --- /dev/null +++ b/crates/typst-library/src/math/spacing.rs @@ -0,0 +1,60 @@ +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); + +/// 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()); +} + +/// 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 next to a delimiter. + (Large, Opening | Fence) | (Closing | Fence, Large) => 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 new file mode 100644 index 00000000..910f7a81 --- /dev/null +++ b/crates/typst-library/src/math/stretch.rs @@ -0,0 +1,199 @@ +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::new(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 new file mode 100644 index 00000000..235770db --- /dev/null +++ b/crates/typst-library/src/math/style.rs @@ -0,0 +1,620 @@ +use super::*; + +/// Bold font style in math. +/// +/// ## Example { #example } +/// ```example +/// $ bold(A) := B^+ $ +/// ``` +/// +/// Display: Bold +/// Category: math +#[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 { #example } +/// ```example +/// $ upright(A) != A $ +/// ``` +/// +/// Display: Upright +/// Category: math +#[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. +/// +/// Display: Italic +/// Category: math +#[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. +/// +/// Display: Serif +/// Category: math +#[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 { #example } +/// ```example +/// $ sans(A B C) $ +/// ``` +/// +/// Display: Sans-serif +/// Category: math +#[func] +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 { #example } +/// ```example +/// Let $cal(P)$ be the set of ... +/// ``` +/// +/// Display: Calligraphic +/// Category: math +#[func] +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 { #example } +/// ```example +/// $ frak(P) $ +/// ``` +/// +/// Display: Fraktur +/// Category: math +#[func] +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 { #example } +/// ```example +/// $ mono(x + y = z) $ +/// ``` +/// +/// Display: Monospace +/// Category: math +#[func] +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 { #example } +/// ```example +/// $ bb(b) $ +/// $ bb(N) = NN $ +/// $ f: NN -> RR $ +/// ``` +/// +/// Display: Blackboard Bold +/// Category: math +#[func] +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 { #example } +/// ```example +/// $sum_i x_i/2 = display(sum_i x_i/2)$ +/// ``` +/// +/// Display: Display Size +/// Category: math +#[func] +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 { #example } +/// ```example +/// $ sum_i x_i/2 +/// = inline(sum_i x_i/2) $ +/// ``` +/// +/// Display: Inline Size +/// Category: math +#[func] +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 { #example } +/// ```example +/// $sum_i x_i/2 = script(sum_i x_i/2)$ +/// ``` +/// +/// Display: Script Size +/// Category: math +#[func] +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 { #example } +/// ```example +/// $sum_i x_i/2 = sscript(sum_i x_i/2)$ +/// ``` +/// +/// Display: Script-Script Size +/// Category: math +#[func] +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. +/// +/// Display: Bold +/// Category: math +#[element(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, + /// 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 `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)] +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)] +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 new file mode 100644 index 00000000..796c9ebc --- /dev/null +++ b/crates/typst-library/src/math/underover.rs @@ -0,0 +1,339 @@ +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 { #example } +/// ```example +/// $ underline(1 + 2 + ... + 5) $ +/// ``` +/// +/// Display: Underline +/// Category: math +#[element(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 { #example } +/// ```example +/// $ overline(1 + 2 + ... + 5) $ +/// ``` +/// +/// Display: Overline +/// Category: math +#[element(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::new(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(Stroke { + paint: TextElem::fill_in(ctx.styles()), + thickness: bar_height, + ..Stroke::default() + }), + span, + ), + ); + + ctx.push(FrameFragment::new(ctx, frame).with_class(content_class)); + + Ok(()) +} + +/// A horizontal brace under content, with an optional annotation below. +/// +/// ## Example { #example } +/// ```example +/// $ underbrace(1 + 2 + ... + 5, "numbers") $ +/// ``` +/// +/// Display: Underbrace +/// Category: math +#[element(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 { #example } +/// ```example +/// $ overbrace(1 + 2 + ... + 5, "numbers") $ +/// ``` +/// +/// Display: Overbrace +/// Category: math +#[element(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 { #example } +/// ```example +/// $ underbracket(1 + 2 + ... + 5, "numbers") $ +/// ``` +/// +/// Display: Underbracket +/// Category: math +#[element(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 { #example } +/// ```example +/// $ overbracket(1 + 2 + ... + 5, "numbers") $ +/// ``` +/// +/// Display: Overbracket +/// Category: math +#[element(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, Align::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: Align, + 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::new(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 +} |
