From 83b68581461df8968e408bec1b979ed2e3a8f0c5 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Sun, 22 Jan 2023 13:26:42 +0100 Subject: [PATCH] Math framework --- library/src/lib.rs | 21 +-- library/src/math/ctx.rs | 169 ++++++++++++++++++++++ library/src/math/fragment.rs | 239 +++++++++++++++++++++++++++++++ library/src/math/group.rs | 71 ---------- library/src/math/mod.rs | 264 +++++++++++------------------------ library/src/math/row.rs | 118 ++++++++++++++++ src/doc.rs | 18 ++- src/model/styles.rs | 5 + 8 files changed, 628 insertions(+), 277 deletions(-) create mode 100644 library/src/math/ctx.rs create mode 100644 library/src/math/fragment.rs delete mode 100644 library/src/math/group.rs create mode 100644 library/src/math/row.rs diff --git a/library/src/lib.rs b/library/src/lib.rs index 24f3560fb..f714c72be 100644 --- a/library/src/lib.rs +++ b/library/src/lib.rs @@ -49,24 +49,7 @@ fn scope() -> Scope { std.def_func::("raw"); // Math. - std.def_func::("math"); - std.def_func::("acc"); - std.def_func::("frac"); - std.def_func::("binom"); - std.def_func::("script"); - std.def_func::("sqrt"); - std.def_func::("floor"); - std.def_func::("ceil"); - std.def_func::("vec"); - std.def_func::("cases"); - std.def_func::("serif"); - std.def_func::("sans"); - std.def_func::("bold"); - std.def_func::("ital"); - std.def_func::("cal"); - std.def_func::("frak"); - std.def_func::("mono"); - std.def_func::("bb"); + math::define(&mut std); // Layout. std.def_func::("page"); @@ -204,7 +187,7 @@ fn items() -> LangItems { term_item: |term, description| { layout::ListItem::Term(basics::TermItem { term, description }).pack() }, - math: |children, block| math::MathNode { children, block }.pack(), + math: |body, block| math::MathNode { body, block }.pack(), math_atom: |atom| math::AtomNode(atom).pack(), math_script: |base, sub, sup| math::ScriptNode { base, sub, sup }.pack(), math_frac: |num, denom| math::FracNode { num, denom }.pack(), diff --git a/library/src/math/ctx.rs b/library/src/math/ctx.rs new file mode 100644 index 000000000..41299f985 --- /dev/null +++ b/library/src/math/ctx.rs @@ -0,0 +1,169 @@ +use ttf_parser::math::MathValue; + +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(super) struct MathContext<'a, 'b, 'v> { + pub vt: &'v mut Vt<'b>, + pub outer: StyleChain<'a>, + pub map: StyleMap, + pub regions: Regions<'a>, + 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 fill: Paint, + pub lang: Lang, + pub row: MathRow, + pub style: MathStyle, + base_size: Abs, + scaled_size: Abs, + style_stack: Vec, +} + +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 size = styles.get(TextNode::SIZE); + Self { + vt, + outer: styles, + map: StyleMap::new(), + regions: { + let size = Size::new(regions.first.x, regions.base.y); + Regions::one(size, regions.base, Axes::splat(false)) + }, + style: MathStyle { + variant: MathVariant::Serif, + size: if block { MathSize::Display } else { MathSize::Text }, + cramped: false, + bold: false, + italic: true, + }, + fill: styles.get(TextNode::FILL), + lang: styles.get(TextNode::LANG), + font: &font, + ttf: font.ttf(), + table, + constants, + row: MathRow::new(), + base_size: size, + scaled_size: size, + style_stack: vec![], + } + } + + pub fn push(&mut self, fragment: impl Into) { + self.row.push(self.scaled_size, self.style, fragment); + } + + pub fn layout_non_math(&mut self, content: &Content) -> SourceResult { + Ok(content + .layout(&mut self.vt, self.outer.chain(&self.map), self.regions)? + .into_frame()) + } + + pub fn layout_fragment( + &mut self, + node: &dyn LayoutMath, + ) -> SourceResult { + let row = self.layout_row(node)?; + Ok(if row.0.len() == 1 { + row.0.into_iter().next().unwrap() + } else { + row.to_frame(self).into() + }) + } + + pub fn layout_row(&mut self, node: &dyn LayoutMath) -> SourceResult { + let prev = std::mem::take(&mut self.row); + node.layout_math(self)?; + Ok(std::mem::replace(&mut self.row, prev)) + } + + pub fn layout_frame(&mut self, node: &dyn LayoutMath) -> SourceResult { + Ok(self.layout_fragment(node)?.to_frame(self)) + } + + pub fn size(&self) -> Abs { + self.scaled_size + } + + pub fn style(&mut self, style: MathStyle) { + self.style_stack.push(self.style); + self.style = style; + self.rescale(); + self.map.set(TextNode::SIZE, TextSize(self.scaled_size.into())); + } + + pub fn unstyle(&mut self) { + self.style = self.style_stack.pop().unwrap(); + self.rescale(); + self.map.unset(); + } + + fn rescale(&mut self) { + self.scaled_size = match self.style.size { + MathSize::Display | MathSize::Text => self.base_size, + MathSize::Script => { + self.base_size * percent!(self, script_percent_scale_down) + } + MathSize::ScriptScript => { + self.base_size * percent!(self, script_script_percent_scale_down) + } + }; + } +} + +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/fragment.rs b/library/src/math/fragment.rs new file mode 100644 index 000000000..d6d55cc33 --- /dev/null +++ b/library/src/math/fragment.rs @@ -0,0 +1,239 @@ +use super::*; + +#[derive(Debug, Clone)] +pub(super) enum MathFragment { + Glyph(GlyphFragment), + Variant(VariantFragment), + Frame(FrameFragment), + Spacing(Abs), + Align, + Linebreak, +} + +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, + _ => 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 { + match self { + Self::Glyph(glyph) => glyph.class(), + Self::Variant(variant) => variant.class(), + Self::Frame(fragment) => Some(fragment.class), + _ => None, + } + } + + pub fn italics_correction(&self) -> Abs { + match self { + Self::Glyph(glyph) => glyph.italics_correction, + Self::Variant(variant) => variant.italics_correction, + _ => Abs::zero(), + } + } + + pub fn to_frame(self, ctx: &MathContext) -> Frame { + match self { + Self::Glyph(glyph) => glyph.to_frame(ctx), + Self::Variant(variant) => variant.frame, + Self::Frame(fragment) => fragment.frame, + _ => Frame::new(self.size()), + } + } +} + +impl From for MathFragment { + fn from(glyph: GlyphFragment) -> Self { + Self::Glyph(glyph) + } +} + +impl From for MathFragment { + fn from(variant: VariantFragment) -> Self { + Self::Variant(variant) + } +} + +impl From for MathFragment { + fn from(fragment: FrameFragment) -> Self { + Self::Frame(fragment) + } +} + +impl From for MathFragment { + fn from(frame: Frame) -> Self { + Self::Frame(FrameFragment { frame, class: MathClass::Normal, limits: false }) + } +} + +#[derive(Debug, Clone, Copy)] +pub(super) struct GlyphFragment { + pub id: GlyphId, + pub c: char, + pub font_size: Abs, + pub width: Abs, + pub ascent: Abs, + pub descent: Abs, + pub italics_correction: Abs, +} + +impl GlyphFragment { + pub fn new(ctx: &MathContext, c: char) -> Self { + let c = ctx.style.styled_char(c); + let id = ctx.ttf.glyph_index(c).unwrap_or_default(); + Self::with_id(ctx, c, id) + } + + pub fn try_new(ctx: &MathContext, c: char) -> Option { + let c = ctx.style.styled_char(c); + let id = ctx.ttf.glyph_index(c)?; + Some(Self::with_id(ctx, c, id)) + } + + pub fn with_id(ctx: &MathContext, c: char, id: GlyphId) -> Self { + 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, + }); + Self { + id, + c, + font_size: ctx.size(), + width: advance.scaled(ctx), + ascent: bbox.y_max.scaled(ctx), + descent: -bbox.y_min.scaled(ctx), + italics_correction: italics, + } + } + + pub fn height(&self) -> Abs { + self.ascent + self.descent + } + + pub fn class(&self) -> Option { + unicode_math_class::class(self.c) + } + + pub fn to_variant(&self, ctx: &MathContext) -> VariantFragment { + VariantFragment { + c: self.c, + id: Some(self.id), + frame: self.to_frame(ctx), + italics_correction: self.italics_correction, + } + } + + pub fn to_frame(&self, ctx: &MathContext) -> Frame { + let text = Text { + font: ctx.font.clone(), + size: self.font_size, + fill: ctx.fill, + lang: ctx.lang, + glyphs: vec![Glyph { + id: self.id.0, + c: self.c, + x_advance: Em::from_length(self.width, ctx.size()), + x_offset: Em::zero(), + }], + }; + 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), Element::Text(text)); + frame + } +} + +#[derive(Debug, Clone)] +pub struct VariantFragment { + pub c: char, + pub id: Option, + pub frame: Frame, + pub italics_correction: Abs, +} + +impl VariantFragment { + pub fn class(&self) -> Option { + unicode_math_class::class(self.c) + } +} + +#[derive(Debug, Clone)] +pub struct FrameFragment { + pub frame: Frame, + pub class: MathClass, + pub limits: bool, +} + +/// Look up the italics correction for a glyph. +fn italics_correction(ctx: &MathContext, id: GlyphId) -> Option { + Some(ctx.table.glyph_info?.italic_corrections?.get(id)?.scaled(ctx)) +} + +/// 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 { + 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/group.rs b/library/src/math/group.rs deleted file mode 100644 index 4a55e0e8f..000000000 --- a/library/src/math/group.rs +++ /dev/null @@ -1,71 +0,0 @@ -use super::*; - -/// # Floor -/// A floored expression. -/// -/// ## Example -/// ``` -/// $ floor(x/2) $ -/// ``` -/// -/// ## Parameters -/// - body: Content (positional, required) -/// The expression to floor. -/// -/// ## Category -/// math -#[func] -#[capable(Texify)] -#[derive(Debug, Hash)] -pub struct FloorNode(pub Content); - -#[node] -impl FloorNode { - fn construct(_: &Vm, args: &mut Args) -> SourceResult { - Ok(Self(args.expect("body")?).pack()) - } -} - -impl Texify for FloorNode { - fn texify(&self, t: &mut Texifier) -> SourceResult<()> { - t.push_str("\\left\\lfloor "); - self.0.texify(t)?; - t.push_str("\\right\\rfloor "); - Ok(()) - } -} - -/// # Ceil -/// A ceiled expression. -/// -/// ## Example -/// ``` -/// $ ceil(x/2) $ -/// ``` -/// -/// ## Parameters -/// - body: Content (positional, required) -/// The expression to ceil. -/// -/// ## Category -/// math -#[func] -#[capable(Texify)] -#[derive(Debug, Hash)] -pub struct CeilNode(pub Content); - -#[node] -impl CeilNode { - fn construct(_: &Vm, args: &mut Args) -> SourceResult { - Ok(Self(args.expect("body")?).pack()) - } -} - -impl Texify for CeilNode { - fn texify(&self, t: &mut Texifier) -> SourceResult<()> { - t.push_str("\\left\\lceil "); - self.0.texify(t)?; - t.push_str("\\right\\rceil "); - Ok(()) - } -} diff --git a/library/src/math/mod.rs b/library/src/math/mod.rs index 785e027ad..14fa9f5d8 100644 --- a/library/src/math/mod.rs +++ b/library/src/math/mod.rs @@ -1,5 +1,7 @@ //! Mathematical formulas. +#[macro_use] +mod ctx; mod accent; mod atom; mod frac; @@ -8,23 +10,54 @@ mod matrix; mod root; mod script; mod style; -mod tex; pub use self::accent::*; +pub use self::align::*; pub use self::atom::*; +pub use self::braced::*; pub use self::frac::*; -pub use self::group::*; +pub use self::lr::*; pub use self::matrix::*; +pub use self::op::*; pub use self::root::*; pub use self::script::*; pub use self::style::*; -use typst::model::{Guard, SequenceNode}; -use unicode_segmentation::UnicodeSegmentation; +use ttf_parser::GlyphId; +use ttf_parser::Rect; +use typst::font::Font; +use typst::model::{Guard, Scope, SequenceNode}; +use unicode_math_class::MathClass; -use self::tex::layout_tex; +use self::ctx::*; +use self::fragment::*; +use self::row::*; +use self::spacing::*; +use crate::layout::HNode; +use crate::layout::ParNode; use crate::prelude::*; -use crate::text::{FontFamily, LinebreakNode, SpaceNode, SymbolNode, TextNode}; +use crate::text::LinebreakNode; +use crate::text::TextNode; +use crate::text::TextSize; +use crate::text::{families, variant, FallbackList, FontFamily, SpaceNode, SymbolNode}; + +/// Hook up all math definitions. +pub fn define(scope: &mut Scope) { + scope.def_func::("math"); + scope.def_func::("frac"); + scope.def_func::("script"); + scope.def_func::("sqrt"); + scope.def_func::("vec"); + scope.def_func::("cases"); + scope.def_func::("bold"); + scope.def_func::("italic"); + scope.def_func::("serif"); + scope.def_func::("sans"); + scope.def_func::("cal"); + scope.def_func::("frak"); + scope.def_func::("mono"); + scope.def_func::("bb"); +} /// # Math /// A mathematical formula. @@ -67,8 +100,8 @@ use crate::text::{FontFamily, LinebreakNode, SpaceNode, SymbolNode, TextNode}; /// ``` /// /// ## Parameters -/// - items: Content (positional, variadic) -/// The individual parts of the formula. +/// - body: Content (positional, required) +/// The contents of the formula. /// /// - block: bool (named) /// Whether the formula is displayed as a separate block. @@ -76,21 +109,21 @@ use crate::text::{FontFamily, LinebreakNode, SpaceNode, SymbolNode, TextNode}; /// ## Category /// math #[func] -#[capable(Show, Layout, Inline, Texify)] +#[capable(Show, Finalize, Layout, Inline, LayoutMath)] #[derive(Debug, Clone, Hash)] pub struct MathNode { /// Whether the formula is displayed as a separate block. pub block: bool, - /// The pieces of the formula. - pub children: Vec, + /// The content of the formula. + pub body: Content, } #[node] impl MathNode { fn construct(_: &Vm, args: &mut Args) -> SourceResult { + let body = args.expect("body")?; let block = args.named("block")?.unwrap_or(false); - let children = args.all()?; - Ok(Self { block, children }.pack()) + Ok(Self { block, body }.pack()) } fn field(&self, name: &str) -> Option { @@ -125,213 +158,74 @@ impl Layout for MathNode { &self, vt: &mut Vt, styles: StyleChain, - _: Regions, + regions: Regions, ) -> SourceResult { - let mut t = Texifier::new(styles); - self.texify(&mut t)?; - Ok(layout_tex(vt, &t.finish(), self.block, styles) - .unwrap_or(Fragment::frame(Frame::new(Size::zero())))) + // 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, variant)?; + let font = world.font(id)?; + let _ = font.ttf().tables().math?.constants?; + Some(font) + }) + else { + return Ok(Fragment::frame(Frame::new(Size::zero()))) + }; + + let mut ctx = MathContext::new(vt, styles, regions, &font, self.block); + let frame = ctx.layout_frame(self)?; + Ok(Fragment::frame(frame)) } } impl Inline for MathNode {} -/// Turn a math node into TeX math code. #[capability] -trait Texify { - /// Perform the conversion. - fn texify(&self, t: &mut Texifier) -> SourceResult<()>; +trait LayoutMath { + fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()>; +} - /// Texify the node, but trim parentheses.. - fn texify_unparen(&self, t: &mut Texifier) -> SourceResult<()> { - let s = { - let mut sub = Texifier::new(t.styles); - self.texify(&mut sub)?; - sub.finish() - }; - - let unparened = if s.starts_with("\\left(") && s.ends_with("\\right)") { - s[6..s.len() - 7].into() - } else { - s - }; - - t.push_str(&unparened); - Ok(()) +impl LayoutMath for MathNode { + fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { + self.body.layout_math(ctx) } } -/// Builds the TeX representation of the formula. -struct Texifier<'a> { - tex: EcoString, - support: bool, - space: bool, - styles: StyleChain<'a>, -} - -impl<'a> Texifier<'a> { - /// Create a new texifier. - fn new(styles: StyleChain<'a>) -> Self { - Self { - tex: EcoString::new(), - support: false, - space: false, - styles, - } - } - - /// Finish texifier and return the TeX string. - fn finish(self) -> EcoString { - self.tex - } - - /// Push a weak space. - fn push_space(&mut self) { - self.space = !self.tex.is_empty(); - } - - /// Mark this position as supportive. This allows a space before or after - /// to exist. - fn support(&mut self) { - self.support = true; - } - - /// Flush a space. - fn flush(&mut self) { - if self.space && self.support { - self.tex.push_str("\\ "); - } - - self.space = false; - self.support = false; - } - - /// Push a string. - fn push_str(&mut self, s: &str) { - self.flush(); - self.tex.push_str(s); - } - - /// Escape and push a char for TeX usage. - #[rustfmt::skip] - fn push_escaped(&mut self, c: char) { - self.flush(); - match c { - ' ' => self.tex.push_str("\\ "), - '%' | '&' | '$' | '#' => { - self.tex.push('\\'); - self.tex.push(c); - self.tex.push(' '); - } - '{' => self.tex.push_str("\\left\\{"), - '}' => self.tex.push_str("\\right\\}"), - '[' | '(' => { - self.tex.push_str("\\left"); - self.tex.push(c); - } - ']' | ')' => { - self.tex.push_str("\\right"); - self.tex.push(c); - } - 'a' ..= 'z' | 'A' ..= 'Z' | '0' ..= '9' | 'Α' ..= 'Ω' | 'α' ..= 'ω' | - '*' | '+' | '-' | '?' | '!' | '=' | '<' | '>' | - ':' | ',' | ';' | '|' | '/' | '@' | '.' | '"' => self.tex.push(c), - c => { - if let Some(sym) = unicode_math::SYMBOLS - .iter() - .find(|sym| sym.codepoint == c) { - self.tex.push('\\'); - self.tex.push_str(sym.name); - self.tex.push(' '); - } - } - } - } -} - -impl Texify for MathNode { - fn texify(&self, t: &mut Texifier) -> SourceResult<()> { - for child in &self.children { - child.texify(t)?; - } - Ok(()) - } -} - -impl Texify for Content { - fn texify(&self, t: &mut Texifier) -> SourceResult<()> { +impl LayoutMath for Content { + fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { if self.is::() { - t.push_space(); return Ok(()); } if self.is::() { - t.push_str("\\"); + ctx.push(MathFragment::Linebreak); return Ok(()); } if let Some(node) = self.to::() { if let Some(c) = symmie::get(&node.0) { - t.push_escaped(c); - return Ok(()); + return AtomNode(c.into()).layout_math(ctx); } else if let Some(span) = self.span() { bail!(span, "unknown symbol"); } } - if let Some(node) = self.to::() { - t.support(); - t.push_str("\\mathrm{"); - for c in node.0.chars() { - t.push_escaped(c); - } - t.push_str("}"); - t.support(); - return Ok(()); - } - if let Some(node) = self.to::() { for child in &node.0 { - child.texify(t)?; + child.layout_math(ctx)?; } return Ok(()); } - if let Some(node) = self.with::() { - return node.texify(t); + if let Some(node) = self.with::() { + return node.layout_math(ctx); } - if let Some(span) = self.span() { - bail!(span, "not allowed here"); - } + let frame = ctx.layout_non_math(self)?; + ctx.push(frame); Ok(()) } } - -/// # Alignment Point -/// A math alignment point: `&`, `&&`. -/// -/// ## Parameters -/// - index: usize (positional, required) -/// The alignment point's index. -/// -/// ## Category -/// math -#[func] -#[capable(Texify)] -#[derive(Debug, Hash)] -pub struct AlignPointNode; - -#[node] -impl AlignPointNode { - fn construct(_: &Vm, _: &mut Args) -> SourceResult { - Ok(Self.pack()) - } -} - -impl Texify for AlignPointNode { - fn texify(&self, _: &mut Texifier) -> SourceResult<()> { - Ok(()) - } -} diff --git a/library/src/math/row.rs b/library/src/math/row.rs new file mode 100644 index 000000000..f7b2b3844 --- /dev/null +++ b/library/src/math/row.rs @@ -0,0 +1,118 @@ +use super::*; + +#[derive(Debug, Default, Clone)] +pub(super) struct MathRow(pub Vec); + +impl MathRow { + pub fn new() -> Self { + Self(vec![]) + } + + pub fn width(&self) -> Abs { + self.0.iter().map(|fragment| fragment.width()).sum() + } + + pub fn push( + &mut self, + font_size: Abs, + style: MathStyle, + fragment: impl Into, + ) { + let fragment = fragment.into(); + if let Some(fragment_class) = fragment.class() { + for (i, prev) in self.0.iter().enumerate().rev() { + if matches!(prev, MathFragment::Align) { + continue; + } + + let mut amount = Abs::zero(); + if let MathFragment::Glyph(glyph) = *prev { + if !glyph.italics_correction.is_zero() + && fragment_class != MathClass::Alphabetic + { + amount += glyph.italics_correction; + } + } + + if let Some(prev_class) = prev.class() { + amount += spacing(prev_class, fragment_class, style).at(font_size); + } + + if !amount.is_zero() { + self.0.insert(i + 1, MathFragment::Spacing(amount)); + } + + break; + } + } + self.0.push(fragment); + } + + pub fn to_frame(mut self, ctx: &MathContext) -> Frame { + if self.0.iter().any(|frag| matches!(frag, MathFragment::Linebreak)) { + let mut frame = Frame::new(Size::zero()); + let fragments = std::mem::take(&mut self.0); + + let leading = ctx.outer.chain(&ctx.map).get(ParNode::LEADING); + let rows: Vec<_> = fragments + .split(|frag| matches!(frag, MathFragment::Linebreak)) + .map(|slice| Self(slice.to_vec())) + .collect(); + + let points = alignments(&rows); + for (i, row) in rows.into_iter().enumerate() { + let size = frame.size_mut(); + let sub = row.to_line_frame(ctx, &points); + if i > 0 { + size.y += leading; + } + let pos = Point::with_y(size.y); + size.y += sub.height(); + size.x.set_max(sub.width()); + frame.push_frame(pos, sub); + } + frame + } else { + self.to_line_frame(ctx, &[]) + } + } + + pub fn to_line_frame(self, ctx: &MathContext, points: &[Abs]) -> Frame { + let ascent = self.0.iter().map(MathFragment::ascent).max().unwrap_or_default(); + let descent = self.0.iter().map(MathFragment::descent).max().unwrap_or_default(); + + let size = Size::new(Abs::zero(), ascent + descent); + let mut frame = Frame::new(size); + let mut x = Abs::zero(); + frame.set_baseline(ascent); + + let mut fragments = self.0.into_iter().peekable(); + let mut i = 0; + while let Some(fragment) = fragments.next() { + if matches!(fragment, MathFragment::Align) { + if let Some(&point) = points.get(i) { + x = point; + } + i += 1; + continue; + } + + let y = ascent - fragment.ascent(); + let pos = Point::new(x, y); + x += fragment.width(); + frame.push_frame(pos, fragment.to_frame(ctx)); + } + + frame.size_mut().x = x; + frame + } +} + +impl From for MathRow +where + T: Into, +{ + fn from(fragment: T) -> Self { + Self(vec![fragment.into()]) + } +} diff --git a/src/doc.rs b/src/doc.rs index 9e98ec88c..d4ad2d933 100644 --- a/src/doc.rs +++ b/src/doc.rs @@ -7,7 +7,8 @@ use std::sync::Arc; use crate::font::Font; use crate::geom::{ - Abs, Align, Axes, Dir, Em, Numeric, Paint, Point, Shape, Size, Transform, + self, Abs, Align, Axes, Color, Dir, Em, Geometry, Numeric, Paint, Point, RgbaColor, + Shape, Size, Stroke, Transform, }; use crate::image::Image; use crate::model::{ @@ -160,7 +161,7 @@ impl Frame { self.size.y } - /// The baseline of the frame. + /// The vertical position of the frame's baseline. pub fn baseline(&self) -> Abs { self.baseline.unwrap_or(self.size.y) } @@ -170,6 +171,19 @@ impl Frame { self.baseline = Some(baseline); } + /// The distance from the baseline to the top of the frame. + /// + /// This is the same as `baseline()`, but more in line with the terminology + /// used in math layout. + pub fn ascent(&self) -> Abs { + self.baseline() + } + + /// The distance from the baseline to the bottom of the frame. + pub fn descent(&self) -> Abs { + self.size.y - self.baseline() + } + /// An iterator over the elements inside this frame alongside their /// positions relative to the top-left of the frame. pub fn elements(&self) -> std::slice::Iter<'_, (Point, Element)> { diff --git a/src/model/styles.rs b/src/model/styles.rs index e78d83cd8..27c403097 100644 --- a/src/model/styles.rs +++ b/src/model/styles.rs @@ -48,6 +48,11 @@ impl StyleMap { } } + /// Remove the style that was last set. + pub fn unset(&mut self) { + self.0.pop(); + } + /// Whether the map contains a style property for the given key. pub fn contains(&self, _: K) -> bool { self.0