Improve positioning of multiple accents and attachments (#3059)

This commit is contained in:
Eric Biedert 2024-01-04 16:14:26 +01:00 committed by GitHub
parent 9aeb63cafa
commit 55536e218d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 92 additions and 56 deletions

View File

@ -1,9 +1,8 @@
use ttf_parser::GlyphId;
use unicode_math_class::MathClass;
use crate::diag::{bail, SourceResult};
use crate::foundations::{cast, elem, Content, NativeElement, Resolve, Smart, Value};
use crate::layout::{Abs, Em, Frame, Length, Point, Rel, Size};
use crate::layout::{Em, Frame, Length, Point, Rel, Size};
use crate::math::{
FrameFragment, GlyphFragment, LayoutMath, MathContext, MathFragment, Scaled,
};
@ -72,12 +71,7 @@ impl LayoutMath for AccentElem {
// 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,
};
let base_attach = base.accent_attach();
let width = self
.size(ctx.styles())
@ -92,10 +86,7 @@ impl LayoutMath for AccentElem {
let short_fall = ACCENT_SHORT_FALL.scaled(ctx);
let variant = glyph.stretch_horizontal(ctx, 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,
};
let accent_attach = variant.accent_attach;
// Descent is negative because the accent's ink bottom is above the
// baseline. Therefore, the default gap is the accent's negated descent
@ -106,8 +97,14 @@ impl LayoutMath for AccentElem {
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 base_italics_correction = base.italics_correction();
let base_text_like = base.is_text_like();
let base_ascent = match &base {
MathFragment::Frame(frame) => frame.base_ascent,
_ => base.ascent(),
};
let mut frame = Frame::soft(size);
frame.set_baseline(baseline);
@ -116,26 +113,16 @@ impl LayoutMath for AccentElem {
ctx.push(
FrameFragment::new(ctx, frame)
.with_class(base_class)
.with_base_ascent(base_ascent),
.with_base_ascent(base_ascent)
.with_italics_correction(base_italics_correction)
.with_accent_attach(base_attach)
.with_text_like(base_text_like),
);
Ok(())
}
}
/// The horizontal attachment position for the given glyph.
fn attachment(ctx: &MathContext, id: GlyphId, italics_correction: Abs) -> Abs {
ctx.table
.glyph_info
.and_then(|info| info.top_accent_attachments)
.and_then(|attachments| attachments.get(id))
.map(|record| record.value.scaled(ctx))
.unwrap_or_else(|| {
let advance = ctx.ttf.glyph_hor_advance(id).unwrap_or_default();
(advance.scaled(ctx) + italics_correction) / 2.0
})
}
/// An accent character.
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)]
pub struct Accent(char);

View File

@ -2,7 +2,7 @@ use unicode_math_class::MathClass;
use crate::diag::SourceResult;
use crate::foundations::{elem, Content, StyleChain};
use crate::layout::{Abs, Frame, FrameItem, Point, Size};
use crate::layout::{Abs, Frame, Point, Size};
use crate::math::{
FrameFragment, LayoutMath, MathContext, MathFragment, MathSize, Scaled,
};
@ -382,7 +382,7 @@ fn compute_shifts_up_and_down(
let mut shift_up = Abs::zero();
let mut shift_down = Abs::zero();
let is_char_box = is_character_box(base);
let is_text_like = base.is_text_like();
if tl.is_some() || tr.is_some() {
let ascent = match &base {
@ -391,7 +391,7 @@ fn compute_shifts_up_and_down(
};
shift_up = shift_up
.max(sup_shift_up)
.max(if is_char_box { Abs::zero() } else { ascent - sup_drop_max })
.max(if is_text_like { Abs::zero() } else { ascent - sup_drop_max })
.max(sup_bottom_min + measure!(tl, descent))
.max(sup_bottom_min + measure!(tr, descent));
}
@ -399,7 +399,7 @@ fn compute_shifts_up_and_down(
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(if is_text_like { Abs::zero() } else { base.descent() + sub_drop_min })
.max(measure!(bl, ascent) - sub_top_max)
.max(measure!(br, ascent) - sub_top_max);
}
@ -429,24 +429,3 @@ fn compute_shifts_up_and_down(
fn is_integral_char(c: char) -> bool {
('∫'..='∳').contains(&c) || ('⨋'..='⨜').contains(&c)
}
/// Whether the fragment consists of a single character or atomic piece of text.
fn is_character_box(fragment: &MathFragment) -> bool {
match fragment {
MathFragment::Glyph(_) | MathFragment::Variant(_) => {
fragment.class() != Some(MathClass::Large)
}
MathFragment::Frame(fragment) => is_atomic_text_frame(&fragment.frame),
_ => false,
}
}
/// Handles e.g. "sin", "log", "exp", "CustomOperator".
fn is_atomic_text_frame(frame: &Frame) -> bool {
// Meta information isn't visible or renderable, so we exclude it.
let mut iter = frame
.items()
.map(|(_, item)| item)
.filter(|item| !matches!(item, FrameItem::Meta(_, _)));
matches!(iter.next(), Some(FrameItem::Text(_))) && iter.next().is_none()
}

View File

@ -109,8 +109,12 @@ impl LayoutMath for CancelElem {
#[typst_macros::time(name = "math.cancel", span = self.span())]
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.
// Preserve properties of body.
let body_class = body.class().unwrap_or(MathClass::Special);
let body_italics = body.italics_correction();
let body_attach = body.accent_attach();
let body_text_like = body.is_text_like();
let mut body = body.into_frame();
let styles = ctx.styles();
@ -150,7 +154,13 @@ impl LayoutMath for CancelElem {
body.push_frame(center, second_line);
}
ctx.push(FrameFragment::new(ctx, body).with_class(body_class));
ctx.push(
FrameFragment::new(ctx, body)
.with_class(body_class)
.with_italics_correction(body_italics)
.with_accent_attach(body_attach)
.with_text_like(body_text_like),
);
Ok(())
}

View File

@ -226,7 +226,7 @@ impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> {
fragments.push(GlyphFragment::new(self, c, span).into());
}
let frame = MathRow::new(fragments).into_frame(self);
FrameFragment::new(self, frame).into()
FrameFragment::new(self, frame).with_text_like(true).into()
} else {
// Anything else is handled by Typst's standard text layout.
let mut style = self.style;
@ -286,6 +286,7 @@ impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> {
Ok(FrameFragment::new(self, frame)
.with_class(MathClass::Alphabetic)
.with_text_like(true)
.with_spaced(spaced))
}

View File

@ -142,14 +142,32 @@ impl MathFragment {
}
}
pub fn is_text_like(&self) -> bool {
match self {
Self::Glyph(_) | Self::Variant(_) => self.class() != Some(MathClass::Large),
MathFragment::Frame(frame) => frame.text_like,
_ => false,
}
}
pub fn italics_correction(&self) -> Abs {
match self {
Self::Glyph(glyph) => glyph.italics_correction,
Self::Variant(variant) => variant.italics_correction,
Self::Frame(fragment) => fragment.italics_correction,
_ => Abs::zero(),
}
}
pub fn accent_attach(&self) -> Abs {
match self {
Self::Glyph(glyph) => glyph.accent_attach,
Self::Variant(variant) => variant.accent_attach,
Self::Frame(fragment) => fragment.accent_attach,
_ => self.width() / 2.0,
}
}
pub fn into_frame(self) -> Frame {
match self {
Self::Glyph(glyph) => glyph.into_frame(),
@ -199,6 +217,7 @@ pub struct GlyphFragment {
pub ascent: Abs,
pub descent: Abs,
pub italics_correction: Abs,
pub accent_attach: Abs,
pub style: MathStyle,
pub font_size: Abs,
pub class: Option<MathClass>,
@ -241,6 +260,7 @@ impl GlyphFragment {
descent: Abs::zero(),
limits: Limits::for_char(c),
italics_correction: Abs::zero(),
accent_attach: Abs::zero(),
class,
span,
meta: MetaElem::data_in(ctx.styles()),
@ -271,6 +291,8 @@ impl GlyphFragment {
});
let mut width = advance.scaled(ctx);
let accent_attach = accent_attach(ctx, id).unwrap_or((width + italics) / 2.0);
if !is_extended_shape(ctx, id) {
width += italics;
}
@ -280,6 +302,7 @@ impl GlyphFragment {
self.ascent = bbox.y_max.scaled(ctx);
self.descent = -bbox.y_min.scaled(ctx);
self.italics_correction = italics;
self.accent_attach = accent_attach;
}
pub fn height(&self) -> Abs {
@ -293,6 +316,7 @@ impl GlyphFragment {
style: self.style,
font_size: self.font_size,
italics_correction: self.italics_correction,
accent_attach: self.accent_attach,
class: self.class,
span: self.span,
limits: self.limits,
@ -356,6 +380,7 @@ pub struct VariantFragment {
pub c: char,
pub id: Option<GlyphId>,
pub italics_correction: Abs,
pub accent_attach: Abs,
pub frame: Frame,
pub style: MathStyle,
pub font_size: Abs,
@ -389,11 +414,15 @@ pub struct FrameFragment {
pub limits: Limits,
pub spaced: bool,
pub base_ascent: Abs,
pub italics_correction: Abs,
pub accent_attach: Abs,
pub text_like: bool,
}
impl FrameFragment {
pub fn new(ctx: &MathContext, mut frame: Frame) -> Self {
let base_ascent = frame.ascent();
let accent_attach = frame.width() / 2.0;
frame.meta(ctx.styles(), false);
Self {
frame,
@ -403,6 +432,9 @@ impl FrameFragment {
limits: Limits::Never,
spaced: false,
base_ascent,
italics_correction: Abs::zero(),
accent_attach,
text_like: false,
}
}
@ -421,6 +453,18 @@ impl FrameFragment {
pub fn with_base_ascent(self, base_ascent: Abs) -> Self {
Self { base_ascent, ..self }
}
pub fn with_italics_correction(self, italics_correction: Abs) -> Self {
Self { italics_correction, ..self }
}
pub fn with_accent_attach(self, accent_attach: Abs) -> Self {
Self { accent_attach, ..self }
}
pub fn with_text_like(self, text_like: bool) -> Self {
Self { text_like, ..self }
}
}
/// Look up the italics correction for a glyph.
@ -428,6 +472,11 @@ fn italics_correction(ctx: &MathContext, id: GlyphId) -> Option<Abs> {
Some(ctx.table.glyph_info?.italic_corrections?.get(id)?.scaled(ctx))
}
/// Loop up the top accent attachment position for a glyph.
fn accent_attach(ctx: &MathContext, id: GlyphId) -> Option<Abs> {
Some(ctx.table.glyph_info?.top_accent_attachments?.get(id)?.scaled(ctx))
}
/// Look up the script/scriptscript alternates for a glyph
fn script_alternatives<'a>(
ctx: &MathContext<'a, '_, '_>,
@ -438,7 +487,7 @@ fn script_alternatives<'a>(
})
}
/// Look up the italics correction for a glyph.
/// Look up whether a glyph is an extended shape.
fn is_extended_shape(ctx: &MathContext, id: GlyphId) -> bool {
ctx.table
.glyph_info

View File

@ -37,9 +37,16 @@ impl LayoutMath for OpElem {
#[typst_macros::time(name = "math.op", span = self.span())]
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
let fragment = ctx.layout_fragment(self.text())?;
let italics = fragment.italics_correction();
let accent_attach = fragment.accent_attach();
let text_like = fragment.is_text_like();
ctx.push(
FrameFragment::new(ctx, fragment.into_frame())
.with_class(MathClass::Large)
.with_italics_correction(italics)
.with_accent_attach(accent_attach)
.with_text_like(text_like)
.with_limits(if self.limits(ctx.styles()) {
Limits::Display
} else {

View File

@ -177,6 +177,8 @@ fn assemble(
offset += advance;
}
let accent_attach = if horizontal { frame.width() / 2.0 } else { base.accent_attach };
VariantFragment {
c: base.c,
id: None,
@ -184,6 +186,7 @@ fn assemble(
style: base.style,
font_size: base.font_size,
italics_correction: Abs::zero(),
accent_attach,
class: base.class,
span: base.span,
limits: base.limits,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB