mirror of
https://github.com/typst/typst
synced 2025-05-14 17:15:28 +08:00
277 lines
8.9 KiB
Rust
277 lines
8.9 KiB
Rust
use ttf_parser::gsub::SubstitutionSubtable;
|
|
use ttf_parser::math::MathValue;
|
|
use typst::font::{FontStyle, FontWeight};
|
|
use typst::model::realize;
|
|
use unicode_segmentation::UnicodeSegmentation;
|
|
|
|
use super::*;
|
|
use crate::text::tags;
|
|
|
|
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 glyphwise_tables: Option<Vec<GlyphwiseSubsts<'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 math_table = font.ttf().tables().math.unwrap();
|
|
let gsub_table = font.ttf().tables().gsub;
|
|
let constants = math_table.constants.unwrap();
|
|
|
|
let ssty_table = gsub_table
|
|
.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::<SubstitutionSubtable>(0))
|
|
.and_then(|ssty| match ssty {
|
|
SubstitutionSubtable::Alternate(alt_glyphs) => Some(alt_glyphs),
|
|
_ => None,
|
|
});
|
|
|
|
let features = tags(styles);
|
|
let glyphwise_tables = gsub_table.map(|gsub| {
|
|
features
|
|
.into_iter()
|
|
.filter_map(|feature| GlyphwiseSubsts::new(gsub, feature))
|
|
.collect()
|
|
});
|
|
|
|
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: math_table,
|
|
constants,
|
|
ssty_table,
|
|
glyphwise_tables,
|
|
space_width,
|
|
fragments: vec![],
|
|
local: Styles::new(),
|
|
style: MathStyle {
|
|
variant: MathVariant::Serif,
|
|
size: if block { MathSize::Display } else { MathSize::Text },
|
|
class: Smart::Auto,
|
|
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 => {
|
|
let class = self.style.class.as_custom().or(glyph.class);
|
|
if 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)
|
|
}
|
|
}
|