diff --git a/crates/typst-library/src/math/ctx.rs b/crates/typst-library/src/math/ctx.rs index 999a5ccb9..e6a846745 100644 --- a/crates/typst-library/src/math/ctx.rs +++ b/crates/typst-library/src/math/ctx.rs @@ -5,7 +5,7 @@ use typst::model::realize; use unicode_segmentation::UnicodeSegmentation; use super::*; -use crate::text::tags; +use crate::text::{tags, BottomEdge, BottomEdgeMetric, TopEdge, TopEdgeMetric}; macro_rules! scaled { ($ctx:expr, text: $text:ident, display: $display:ident $(,)?) => { @@ -203,7 +203,27 @@ impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> { 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))?; + let text = TextElem::packed(text) + .styled(TextElem::set_top_edge(TopEdge::Metric(TopEdgeMetric::Bounds))) + .styled(TextElem::set_bottom_edge(BottomEdge::Metric( + BottomEdgeMetric::Bounds, + ))) + .spanned(span); + let par = ParElem::new(vec![text]); + + // There isn't a natural width for a paragraph in a math environment; + // because it will be placed somewhere probably not at the left margin + // it will overflow. So emulate an `hbox` instead and allow the paragraph + // to extend as far as needed. + let frame = par + .layout( + self.vt, + self.outer.chain(&self.local), + false, + Size::splat(Abs::inf()), + false, + )? + .into_frame(); FrameFragment::new(self, frame) .with_class(MathClass::Alphabetic) .with_spaced(spaced) diff --git a/crates/typst-library/src/math/mod.rs b/crates/typst-library/src/math/mod.rs index b5410a03a..c29ad29b4 100644 --- a/crates/typst-library/src/math/mod.rs +++ b/crates/typst-library/src/math/mod.rs @@ -292,9 +292,9 @@ impl Layout for EquationElem { } } else { let slack = ParElem::leading_in(styles) * 0.7; - let top_edge = TextElem::top_edge_in(styles).resolve(styles, font.metrics()); + let top_edge = TextElem::top_edge_in(styles).resolve(styles, &font, None); let bottom_edge = - -TextElem::bottom_edge_in(styles).resolve(styles, font.metrics()); + -TextElem::bottom_edge_in(styles).resolve(styles, &font, None); let ascent = top_edge.max(frame.ascent() - slack); let descent = bottom_edge.max(frame.descent() - slack); diff --git a/crates/typst-library/src/text/mod.rs b/crates/typst-library/src/text/mod.rs index 3c3ccdeff..f7c15c29d 100644 --- a/crates/typst-library/src/text/mod.rs +++ b/crates/typst-library/src/text/mod.rs @@ -15,7 +15,8 @@ pub use self::shaping::*; pub use self::shift::*; use rustybuzz::Tag; -use typst::font::{FontMetrics, FontStretch, FontStyle, FontWeight, VerticalFontMetric}; +use ttf_parser::Rect; +use typst::font::{Font, FontStretch, FontStyle, FontWeight, VerticalFontMetric}; use crate::layout::ParElem; use crate::prelude::*; @@ -245,8 +246,8 @@ pub struct TextElem { /// #set text(top-edge: "cap-height") /// #rect(fill: aqua)[Typst] /// ``` - #[default(TextEdge::Metric(VerticalFontMetric::CapHeight))] - pub top_edge: TextEdge, + #[default(TopEdge::Metric(TopEdgeMetric::CapHeight))] + pub top_edge: TopEdge, /// The bottom end of the conceptual frame around the text used for layout /// and positioning. This affects the size of containers that hold text. @@ -261,8 +262,8 @@ pub struct TextElem { /// #set text(bottom-edge: "descender") /// #rect(fill: aqua)[Typst] /// ``` - #[default(TextEdge::Metric(VerticalFontMetric::Baseline))] - pub bottom_edge: TextEdge, + #[default(BottomEdge::Metric(BottomEdgeMetric::Baseline))] + pub bottom_edge: BottomEdge, /// An [ISO 639-1/2/3 language code.](https://en.wikipedia.org/wiki/ISO_639) /// @@ -606,35 +607,140 @@ cast! { v: Length => Self(v), } -/// Specifies the bottom or top edge of text. +/// Specifies the top edge of text. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum TextEdge { - /// An edge specified using one of the well-known font metrics. - Metric(VerticalFontMetric), +pub enum TopEdge { + /// An edge specified via font metrics or bounding box. + Metric(TopEdgeMetric), /// An edge specified as a length. Length(Length), } -impl TextEdge { +impl TopEdge { + /// Determine if the edge is specified from bounding box info. + pub fn is_bounds(&self) -> bool { + matches!(self, Self::Metric(TopEdgeMetric::Bounds)) + } + /// Resolve the value of the text edge given a font's metrics. - pub fn resolve(self, styles: StyleChain, metrics: &FontMetrics) -> Abs { + pub fn resolve(self, styles: StyleChain, font: &Font, bbox: Option) -> Abs { match self { - Self::Metric(metric) => metrics.vertical(metric).resolve(styles), - Self::Length(length) => length.resolve(styles), + TopEdge::Metric(metric) => { + if let Ok(metric) = metric.try_into() { + font.metrics().vertical(metric).resolve(styles) + } else { + bbox.map(|bbox| (font.to_em(bbox.y_max)).resolve(styles)) + .unwrap_or_default() + } + } + TopEdge::Length(length) => length.resolve(styles), } } } cast! { - TextEdge, + TopEdge, self => match self { Self::Metric(metric) => metric.into_value(), Self::Length(length) => length.into_value(), }, - v: VerticalFontMetric => Self::Metric(v), + v: TopEdgeMetric => Self::Metric(v), v: Length => Self::Length(v), } +/// Metrics that describe the top edge of text. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] +pub enum TopEdgeMetric { + /// The font's ascender, which typically exceeds the height of all glyphs. + Ascender, + /// The approximate height of uppercase letters. + CapHeight, + /// The approximate height of non-ascending lowercase letters. + XHeight, + /// The baseline on which the letters rest. + Baseline, + /// The top edge of the glyph's bounding box. + Bounds, +} + +impl TryInto for TopEdgeMetric { + type Error = (); + + fn try_into(self) -> Result { + match self { + Self::Ascender => Ok(VerticalFontMetric::Ascender), + Self::CapHeight => Ok(VerticalFontMetric::CapHeight), + Self::XHeight => Ok(VerticalFontMetric::XHeight), + Self::Baseline => Ok(VerticalFontMetric::Baseline), + _ => Err(()), + } + } +} + +/// Specifies the top edge of text. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum BottomEdge { + /// An edge specified via font metrics or bounding box. + Metric(BottomEdgeMetric), + /// An edge specified as a length. + Length(Length), +} + +impl BottomEdge { + /// Determine if the edge is specified from bounding box info. + pub fn is_bounds(&self) -> bool { + matches!(self, Self::Metric(BottomEdgeMetric::Bounds)) + } + + /// Resolve the value of the text edge given a font's metrics. + pub fn resolve(self, styles: StyleChain, font: &Font, bbox: Option) -> Abs { + match self { + BottomEdge::Metric(metric) => { + if let Ok(metric) = metric.try_into() { + font.metrics().vertical(metric).resolve(styles) + } else { + bbox.map(|bbox| (font.to_em(bbox.y_min)).resolve(styles)) + .unwrap_or_default() + } + } + BottomEdge::Length(length) => length.resolve(styles), + } + } +} + +cast! { + BottomEdge, + self => match self { + Self::Metric(metric) => metric.into_value(), + Self::Length(length) => length.into_value(), + }, + v: BottomEdgeMetric => Self::Metric(v), + v: Length => Self::Length(v), +} + +/// Metrics that describe the bottom edge of text. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] +pub enum BottomEdgeMetric { + /// The baseline on which the letters rest. + Baseline, + /// The font's descender, which typically exceeds the depth of all glyphs. + Descender, + /// The bottom edge of the glyph's bounding box. + Bounds, +} + +impl TryInto for BottomEdgeMetric { + type Error = (); + + fn try_into(self) -> Result { + match self { + Self::Baseline => Ok(VerticalFontMetric::Baseline), + Self::Descender => Ok(VerticalFontMetric::Descender), + _ => Err(()), + } + } +} + /// The direction of text and inline objects in their line. #[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] pub struct TextDir(pub Smart); diff --git a/crates/typst-library/src/text/shaping.rs b/crates/typst-library/src/text/shaping.rs index 5be223905..3ccac635a 100644 --- a/crates/typst-library/src/text/shaping.rs +++ b/crates/typst-library/src/text/shaping.rs @@ -320,10 +320,9 @@ impl<'a> ShapedText<'a> { let bottom_edge = TextElem::bottom_edge_in(self.styles); // Expand top and bottom by reading the font's vertical metrics. - let mut expand = |font: &Font| { - let metrics = font.metrics(); - top.set_max(top_edge.resolve(self.styles, metrics)); - bottom.set_max(-bottom_edge.resolve(self.styles, metrics)); + let mut expand = |font: &Font, bbox: Option| { + top.set_max(top_edge.resolve(self.styles, font, bbox)); + bottom.set_max(-bottom_edge.resolve(self.styles, font, bbox)); }; if self.glyphs.is_empty() { @@ -336,13 +335,18 @@ impl<'a> ShapedText<'a> { .select(family.as_str(), self.variant) .and_then(|id| world.font(id)) { - expand(&font); + expand(&font, None); break; } } } else { for g in self.glyphs.iter() { - expand(&g.font); + let bbox = if top_edge.is_bounds() || bottom_edge.is_bounds() { + g.font.ttf().glyph_bounding_box(ttf_parser::GlyphId(g.glyph_id)) + } else { + None + }; + expand(&g.font, bbox); } } diff --git a/tests/ref/bugs/math-realize.png b/tests/ref/bugs/math-realize.png index 2a17f6297..5ce129f68 100644 Binary files a/tests/ref/bugs/math-realize.png and b/tests/ref/bugs/math-realize.png differ diff --git a/tests/ref/math/accent.png b/tests/ref/math/accent.png index 5a963b385..1065576eb 100644 Binary files a/tests/ref/math/accent.png and b/tests/ref/math/accent.png differ diff --git a/tests/ref/math/alignment.png b/tests/ref/math/alignment.png index c92e0ea98..9b5ebaf93 100644 Binary files a/tests/ref/math/alignment.png and b/tests/ref/math/alignment.png differ diff --git a/tests/ref/math/attach.png b/tests/ref/math/attach.png index 9d01e7bf0..370a2ba75 100644 Binary files a/tests/ref/math/attach.png and b/tests/ref/math/attach.png differ diff --git a/tests/ref/math/cancel.png b/tests/ref/math/cancel.png index 571edcc2b..146bb8558 100644 Binary files a/tests/ref/math/cancel.png and b/tests/ref/math/cancel.png differ diff --git a/tests/ref/math/cases.png b/tests/ref/math/cases.png index c824a8012..b9e54e753 100644 Binary files a/tests/ref/math/cases.png and b/tests/ref/math/cases.png differ diff --git a/tests/ref/math/content.png b/tests/ref/math/content.png index ce727e66e..a5f5f1378 100644 Binary files a/tests/ref/math/content.png and b/tests/ref/math/content.png differ diff --git a/tests/ref/math/font-features.png b/tests/ref/math/font-features.png index 1fff35470..f7c4b9147 100644 Binary files a/tests/ref/math/font-features.png and b/tests/ref/math/font-features.png differ diff --git a/tests/ref/math/frac.png b/tests/ref/math/frac.png index adb9ad0c4..94990208f 100644 Binary files a/tests/ref/math/frac.png and b/tests/ref/math/frac.png differ diff --git a/tests/ref/math/matrix-alignment.png b/tests/ref/math/matrix-alignment.png index b272a2906..c0acd958a 100644 Binary files a/tests/ref/math/matrix-alignment.png and b/tests/ref/math/matrix-alignment.png differ diff --git a/tests/ref/math/multiline.png b/tests/ref/math/multiline.png index 84dcb87d5..19276846d 100644 Binary files a/tests/ref/math/multiline.png and b/tests/ref/math/multiline.png differ diff --git a/tests/ref/math/op.png b/tests/ref/math/op.png index 15c7329dd..863c66848 100644 Binary files a/tests/ref/math/op.png and b/tests/ref/math/op.png differ diff --git a/tests/ref/math/spacing.png b/tests/ref/math/spacing.png index 5e717effc..4767f6f43 100644 Binary files a/tests/ref/math/spacing.png and b/tests/ref/math/spacing.png differ diff --git a/tests/ref/math/style.png b/tests/ref/math/style.png index cec04ba50..cf9625742 100644 Binary files a/tests/ref/math/style.png and b/tests/ref/math/style.png differ diff --git a/tests/ref/math/underover.png b/tests/ref/math/underover.png index 24c96b21f..62dcfefce 100644 Binary files a/tests/ref/math/underover.png and b/tests/ref/math/underover.png differ diff --git a/tests/ref/text/edge.png b/tests/ref/text/edge.png index da8ed34d5..2226af9de 100644 Binary files a/tests/ref/text/edge.png and b/tests/ref/text/edge.png differ diff --git a/tests/typ/math/attach.typ b/tests/typ/math/attach.typ index 0f404ac7f..d471551a2 100644 --- a/tests/typ/math/attach.typ +++ b/tests/typ/math/attach.typ @@ -64,6 +64,12 @@ $ sqrt(a_(1/2)^zeta), sqrt(a_alpha^(1/2)), sqrt(a_(1/2)^(3/4)) \ sqrt(attach(a, tl: 1/2, bl: 3/4)), sqrt(attach(a, tl: 1/2, bl: 3/4, tr: 1/2, br: 3/4)) $ +--- +// Test for no collisions between descenders/ascenders and attachments + +$ sup_(x in P_i) quad inf_(x in P_i) $ +$ op("fff",limits: #true)^(y) quad op("yyy", limits:#true)_(f) $ + --- // Test frame base. $ (-1)^n + (1/2 + 3)^(-1/2) $ diff --git a/tests/typ/text/edge.typ b/tests/typ/text/edge.typ index 85aff68a6..053576e84 100644 --- a/tests/typ/text/edge.typ +++ b/tests/typ/text/edge.typ @@ -9,17 +9,31 @@ From #top to #bottom ] +#let try-bounds(top, bottom) = rect(inset: 0pt, fill: conifer)[ + #set text(font: "IBM Plex Mono", top-edge: top, bottom-edge: bottom) + #top to #bottom: "yay, Typst" +] + #try("ascender", "descender") #try("ascender", "baseline") #try("cap-height", "baseline") #try("x-height", "baseline") +#try-bounds("cap-height", "baseline") +#try-bounds("bounds", "baseline") +#try-bounds("bounds", "bounds") +#try-bounds("x-height", "bounds") + #try(4pt, -2pt) #try(1pt + 0.3em, -0.15em) --- -// Error: 21-23 expected "ascender", "cap-height", "x-height", "baseline", "descender", or length, found array +// Error: 21-23 expected "ascender", "cap-height", "x-height", "baseline", "bounds", or length, found array #set text(top-edge: ()) --- -// Error: 24-26 expected "ascender", "cap-height", "x-height", "baseline", "descender", or length +// Error: 24-26 expected "baseline", "descender", "bounds", or length #set text(bottom-edge: "") + +--- +// Error: 24-36 expected "baseline", "descender", "bounds", or length +#set text(bottom-edge: "cap-height")