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 /library/src/math | |
| parent | 3ab19185093d7709f824b95b979060ce125389d8 (diff) | |
Move everything into `crates/` directory
Diffstat (limited to 'library/src/math')
| -rw-r--r-- | library/src/math/accent.rs | 139 | ||||
| -rw-r--r-- | library/src/math/align.rs | 63 | ||||
| -rw-r--r-- | library/src/math/attach.rs | 411 | ||||
| -rw-r--r-- | library/src/math/cancel.rs | 187 | ||||
| -rw-r--r-- | library/src/math/ctx.rs | 268 | ||||
| -rw-r--r-- | library/src/math/delimited.rs | 200 | ||||
| -rw-r--r-- | library/src/math/frac.rs | 148 | ||||
| -rw-r--r-- | library/src/math/fragment.rs | 414 | ||||
| -rw-r--r-- | library/src/math/matrix.rs | 313 | ||||
| -rw-r--r-- | library/src/math/mod.rs | 480 | ||||
| -rw-r--r-- | library/src/math/op.rs | 113 | ||||
| -rw-r--r-- | library/src/math/root.rs | 156 | ||||
| -rw-r--r-- | library/src/math/row.rs | 258 | ||||
| -rw-r--r-- | library/src/math/spacing.rs | 60 | ||||
| -rw-r--r-- | library/src/math/stretch.rs | 199 | ||||
| -rw-r--r-- | library/src/math/style.rs | 620 | ||||
| -rw-r--r-- | library/src/math/underover.rs | 339 |
17 files changed, 0 insertions, 4368 deletions
diff --git a/library/src/math/accent.rs b/library/src/math/accent.rs deleted file mode 100644 index d1bee198..00000000 --- a/library/src/math/accent.rs +++ /dev/null @@ -1,139 +0,0 @@ -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/library/src/math/align.rs b/library/src/math/align.rs deleted file mode 100644 index aee89a89..00000000 --- a/library/src/math/align.rs +++ /dev/null @@ -1,63 +0,0 @@ -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/library/src/math/attach.rs b/library/src/math/attach.rs deleted file mode 100644 index fedeb908..00000000 --- a/library/src/math/attach.rs +++ /dev/null @@ -1,411 +0,0 @@ -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/library/src/math/cancel.rs b/library/src/math/cancel.rs deleted file mode 100644 index f576a727..00000000 --- a/library/src/math/cancel.rs +++ /dev/null @@ -1,187 +0,0 @@ -use super::*; - -/// Displays a diagonal line over a part of an equation. -/// -/// This is commonly used to show the elimination of a term. -/// -/// ## Example { #example } -/// ```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/library/src/math/ctx.rs b/library/src/math/ctx.rs deleted file mode 100644 index a1dc6cf4..00000000 --- a/library/src/math/ctx.rs +++ /dev/null @@ -1,268 +0,0 @@ -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/library/src/math/delimited.rs b/library/src/math/delimited.rs deleted file mode 100644 index 99cd6c33..00000000 --- a/library/src/math/delimited.rs +++ /dev/null @@ -1,200 +0,0 @@ -use super::*; - -/// How much less high scaled delimiters can be than what they wrap. -pub(super) const DELIM_SHORT_FALL: Em = Em::new(0.1); - -/// Scales delimiters. -/// -/// While matched delimiters scale by default, this can be used to scale -/// unmatched delimiters and to control the delimiter scaling more precisely. -/// -/// ## 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/library/src/math/frac.rs b/library/src/math/frac.rs deleted file mode 100644 index 0e1f78cc..00000000 --- a/library/src/math/frac.rs +++ /dev/null @@ -1,148 +0,0 @@ -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/library/src/math/fragment.rs b/library/src/math/fragment.rs deleted file mode 100644 index 139ce07b..00000000 --- a/library/src/math/fragment.rs +++ /dev/null @@ -1,414 +0,0 @@ -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/library/src/math/matrix.rs b/library/src/math/matrix.rs deleted file mode 100644 index aaccc332..00000000 --- a/library/src/math/matrix.rs +++ /dev/null @@ -1,313 +0,0 @@ -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/library/src/math/mod.rs b/library/src/math/mod.rs deleted file mode 100644 index 0429265f..00000000 --- a/library/src/math/mod.rs +++ /dev/null @@ -1,480 +0,0 @@ -//! 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/library/src/math/op.rs b/library/src/math/op.rs deleted file mode 100644 index 8ad74c49..00000000 --- a/library/src/math/op.rs +++ /dev/null @@ -1,113 +0,0 @@ -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/library/src/math/root.rs b/library/src/math/root.rs deleted file mode 100644 index d1c5f46a..00000000 --- a/library/src/math/root.rs +++ /dev/null @@ -1,156 +0,0 @@ -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/library/src/math/row.rs b/library/src/math/row.rs deleted file mode 100644 index 687f82b8..00000000 --- a/library/src/math/row.rs +++ /dev/null @@ -1,258 +0,0 @@ -use std::iter::once; - -use crate::layout::AlignElem; - -use super::*; - -pub const TIGHT_LEADING: Em = Em::new(0.25); - -#[derive(Debug, Default, Clone)] -pub struct MathRow(Vec<MathFragment>); - -impl MathRow { - pub fn new(fragments: Vec<MathFragment>) -> Self { - let iter = fragments.into_iter().peekable(); - let mut last: Option<usize> = None; - let mut space: Option<MathFragment> = None; - let mut resolved: Vec<MathFragment> = vec![]; - - for mut fragment in iter { - match fragment { - // Keep space only if supported by spaced fragments. - MathFragment::Space(_) => { - if last.is_some() { - space = Some(fragment); - } - continue; - } - - // Explicit spacing disables automatic spacing. - MathFragment::Spacing(_) => { - last = None; - space = None; - resolved.push(fragment); - continue; - } - - // Alignment points are resolved later. - MathFragment::Align => { - resolved.push(fragment); - continue; - } - - // New line, new things. - MathFragment::Linebreak => { - resolved.push(fragment); - space = None; - last = None; - continue; - } - - _ => {} - } - - // Convert variable operators into binary operators if something - // precedes them and they are not preceded by a operator or comparator. - if fragment.class() == Some(MathClass::Vary) - && matches!( - last.and_then(|i| resolved[i].class()), - Some( - MathClass::Normal - | MathClass::Alphabetic - | MathClass::Closing - | MathClass::Fence - ) - ) - { - fragment.set_class(MathClass::Binary); - } - - // Insert spacing between the last and this item. - if let Some(i) = last { - if let Some(s) = spacing(&resolved[i], space.take(), &fragment) { - resolved.insert(i + 1, s); - } - } - - last = Some(resolved.len()); - resolved.push(fragment); - } - - Self(resolved) - } - - pub fn iter(&self) -> std::slice::Iter<'_, MathFragment> { - self.0.iter() - } - - /// Extract the sublines of the row. - /// - /// It is very unintuitive, but in current state of things, a `MathRow` can - /// contain several actual rows. That function deconstructs it to "single" - /// rows. Hopefully this is only a temporary hack. - pub fn rows(&self) -> Vec<Self> { - self.0 - .split(|frag| matches!(frag, MathFragment::Linebreak)) - .map(|slice| Self(slice.to_vec())) - .collect() - } - - pub fn ascent(&self) -> Abs { - self.iter().map(MathFragment::ascent).max().unwrap_or_default() - } - - pub fn descent(&self) -> Abs { - self.iter().map(MathFragment::descent).max().unwrap_or_default() - } - - pub fn class(&self) -> MathClass { - // Predict the class of the output of 'into_fragment' - if self.0.len() == 1 { - self.0 - .first() - .and_then(|fragment| fragment.class()) - .unwrap_or(MathClass::Special) - } else { - // FrameFragment::new() (inside 'into_fragment' in this branch) defaults - // to MathClass::Normal for its class. - MathClass::Normal - } - } - - pub fn into_frame(self, ctx: &MathContext) -> Frame { - let styles = ctx.styles(); - let align = AlignElem::alignment_in(styles).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/library/src/math/spacing.rs b/library/src/math/spacing.rs deleted file mode 100644 index 848aca78..00000000 --- a/library/src/math/spacing.rs +++ /dev/null @@ -1,60 +0,0 @@ -use super::*; - -pub(super) const THIN: Em = Em::new(1.0 / 6.0); -pub(super) const MEDIUM: Em = Em::new(2.0 / 9.0); -pub(super) const THICK: Em = Em::new(5.0 / 18.0); -pub(super) const QUAD: Em = Em::new(1.0); - -/// 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/library/src/math/stretch.rs b/library/src/math/stretch.rs deleted file mode 100644 index 910f7a81..00000000 --- a/library/src/math/stretch.rs +++ /dev/null @@ -1,199 +0,0 @@ -use ttf_parser::math::{GlyphAssembly, GlyphConstruction, GlyphPart}; -use ttf_parser::LazyArray16; - -use super::*; - -/// Maximum number of times extenders can be repeated. -const MAX_REPEATS: usize = 1024; - -impl GlyphFragment { - /// Try to stretch a glyph to a desired height. - pub fn stretch_vertical( - self, - ctx: &MathContext, - height: Abs, - short_fall: Abs, - ) -> VariantFragment { - stretch_glyph(ctx, self, height, short_fall, false) - } - - /// Try to stretch a glyph to a desired width. - pub fn stretch_horizontal( - self, - ctx: &MathContext, - width: Abs, - short_fall: Abs, - ) -> VariantFragment { - stretch_glyph(ctx, self, width, short_fall, true) - } -} - -/// Try to stretch a glyph to a desired width or height. -/// -/// The resulting frame may not have the exact desired width. -fn stretch_glyph( - ctx: &MathContext, - mut base: GlyphFragment, - target: Abs, - short_fall: Abs, - horizontal: bool, -) -> VariantFragment { - let short_target = target - short_fall; - let mut min_overlap = Abs::zero(); - let construction = ctx - .table - .variants - .and_then(|variants| { - min_overlap = variants.min_connector_overlap.scaled(ctx); - if horizontal { - variants.horizontal_constructions - } else { - variants.vertical_constructions - } - .get(base.id) - }) - .unwrap_or(GlyphConstruction { assembly: None, variants: LazyArray16::new(&[]) }); - - // If the base glyph is good enough, use it. - let advance = if horizontal { base.width } else { base.height() }; - if short_target <= advance { - return base.into_variant(); - } - - // Search for a pre-made variant with a good advance. - let mut best_id = base.id; - let mut best_advance = base.width; - for variant in construction.variants { - best_id = variant.variant_glyph; - best_advance = base.font.to_em(variant.advance_measurement).at(base.font_size); - if short_target <= best_advance { - break; - } - } - - // This is either good or the best we've got. - if short_target <= best_advance || construction.assembly.is_none() { - base.set_id(ctx, best_id); - return base.into_variant(); - } - - // Assemble from parts. - let assembly = construction.assembly.unwrap(); - assemble(ctx, base, assembly, min_overlap, target, horizontal) -} - -/// Assemble a glyph from parts. -fn assemble( - ctx: &MathContext, - base: GlyphFragment, - assembly: GlyphAssembly, - min_overlap: Abs, - target: Abs, - horizontal: bool, -) -> VariantFragment { - // Determine the number of times the extenders need to be repeated as well - // as a ratio specifying how much to spread the parts apart - // (0 = maximal overlap, 1 = minimal overlap). - let mut full; - let mut ratio; - let mut repeat = 0; - loop { - full = Abs::zero(); - ratio = 0.0; - - let mut parts = parts(assembly, repeat).peekable(); - let mut growable = Abs::zero(); - - while let Some(part) = parts.next() { - let mut advance = part.full_advance.scaled(ctx); - if let Some(next) = parts.peek() { - let max_overlap = part - .end_connector_length - .min(next.start_connector_length) - .scaled(ctx); - - advance -= max_overlap; - growable += max_overlap - min_overlap; - } - - full += advance; - } - - if full < target { - let delta = target - full; - ratio = (delta / growable).min(1.0); - full += ratio * growable; - } - - if target <= full || repeat >= MAX_REPEATS { - break; - } - - repeat += 1; - } - - let mut selected = vec![]; - let mut parts = parts(assembly, repeat).peekable(); - while let Some(part) = parts.next() { - let mut advance = part.full_advance.scaled(ctx); - if let Some(next) = parts.peek() { - let max_overlap = - part.end_connector_length.min(next.start_connector_length).scaled(ctx); - advance -= max_overlap; - advance += ratio * (max_overlap - min_overlap); - } - - let mut fragment = base.clone(); - fragment.set_id(ctx, part.glyph_id); - selected.push((fragment, advance)); - } - - let size; - let baseline; - if horizontal { - let height = base.ascent + base.descent; - size = Size::new(full, height); - baseline = base.ascent; - } else { - let axis = scaled!(ctx, axis_height); - let width = selected.iter().map(|(f, _)| f.width).max().unwrap_or_default(); - size = Size::new(width, full); - baseline = full / 2.0 + axis; - } - - let mut frame = Frame::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/library/src/math/style.rs b/library/src/math/style.rs deleted file mode 100644 index 235770db..00000000 --- a/library/src/math/style.rs +++ /dev/null @@ -1,620 +0,0 @@ -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/library/src/math/underover.rs b/library/src/math/underover.rs deleted file mode 100644 index 796c9ebc..00000000 --- a/library/src/math/underover.rs +++ /dev/null @@ -1,339 +0,0 @@ -use super::*; - -const BRACE_GAP: Em = Em::new(0.25); -const BRACKET_GAP: Em = Em::new(0.25); - -/// A marker to distinguish under- vs. overlines. -enum LineKind { - Over, - Under, -} - -/// A horizontal line under content. -/// -/// ## Example { #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 -} |
