mirror of https://github.com/stelzo/typst.git
413 lines
13 KiB
Rust
413 lines
13 KiB
Rust
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])
|
||
}
|
||
}
|
||
|
||
/// Force 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(())
|
||
}
|
||
}
|
||
|
||
/// Force 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 apply limits in inline equations.
|
||
///
|
||
/// It is useful to disable this setting
|
||
/// in most cases of applying limits globally
|
||
/// (inside show rules or new elements)
|
||
#[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()
|
||
}
|