Implement math kerning and fix various math.attach
bugs (#4762)
2
Cargo.lock
generated
@ -2704,7 +2704,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "typst-dev-assets"
|
name = "typst-dev-assets"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
source = "git+https://github.com/typst/typst-dev-assets?rev=48a924d#48a924d9de82b631bc775124a69384c8d860db04"
|
source = "git+https://github.com/typst/typst-dev-assets?rev=e9f8127#e9f81271547c0d7003770b4fa1e59343e51f7ae8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typst-docs"
|
name = "typst-docs"
|
||||||
|
@ -28,7 +28,7 @@ typst-syntax = { path = "crates/typst-syntax", version = "0.11.0" }
|
|||||||
typst-timing = { path = "crates/typst-timing", version = "0.11.0" }
|
typst-timing = { path = "crates/typst-timing", version = "0.11.0" }
|
||||||
typst-utils = { path = "crates/typst-utils", version = "0.11.0" }
|
typst-utils = { path = "crates/typst-utils", version = "0.11.0" }
|
||||||
typst-assets = { git = "https://github.com/typst/typst-assets", rev = "4ee794c" }
|
typst-assets = { git = "https://github.com/typst/typst-assets", rev = "4ee794c" }
|
||||||
typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "48a924d" }
|
typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "e9f8127" }
|
||||||
arrayvec = "0.7.4"
|
arrayvec = "0.7.4"
|
||||||
az = "1.2"
|
az = "1.2"
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
|
@ -83,6 +83,27 @@ impl<T: Clone> ArcExt<T> for Arc<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Extra methods for [`Option`].
|
||||||
|
pub trait OptionExt<T> {
|
||||||
|
/// Maps an `Option<T>` to `U` by applying a function to a contained value
|
||||||
|
/// (if `Some`) or returns a default (if `None`).
|
||||||
|
fn map_or_default<U: Default, F>(self, f: F) -> U
|
||||||
|
where
|
||||||
|
F: FnOnce(T) -> U;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> OptionExt<T> for Option<T> {
|
||||||
|
fn map_or_default<U: Default, F>(self, f: F) -> U
|
||||||
|
where
|
||||||
|
F: FnOnce(T) -> U,
|
||||||
|
{
|
||||||
|
match self {
|
||||||
|
Some(x) => f(x),
|
||||||
|
None => U::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Extra methods for [`[T]`](slice).
|
/// Extra methods for [`[T]`](slice).
|
||||||
pub trait SliceExt<T> {
|
pub trait SliceExt<T> {
|
||||||
/// Split a slice into consecutive runs with the same key and yield for
|
/// Split a slice into consecutive runs with the same key and yield for
|
||||||
|
@ -262,6 +262,16 @@ pub enum Corner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Corner {
|
impl Corner {
|
||||||
|
/// The opposite corner.
|
||||||
|
pub fn inv(self) -> Self {
|
||||||
|
match self {
|
||||||
|
Self::TopLeft => Self::BottomRight,
|
||||||
|
Self::TopRight => Self::BottomLeft,
|
||||||
|
Self::BottomRight => Self::TopLeft,
|
||||||
|
Self::BottomLeft => Self::TopRight,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The next corner, clockwise.
|
/// The next corner, clockwise.
|
||||||
pub fn next_cw(self) -> Self {
|
pub fn next_cw(self) -> Self {
|
||||||
match self {
|
match self {
|
||||||
|
@ -2,12 +2,13 @@ use unicode_math_class::MathClass;
|
|||||||
|
|
||||||
use crate::diag::SourceResult;
|
use crate::diag::SourceResult;
|
||||||
use crate::foundations::{elem, Content, Packed, StyleChain};
|
use crate::foundations::{elem, Content, Packed, StyleChain};
|
||||||
use crate::layout::{Abs, Frame, Point, Size};
|
use crate::layout::{Abs, Corner, Frame, Point, Size};
|
||||||
use crate::math::{
|
use crate::math::{
|
||||||
style_for_subscript, style_for_superscript, EquationElem, FrameFragment, LayoutMath,
|
style_for_subscript, style_for_superscript, EquationElem, FrameFragment, LayoutMath,
|
||||||
MathContext, MathFragment, MathSize, Scaled,
|
MathContext, MathFragment, MathSize, Scaled,
|
||||||
};
|
};
|
||||||
use crate::text::TextElem;
|
use crate::text::TextElem;
|
||||||
|
use crate::utils::OptionExt;
|
||||||
|
|
||||||
/// A base with optional attachments.
|
/// A base with optional attachments.
|
||||||
///
|
///
|
||||||
@ -240,7 +241,7 @@ impl Limits {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether limits should be displayed in this context
|
/// Whether limits should be displayed in this context.
|
||||||
pub fn active(&self, styles: StyleChain) -> bool {
|
pub fn active(&self, styles: StyleChain) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Self::Always => true,
|
Self::Always => true,
|
||||||
@ -300,145 +301,231 @@ fn layout_attachments(
|
|||||||
base: MathFragment,
|
base: MathFragment,
|
||||||
[tl, t, tr, bl, b, br]: [Option<MathFragment>; 6],
|
[tl, t, tr, bl, b, br]: [Option<MathFragment>; 6],
|
||||||
) -> SourceResult<()> {
|
) -> SourceResult<()> {
|
||||||
let (shift_up, shift_down) = if [&tl, &tr, &bl, &br].iter().all(|e| e.is_none()) {
|
|
||||||
(Abs::zero(), Abs::zero())
|
|
||||||
} else {
|
|
||||||
compute_shifts_up_and_down(ctx, styles, &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();
|
let base_class = base.class();
|
||||||
|
|
||||||
let mut ascent = base_ascent
|
// Calculate the distance from the base's baseline to the superscripts' and
|
||||||
.max(shift_up + measure!(tr, ascent))
|
// subscripts' baseline.
|
||||||
.max(shift_up + measure!(tl, ascent))
|
let (tx_shift, bx_shift) = if [&tl, &tr, &bl, &br].iter().all(|e| e.is_none()) {
|
||||||
.max(shift_up + measure!(t, height));
|
(Abs::zero(), Abs::zero())
|
||||||
|
|
||||||
let mut 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_width_max =
|
|
||||||
(sup_delta + measure!(tr, width)).max(sub_delta + measure!(br, width));
|
|
||||||
|
|
||||||
let (center_frame, base_offset) = if t.is_none() && b.is_none() {
|
|
||||||
(base.into_frame(), Abs::zero())
|
|
||||||
} else {
|
} else {
|
||||||
attach_top_and_bottom(ctx, styles, base, t, b)
|
compute_script_shifts(ctx, styles, &base, [&tl, &tr, &bl, &br])
|
||||||
};
|
};
|
||||||
if [&tl, &bl, &tr, &br].iter().all(|&e| e.is_none()) {
|
|
||||||
ctx.push(FrameFragment::new(ctx, styles, center_frame).with_class(base_class));
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
ascent.set_max(center_frame.ascent());
|
// Calculate the distance from the base's baseline to the top attachment's
|
||||||
descent.set_max(center_frame.descent());
|
// and bottom attachment's baseline.
|
||||||
|
let (t_shift, b_shift) =
|
||||||
|
compute_limit_shifts(ctx, styles, &base, [t.as_ref(), b.as_ref()]);
|
||||||
|
|
||||||
let mut frame = Frame::soft(Size::new(
|
// Calculate the final frame height.
|
||||||
pre_width_max
|
let ascent = base
|
||||||
+ base_width
|
.ascent()
|
||||||
+ post_width_max
|
.max(tx_shift + measure!(tr, ascent))
|
||||||
+ scaled!(ctx, styles, space_after_script),
|
.max(tx_shift + measure!(tl, ascent))
|
||||||
ascent + descent,
|
.max(t_shift + measure!(t, ascent));
|
||||||
));
|
let descent = base
|
||||||
frame.set_baseline(ascent);
|
.descent()
|
||||||
frame.push_frame(
|
.max(bx_shift + measure!(br, descent))
|
||||||
Point::new(sup_delta + pre_width_max, frame.ascent() - base_ascent - base_offset),
|
.max(bx_shift + measure!(bl, descent))
|
||||||
center_frame,
|
.max(b_shift + measure!(b, descent));
|
||||||
|
let height = ascent + descent;
|
||||||
|
|
||||||
|
// Calculate the vertical position of each element in the final frame.
|
||||||
|
let base_y = ascent - base.ascent();
|
||||||
|
let tx_y = |tx: &MathFragment| ascent - tx_shift - tx.ascent();
|
||||||
|
let bx_y = |bx: &MathFragment| ascent + bx_shift - bx.ascent();
|
||||||
|
let t_y = |t: &MathFragment| ascent - t_shift - t.ascent();
|
||||||
|
let b_y = |b: &MathFragment| ascent + b_shift - b.ascent();
|
||||||
|
|
||||||
|
// Calculate the distance each limit extends to the left and right of the
|
||||||
|
// base's width.
|
||||||
|
let ((t_pre_width, t_post_width), (b_pre_width, b_post_width)) =
|
||||||
|
compute_limit_widths(&base, [t.as_ref(), b.as_ref()]);
|
||||||
|
|
||||||
|
// `space_after_script` is extra spacing that is at the start before each
|
||||||
|
// pre-script, and at the end after each post-script (see the MathConstants
|
||||||
|
// table in the OpenType MATH spec).
|
||||||
|
let space_after_script = scaled!(ctx, styles, space_after_script);
|
||||||
|
|
||||||
|
// Calculate the distance each pre-script extends to the left of the base's
|
||||||
|
// width.
|
||||||
|
let (tl_pre_width, bl_pre_width) = compute_pre_script_widths(
|
||||||
|
ctx,
|
||||||
|
&base,
|
||||||
|
[tl.as_ref(), bl.as_ref()],
|
||||||
|
(tx_shift, bx_shift),
|
||||||
|
space_after_script,
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Some(tl) = tl {
|
// Calculate the distance each post-script extends to the right of the
|
||||||
let pos =
|
// base's width. Also calculate each post-script's kerning (we need this for
|
||||||
Point::new(-pre_width_dif.min(Abs::zero()), ascent - shift_up - tl.ascent());
|
// its position later).
|
||||||
frame.push_frame(pos, tl.into_frame());
|
let ((tr_post_width, tr_kern), (br_post_width, br_kern)) = compute_post_script_widths(
|
||||||
|
ctx,
|
||||||
|
&base,
|
||||||
|
[tr.as_ref(), br.as_ref()],
|
||||||
|
(tx_shift, bx_shift),
|
||||||
|
space_after_script,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Calculate the final frame width.
|
||||||
|
let pre_width = t_pre_width.max(b_pre_width).max(tl_pre_width).max(bl_pre_width);
|
||||||
|
let base_width = base.width();
|
||||||
|
let post_width = t_post_width.max(b_post_width).max(tr_post_width).max(br_post_width);
|
||||||
|
let width = pre_width + base_width + post_width;
|
||||||
|
|
||||||
|
// Calculate the horizontal position of each element in the final frame.
|
||||||
|
let base_x = pre_width;
|
||||||
|
let tl_x = pre_width - tl_pre_width + space_after_script;
|
||||||
|
let bl_x = pre_width - bl_pre_width + space_after_script;
|
||||||
|
let tr_x = pre_width + base_width + tr_kern;
|
||||||
|
let br_x = pre_width + base_width + br_kern;
|
||||||
|
let t_x = pre_width - t_pre_width;
|
||||||
|
let b_x = pre_width - b_pre_width;
|
||||||
|
|
||||||
|
// Create the final frame.
|
||||||
|
let mut frame = Frame::soft(Size::new(width, height));
|
||||||
|
frame.set_baseline(ascent);
|
||||||
|
frame.push_frame(Point::new(base_x, base_y), base.into_frame());
|
||||||
|
|
||||||
|
macro_rules! layout {
|
||||||
|
($e: ident, $x: ident, $y: ident) => {
|
||||||
|
if let Some($e) = $e {
|
||||||
|
frame.push_frame(Point::new($x, $y(&$e)), $e.into_frame());
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(bl) = bl {
|
layout!(tl, tl_x, tx_y); // pre-superscript
|
||||||
let pos =
|
layout!(bl, bl_x, bx_y); // pre-subscript
|
||||||
Point::new(pre_width_dif.max(Abs::zero()), ascent + shift_down - bl.ascent());
|
layout!(tr, tr_x, tx_y); // post-superscript
|
||||||
frame.push_frame(pos, bl.into_frame());
|
layout!(br, br_x, bx_y); // post-subscript
|
||||||
}
|
layout!(t, t_x, t_y); // upper-limit
|
||||||
|
layout!(b, b_x, b_y); // lower-limit
|
||||||
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());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Done! Note that we retain the class of the base.
|
||||||
ctx.push(FrameFragment::new(ctx, styles, frame).with_class(base_class));
|
ctx.push(FrameFragment::new(ctx, styles, frame).with_class(base_class));
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn attach_top_and_bottom(
|
/// Calculate the distance each post-script extends to the right of the base's
|
||||||
|
/// width, as well as its kerning value. Requires the distance from the base's
|
||||||
|
/// baseline to each post-script's baseline to obtain the correct kerning value.
|
||||||
|
/// Returns 2 tuples of two lengths, each first containing the distance the
|
||||||
|
/// post-script extends left of the base's width and second containing the
|
||||||
|
/// post-script's kerning value. The first tuple is for the post-superscript,
|
||||||
|
/// and the second is for the post-subscript.
|
||||||
|
fn compute_post_script_widths(
|
||||||
ctx: &MathContext,
|
ctx: &MathContext,
|
||||||
styles: StyleChain,
|
base: &MathFragment,
|
||||||
base: MathFragment,
|
[tr, br]: [Option<&MathFragment>; 2],
|
||||||
t: Option<MathFragment>,
|
(tr_shift, br_shift): (Abs, Abs),
|
||||||
b: Option<MathFragment>,
|
space_after_post_script: Abs,
|
||||||
) -> (Frame, Abs) {
|
) -> ((Abs, Abs), (Abs, Abs)) {
|
||||||
let upper_gap_min = scaled!(ctx, styles, upper_limit_gap_min);
|
let tr_values = tr.map_or_default(|tr| {
|
||||||
let upper_rise_min = scaled!(ctx, styles, upper_limit_baseline_rise_min);
|
let kern = math_kern(ctx, base, tr, tr_shift, Corner::TopRight);
|
||||||
let lower_gap_min = scaled!(ctx, styles, lower_limit_gap_min);
|
(space_after_post_script + tr.width() + kern, kern)
|
||||||
let lower_drop_min = scaled!(ctx, styles, lower_limit_baseline_drop_min);
|
});
|
||||||
|
|
||||||
let mut base_offset = Abs::zero();
|
// The base's bounding box already accounts for its italic correction, so we
|
||||||
let mut width = base.width();
|
// need to shift the post-subscript left by the base's italic correction
|
||||||
let mut height = base.height();
|
// (see the kerning algorithm as described in the OpenType MATH spec).
|
||||||
|
let br_values = br.map_or_default(|br| {
|
||||||
|
let kern = math_kern(ctx, base, br, br_shift, Corner::BottomRight)
|
||||||
|
- base.italics_correction();
|
||||||
|
(space_after_post_script + br.width() + kern, kern)
|
||||||
|
});
|
||||||
|
|
||||||
if let Some(t) = &t {
|
(tr_values, br_values)
|
||||||
let top_gap = upper_gap_min.max(upper_rise_min - t.descent());
|
|
||||||
width.set_max(t.width());
|
|
||||||
height += t.height() + top_gap;
|
|
||||||
base_offset = top_gap + t.height();
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(b) = &b {
|
|
||||||
let bottom_gap = lower_gap_min.max(lower_drop_min - b.ascent());
|
|
||||||
width.set_max(b.width());
|
|
||||||
height += b.height() + bottom_gap;
|
|
||||||
}
|
|
||||||
|
|
||||||
let base_pos = Point::new((width - base.width()) / 2.0, base_offset);
|
|
||||||
let delta = base.italics_correction() / 2.0;
|
|
||||||
|
|
||||||
let mut frame = Frame::soft(Size::new(width, height));
|
|
||||||
frame.set_baseline(base_pos.y + base.ascent());
|
|
||||||
frame.push_frame(base_pos, base.into_frame());
|
|
||||||
|
|
||||||
if let Some(t) = t {
|
|
||||||
let top_pos = Point::with_x((width - t.width()) / 2.0 + delta);
|
|
||||||
frame.push_frame(top_pos, t.into_frame());
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(b) = b {
|
|
||||||
let bottom_pos =
|
|
||||||
Point::new((width - b.width()) / 2.0 - delta, height - b.height());
|
|
||||||
frame.push_frame(bottom_pos, b.into_frame());
|
|
||||||
}
|
|
||||||
|
|
||||||
(frame, base_offset)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compute_shifts_up_and_down(
|
/// Calculate the distance each pre-script extends to the left of the base's
|
||||||
|
/// width. Requires the distance from the base's baseline to each pre-script's
|
||||||
|
/// baseline to obtain the correct kerning value.
|
||||||
|
/// Returns two lengths, the first being the distance the pre-superscript
|
||||||
|
/// extends left of the base's width and the second being the distance the
|
||||||
|
/// pre-subscript extends left of the base's width.
|
||||||
|
fn compute_pre_script_widths(
|
||||||
|
ctx: &MathContext,
|
||||||
|
base: &MathFragment,
|
||||||
|
[tl, bl]: [Option<&MathFragment>; 2],
|
||||||
|
(tl_shift, bl_shift): (Abs, Abs),
|
||||||
|
space_before_pre_script: Abs,
|
||||||
|
) -> (Abs, Abs) {
|
||||||
|
let tl_pre_width = tl.map_or_default(|tl| {
|
||||||
|
let kern = math_kern(ctx, base, tl, tl_shift, Corner::TopLeft);
|
||||||
|
space_before_pre_script + tl.width() + kern
|
||||||
|
});
|
||||||
|
|
||||||
|
let bl_pre_width = bl.map_or_default(|bl| {
|
||||||
|
let kern = math_kern(ctx, base, bl, bl_shift, Corner::BottomLeft);
|
||||||
|
space_before_pre_script + bl.width() + kern
|
||||||
|
});
|
||||||
|
|
||||||
|
(tl_pre_width, bl_pre_width)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculate the distance each limit extends beyond the base's width, in each
|
||||||
|
/// direction. Can be a negative value if the limit does not extend beyond the
|
||||||
|
/// base's width, indicating how far into the base's width the limit extends.
|
||||||
|
/// Returns 2 tuples of two lengths, each first containing the distance the
|
||||||
|
/// limit extends leftward beyond the base's width and second containing the
|
||||||
|
/// distance the limit extends rightward beyond the base's width. The first
|
||||||
|
/// tuple is for the upper-limit, and the second is for the lower-limit.
|
||||||
|
fn compute_limit_widths(
|
||||||
|
base: &MathFragment,
|
||||||
|
[t, b]: [Option<&MathFragment>; 2],
|
||||||
|
) -> ((Abs, Abs), (Abs, Abs)) {
|
||||||
|
// The upper- (lower-) limit is shifted to the right (left) of the base's
|
||||||
|
// center by half the base's italic correction.
|
||||||
|
let delta = base.italics_correction() / 2.0;
|
||||||
|
|
||||||
|
let t_widths = t.map_or_default(|t| {
|
||||||
|
let half = (t.width() - base.width()) / 2.0;
|
||||||
|
(half - delta, half + delta)
|
||||||
|
});
|
||||||
|
|
||||||
|
let b_widths = b.map_or_default(|b| {
|
||||||
|
let half = (b.width() - base.width()) / 2.0;
|
||||||
|
(half + delta, half - delta)
|
||||||
|
});
|
||||||
|
|
||||||
|
(t_widths, b_widths)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculate the distance from the base's baseline to each limit's baseline.
|
||||||
|
/// Returns two lengths, the first being the distance to the upper-limit's
|
||||||
|
/// baseline and the second being the distance to the lower-limit's baseline.
|
||||||
|
fn compute_limit_shifts(
|
||||||
|
ctx: &MathContext,
|
||||||
|
styles: StyleChain,
|
||||||
|
base: &MathFragment,
|
||||||
|
[t, b]: [Option<&MathFragment>; 2],
|
||||||
|
) -> (Abs, Abs) {
|
||||||
|
// `upper_gap_min` and `lower_gap_min` give gaps to the descender and
|
||||||
|
// ascender of the limits respectively, whereas `upper_rise_min` and
|
||||||
|
// `lower_drop_min` give gaps to each limit's baseline (see the
|
||||||
|
// MathConstants table in the OpenType MATH spec).
|
||||||
|
|
||||||
|
let t_shift = t.map_or_default(|t| {
|
||||||
|
let upper_gap_min = scaled!(ctx, styles, upper_limit_gap_min);
|
||||||
|
let upper_rise_min = scaled!(ctx, styles, upper_limit_baseline_rise_min);
|
||||||
|
base.ascent() + upper_rise_min.max(upper_gap_min + t.descent())
|
||||||
|
});
|
||||||
|
|
||||||
|
let b_shift = b.map_or_default(|b| {
|
||||||
|
let lower_gap_min = scaled!(ctx, styles, lower_limit_gap_min);
|
||||||
|
let lower_drop_min = scaled!(ctx, styles, lower_limit_baseline_drop_min);
|
||||||
|
base.descent() + lower_drop_min.max(lower_gap_min + b.ascent())
|
||||||
|
});
|
||||||
|
|
||||||
|
(t_shift, b_shift)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculate the distance from the base's baseline to each script's baseline.
|
||||||
|
/// Returns two lengths, the first being the distance to the superscripts'
|
||||||
|
/// baseline and the second being the distance to the subscripts' baseline.
|
||||||
|
fn compute_script_shifts(
|
||||||
ctx: &MathContext,
|
ctx: &MathContext,
|
||||||
styles: StyleChain,
|
styles: StyleChain,
|
||||||
base: &MathFragment,
|
base: &MathFragment,
|
||||||
@ -504,7 +591,56 @@ fn compute_shifts_up_and_down(
|
|||||||
(shift_up, shift_down)
|
(shift_up, shift_down)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Determines if the character is one of a variety of integral signs
|
/// Calculate the kerning value for a script with respect to the base. A
|
||||||
|
/// positive value means shifting the script further away from the base, whereas
|
||||||
|
/// a negative value means shifting the script closer to the base. Requires the
|
||||||
|
/// distance from the base's baseline to the script's baseline, as well as the
|
||||||
|
/// script's corner (tl, tr, bl, br).
|
||||||
|
fn math_kern(
|
||||||
|
ctx: &MathContext,
|
||||||
|
base: &MathFragment,
|
||||||
|
script: &MathFragment,
|
||||||
|
shift: Abs,
|
||||||
|
pos: Corner,
|
||||||
|
) -> Abs {
|
||||||
|
// This process is described under the MathKernInfo table in the OpenType
|
||||||
|
// MATH spec.
|
||||||
|
|
||||||
|
let (corr_height_top, corr_height_bot) = match pos {
|
||||||
|
// Calculate two correction heights for superscripts:
|
||||||
|
// - The distance from the superscript's baseline to the top of the
|
||||||
|
// base's bounding box.
|
||||||
|
// - The distance from the base's baseline to the bottom of the
|
||||||
|
// superscript's bounding box.
|
||||||
|
Corner::TopLeft | Corner::TopRight => {
|
||||||
|
(base.ascent() - shift, shift - script.descent())
|
||||||
|
}
|
||||||
|
// Calculate two correction heights for subscripts:
|
||||||
|
// - The distance from the base's baseline to the top of the
|
||||||
|
// subscript's bounding box.
|
||||||
|
// - The distance from the subscript's baseline to the bottom of the
|
||||||
|
// base's bounding box.
|
||||||
|
Corner::BottomLeft | Corner::BottomRight => {
|
||||||
|
(script.ascent() - shift, shift - base.descent())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate the sum of kerning values for each correction height.
|
||||||
|
let summed_kern = |height| {
|
||||||
|
let base_kern = base.kern_at_height(ctx, pos, height);
|
||||||
|
let attach_kern = script.kern_at_height(ctx, pos.inv(), height);
|
||||||
|
base_kern + attach_kern
|
||||||
|
};
|
||||||
|
|
||||||
|
// Take the smaller kerning amount (and so the larger value). Note that
|
||||||
|
// there is a bug in the spec (as of 2024-08-15): it says to take the
|
||||||
|
// minimum of the two sums, but as the kerning value is usually negative it
|
||||||
|
// really means the smaller kern. The current wording of the spec could
|
||||||
|
// result in glyphs colliding.
|
||||||
|
summed_kern(corr_height_top).max(summed_kern(corr_height_bot))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determines if the character is one of a variety of integral signs.
|
||||||
fn is_integral_char(c: char) -> bool {
|
fn is_integral_char(c: char) -> bool {
|
||||||
('∫'..='∳').contains(&c) || ('⨋'..='⨜').contains(&c)
|
('∫'..='∳').contains(&c) || ('⨋'..='⨜').contains(&c)
|
||||||
}
|
}
|
||||||
|
@ -184,6 +184,18 @@ impl MathFragment {
|
|||||||
_ => Limits::Never,
|
_ => Limits::Never,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// If no kern table is provided for a corner, a kerning amount of zero is
|
||||||
|
/// assumed.
|
||||||
|
pub fn kern_at_height(&self, ctx: &MathContext, corner: Corner, height: Abs) -> Abs {
|
||||||
|
match self {
|
||||||
|
Self::Glyph(glyph) => {
|
||||||
|
kern_at_height(ctx, glyph.font_size, glyph.id, corner, height)
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
_ => Abs::zero(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<GlyphFragment> for MathFragment {
|
impl From<GlyphFragment> for MathFragment {
|
||||||
@ -552,10 +564,6 @@ fn is_extended_shape(ctx: &MathContext, id: GlyphId) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Look up a kerning value at a specific corner and height.
|
/// 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(
|
fn kern_at_height(
|
||||||
ctx: &MathContext,
|
ctx: &MathContext,
|
||||||
font_size: Abs,
|
font_size: Abs,
|
||||||
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 965 B After Width: | Height: | Size: 964 B |
BIN
tests/ref/math-attach-kerning-mixed.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
tests/ref/math-attach-kerning.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
tests/ref/math-attach-limit-long.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 952 B After Width: | Height: | Size: 957 B |
Before Width: | Height: | Size: 675 B After Width: | Height: | Size: 670 B |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
@ -129,6 +129,37 @@ $integral.sect_a^b quad \u{2a1b}_a^b quad limits(\u{2a1b})_a^b$
|
|||||||
$ tack.t.big_0^1 quad \u{02A0A}_0^1 quad join_0^1 $
|
$ tack.t.big_0^1 quad \u{02A0A}_0^1 quad join_0^1 $
|
||||||
$tack.t.big_0^1 quad \u{02A0A}_0^1 quad join_0^1$
|
$tack.t.big_0^1 quad \u{02A0A}_0^1 quad join_0^1$
|
||||||
|
|
||||||
|
--- math-attach-limit-long ---
|
||||||
|
// Test long limit attachments.
|
||||||
|
$ attach(product, t: 123456789) attach(product, t: 123456789, bl: x) \
|
||||||
|
attach(product, b: 123456789) attach(product, b: 123456789, tr: x) $
|
||||||
|
$attach(limits(product), t: 123456789) attach(limits(product), t: 123456789, bl: x)$
|
||||||
|
|
||||||
|
$attach(limits(product), b: 123456789) attach(limits(product), b: 123456789, tr: x)$
|
||||||
|
|
||||||
|
--- math-attach-kerning ---
|
||||||
|
// Test math kerning.
|
||||||
|
#show math.equation: set text(font: "STIX Two Math")
|
||||||
|
|
||||||
|
$ L^A Y^c R^2 delta^y omega^f a^2 t^w gamma^V p^+ \
|
||||||
|
b_lambda f_k p_i x_1 x_j x_A y_l y_y beta_s theta_k \
|
||||||
|
J_0 Y_0 T_1 T_f V_a V_A F_j cal(F)_j lambda_y \
|
||||||
|
attach(W, tl: l) attach(A, tl: 2) attach(cal(V), tl: beta)
|
||||||
|
attach(cal(P), tl: iota) attach(f, bl: i) attach(A, bl: x)
|
||||||
|
attach(cal(J), bl: xi) attach(cal(A), bl: m) $
|
||||||
|
|
||||||
|
--- math-attach-kerning-mixed ---
|
||||||
|
// Test mixtures of math kerning.
|
||||||
|
#show math.equation: set text(font: "STIX Two Math")
|
||||||
|
|
||||||
|
$ x_1^i x_2^lambda x_2^(2alpha) x_2^(k+1) x_2^(-p_(-1)) x_j^gamma \
|
||||||
|
f_2^2 v_0^2 z_0^2 beta_s^2 xi_i^k J_1^2 N_(k y)^(-1) V_pi^x \
|
||||||
|
attach(J, tl: 1, br: i) attach(P, tl: i, br: 2) B_i_0 phi.alt_i_(n-1)
|
||||||
|
attach(A, tr: x, bl: x, br: x, tl: x) attach(F, tl: i, tr: f) \
|
||||||
|
attach(cal(A), tl: 2, bl: o) attach(cal(J), bl: l, br: A)
|
||||||
|
attach(cal(y), tr: p, bl: n t) attach(cal(O), tl: 16, tr: +, br: sigma)
|
||||||
|
attach(italic(Upsilon), tr: s, br: Psi, bl: d) $
|
||||||
|
|
||||||
--- math-attach-nested-base ---
|
--- math-attach-nested-base ---
|
||||||
// Test attachments when the base has attachments.
|
// Test attachments when the base has attachments.
|
||||||
$ attach(a^b, b: c) quad
|
$ attach(a^b, b: c) quad
|
||||||
|