summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordamaxwell <damaxwell@alaska.edu>2023-07-17 08:19:46 -0800
committerGitHub <noreply@github.com>2023-07-17 18:19:46 +0200
commit7a1cd362aa150045371cc85578ef50bef2951be2 (patch)
treed23560866992a426d2cb2ab1dcb04cf1ebb82699
parent9b72ee4d221d1e9e8031e53631aaccd06841ff04 (diff)
Support for bounding box text edges (#1626)
-rw-r--r--crates/typst-library/src/math/ctx.rs24
-rw-r--r--crates/typst-library/src/math/mod.rs4
-rw-r--r--crates/typst-library/src/text/mod.rs136
-rw-r--r--crates/typst-library/src/text/shaping.rs16
-rw-r--r--tests/ref/bugs/math-realize.pngbin21228 -> 21192 bytes
-rw-r--r--tests/ref/math/accent.pngbin8038 -> 8080 bytes
-rw-r--r--tests/ref/math/alignment.pngbin8040 -> 7924 bytes
-rw-r--r--tests/ref/math/attach.pngbin28615 -> 30373 bytes
-rw-r--r--tests/ref/math/cancel.pngbin24765 -> 24765 bytes
-rw-r--r--tests/ref/math/cases.pngbin2947 -> 2979 bytes
-rw-r--r--tests/ref/math/content.pngbin7883 -> 7891 bytes
-rw-r--r--tests/ref/math/font-features.pngbin3005 -> 3023 bytes
-rw-r--r--tests/ref/math/frac.pngbin28719 -> 28867 bytes
-rw-r--r--tests/ref/math/matrix-alignment.pngbin14228 -> 14102 bytes
-rw-r--r--tests/ref/math/multiline.pngbin13479 -> 13437 bytes
-rw-r--r--tests/ref/math/op.pngbin6730 -> 6576 bytes
-rw-r--r--tests/ref/math/spacing.pngbin17881 -> 18031 bytes
-rw-r--r--tests/ref/math/style.pngbin23804 -> 23825 bytes
-rw-r--r--tests/ref/math/underover.pngbin4797 -> 4731 bytes
-rw-r--r--tests/ref/text/edge.pngbin15244 -> 28077 bytes
-rw-r--r--tests/typ/math/attach.typ6
-rw-r--r--tests/typ/text/edge.typ18
22 files changed, 177 insertions, 27 deletions
diff --git a/crates/typst-library/src/math/ctx.rs b/crates/typst-library/src/math/ctx.rs
index 999a5ccb..e6a84674 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 b5410a03..c29ad29b 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 3c3ccdef..f7c15c29 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<Rect>) -> 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<VerticalFontMetric> for TopEdgeMetric {
+ type Error = ();
+
+ fn try_into(self) -> Result<VerticalFontMetric, Self::Error> {
+ 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<Rect>) -> 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<VerticalFontMetric> for BottomEdgeMetric {
+ type Error = ();
+
+ fn try_into(self) -> Result<VerticalFontMetric, Self::Error> {
+ 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<Dir>);
diff --git a/crates/typst-library/src/text/shaping.rs b/crates/typst-library/src/text/shaping.rs
index 5be22390..3ccac635 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<ttf_parser::Rect>| {
+ 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 2a17f629..5ce129f6 100644
--- a/tests/ref/bugs/math-realize.png
+++ b/tests/ref/bugs/math-realize.png
Binary files differ
diff --git a/tests/ref/math/accent.png b/tests/ref/math/accent.png
index 5a963b38..1065576e 100644
--- a/tests/ref/math/accent.png
+++ b/tests/ref/math/accent.png
Binary files differ
diff --git a/tests/ref/math/alignment.png b/tests/ref/math/alignment.png
index c92e0ea9..9b5ebaf9 100644
--- a/tests/ref/math/alignment.png
+++ b/tests/ref/math/alignment.png
Binary files differ
diff --git a/tests/ref/math/attach.png b/tests/ref/math/attach.png
index 9d01e7bf..370a2ba7 100644
--- a/tests/ref/math/attach.png
+++ b/tests/ref/math/attach.png
Binary files differ
diff --git a/tests/ref/math/cancel.png b/tests/ref/math/cancel.png
index 571edcc2..146bb855 100644
--- a/tests/ref/math/cancel.png
+++ b/tests/ref/math/cancel.png
Binary files differ
diff --git a/tests/ref/math/cases.png b/tests/ref/math/cases.png
index c824a801..b9e54e75 100644
--- a/tests/ref/math/cases.png
+++ b/tests/ref/math/cases.png
Binary files differ
diff --git a/tests/ref/math/content.png b/tests/ref/math/content.png
index ce727e66..a5f5f137 100644
--- a/tests/ref/math/content.png
+++ b/tests/ref/math/content.png
Binary files differ
diff --git a/tests/ref/math/font-features.png b/tests/ref/math/font-features.png
index 1fff3547..f7c4b914 100644
--- a/tests/ref/math/font-features.png
+++ b/tests/ref/math/font-features.png
Binary files differ
diff --git a/tests/ref/math/frac.png b/tests/ref/math/frac.png
index adb9ad0c..94990208 100644
--- a/tests/ref/math/frac.png
+++ b/tests/ref/math/frac.png
Binary files differ
diff --git a/tests/ref/math/matrix-alignment.png b/tests/ref/math/matrix-alignment.png
index b272a290..c0acd958 100644
--- a/tests/ref/math/matrix-alignment.png
+++ b/tests/ref/math/matrix-alignment.png
Binary files differ
diff --git a/tests/ref/math/multiline.png b/tests/ref/math/multiline.png
index 84dcb87d..19276846 100644
--- a/tests/ref/math/multiline.png
+++ b/tests/ref/math/multiline.png
Binary files differ
diff --git a/tests/ref/math/op.png b/tests/ref/math/op.png
index 15c7329d..863c6684 100644
--- a/tests/ref/math/op.png
+++ b/tests/ref/math/op.png
Binary files differ
diff --git a/tests/ref/math/spacing.png b/tests/ref/math/spacing.png
index 5e717eff..4767f6f4 100644
--- a/tests/ref/math/spacing.png
+++ b/tests/ref/math/spacing.png
Binary files differ
diff --git a/tests/ref/math/style.png b/tests/ref/math/style.png
index cec04ba5..cf962574 100644
--- a/tests/ref/math/style.png
+++ b/tests/ref/math/style.png
Binary files differ
diff --git a/tests/ref/math/underover.png b/tests/ref/math/underover.png
index 24c96b21..62dcfefc 100644
--- a/tests/ref/math/underover.png
+++ b/tests/ref/math/underover.png
Binary files differ
diff --git a/tests/ref/text/edge.png b/tests/ref/text/edge.png
index da8ed34d..2226af9d 100644
--- a/tests/ref/text/edge.png
+++ b/tests/ref/text/edge.png
Binary files differ
diff --git a/tests/typ/math/attach.typ b/tests/typ/math/attach.typ
index 0f404ac7..d471551a 100644
--- a/tests/typ/math/attach.typ
+++ b/tests/typ/math/attach.typ
@@ -65,6 +65,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, 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 85aff68a..053576e8 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")