Support for bounding box text edges (#1626)
@ -5,7 +5,7 @@ use typst::model::realize;
|
|||||||
use unicode_segmentation::UnicodeSegmentation;
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::text::tags;
|
use crate::text::{tags, BottomEdge, BottomEdgeMetric, TopEdge, TopEdgeMetric};
|
||||||
|
|
||||||
macro_rules! scaled {
|
macro_rules! scaled {
|
||||||
($ctx:expr, text: $text:ident, display: $display:ident $(,)?) => {
|
($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);
|
style = style.with_italic(false);
|
||||||
}
|
}
|
||||||
let text: EcoString = text.chars().map(|c| style.styled_char(c)).collect();
|
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)
|
FrameFragment::new(self, frame)
|
||||||
.with_class(MathClass::Alphabetic)
|
.with_class(MathClass::Alphabetic)
|
||||||
.with_spaced(spaced)
|
.with_spaced(spaced)
|
||||||
|
@ -292,9 +292,9 @@ impl Layout for EquationElem {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let slack = ParElem::leading_in(styles) * 0.7;
|
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 =
|
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 ascent = top_edge.max(frame.ascent() - slack);
|
||||||
let descent = bottom_edge.max(frame.descent() - slack);
|
let descent = bottom_edge.max(frame.descent() - slack);
|
||||||
|
@ -15,7 +15,8 @@ pub use self::shaping::*;
|
|||||||
pub use self::shift::*;
|
pub use self::shift::*;
|
||||||
|
|
||||||
use rustybuzz::Tag;
|
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::layout::ParElem;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
@ -245,8 +246,8 @@ pub struct TextElem {
|
|||||||
/// #set text(top-edge: "cap-height")
|
/// #set text(top-edge: "cap-height")
|
||||||
/// #rect(fill: aqua)[Typst]
|
/// #rect(fill: aqua)[Typst]
|
||||||
/// ```
|
/// ```
|
||||||
#[default(TextEdge::Metric(VerticalFontMetric::CapHeight))]
|
#[default(TopEdge::Metric(TopEdgeMetric::CapHeight))]
|
||||||
pub top_edge: TextEdge,
|
pub top_edge: TopEdge,
|
||||||
|
|
||||||
/// The bottom end of the conceptual frame around the text used for layout
|
/// The bottom end of the conceptual frame around the text used for layout
|
||||||
/// and positioning. This affects the size of containers that hold text.
|
/// and positioning. This affects the size of containers that hold text.
|
||||||
@ -261,8 +262,8 @@ pub struct TextElem {
|
|||||||
/// #set text(bottom-edge: "descender")
|
/// #set text(bottom-edge: "descender")
|
||||||
/// #rect(fill: aqua)[Typst]
|
/// #rect(fill: aqua)[Typst]
|
||||||
/// ```
|
/// ```
|
||||||
#[default(TextEdge::Metric(VerticalFontMetric::Baseline))]
|
#[default(BottomEdge::Metric(BottomEdgeMetric::Baseline))]
|
||||||
pub bottom_edge: TextEdge,
|
pub bottom_edge: BottomEdge,
|
||||||
|
|
||||||
/// An [ISO 639-1/2/3 language code.](https://en.wikipedia.org/wiki/ISO_639)
|
/// An [ISO 639-1/2/3 language code.](https://en.wikipedia.org/wiki/ISO_639)
|
||||||
///
|
///
|
||||||
@ -606,35 +607,140 @@ cast! {
|
|||||||
v: Length => Self(v),
|
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)]
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||||
pub enum TextEdge {
|
pub enum TopEdge {
|
||||||
/// An edge specified using one of the well-known font metrics.
|
/// An edge specified via font metrics or bounding box.
|
||||||
Metric(VerticalFontMetric),
|
Metric(TopEdgeMetric),
|
||||||
/// An edge specified as a length.
|
/// An edge specified as a length.
|
||||||
Length(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.
|
/// 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 {
|
match self {
|
||||||
Self::Metric(metric) => metrics.vertical(metric).resolve(styles),
|
TopEdge::Metric(metric) => {
|
||||||
Self::Length(length) => length.resolve(styles),
|
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! {
|
cast! {
|
||||||
TextEdge,
|
TopEdge,
|
||||||
self => match self {
|
self => match self {
|
||||||
Self::Metric(metric) => metric.into_value(),
|
Self::Metric(metric) => metric.into_value(),
|
||||||
Self::Length(length) => length.into_value(),
|
Self::Length(length) => length.into_value(),
|
||||||
},
|
},
|
||||||
v: VerticalFontMetric => Self::Metric(v),
|
v: TopEdgeMetric => Self::Metric(v),
|
||||||
v: Length => Self::Length(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.
|
/// The direction of text and inline objects in their line.
|
||||||
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)]
|
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)]
|
||||||
pub struct TextDir(pub Smart<Dir>);
|
pub struct TextDir(pub Smart<Dir>);
|
||||||
|
@ -320,10 +320,9 @@ impl<'a> ShapedText<'a> {
|
|||||||
let bottom_edge = TextElem::bottom_edge_in(self.styles);
|
let bottom_edge = TextElem::bottom_edge_in(self.styles);
|
||||||
|
|
||||||
// Expand top and bottom by reading the font's vertical metrics.
|
// Expand top and bottom by reading the font's vertical metrics.
|
||||||
let mut expand = |font: &Font| {
|
let mut expand = |font: &Font, bbox: Option<ttf_parser::Rect>| {
|
||||||
let metrics = font.metrics();
|
top.set_max(top_edge.resolve(self.styles, font, bbox));
|
||||||
top.set_max(top_edge.resolve(self.styles, metrics));
|
bottom.set_max(-bottom_edge.resolve(self.styles, font, bbox));
|
||||||
bottom.set_max(-bottom_edge.resolve(self.styles, metrics));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if self.glyphs.is_empty() {
|
if self.glyphs.is_empty() {
|
||||||
@ -336,13 +335,18 @@ impl<'a> ShapedText<'a> {
|
|||||||
.select(family.as_str(), self.variant)
|
.select(family.as_str(), self.variant)
|
||||||
.and_then(|id| world.font(id))
|
.and_then(|id| world.font(id))
|
||||||
{
|
{
|
||||||
expand(&font);
|
expand(&font, None);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for g in self.glyphs.iter() {
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 7.9 KiB |
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 7.7 KiB |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 7.7 KiB |
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 6.4 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 27 KiB |
@ -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)),
|
||||||
sqrt(attach(a, tl: 1/2, bl: 3/4, tr: 1/2, br: 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.
|
// Test frame base.
|
||||||
$ (-1)^n + (1/2 + 3)^(-1/2) $
|
$ (-1)^n + (1/2 + 3)^(-1/2) $
|
||||||
|
@ -9,17 +9,31 @@
|
|||||||
From #top to #bottom
|
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", "descender")
|
||||||
#try("ascender", "baseline")
|
#try("ascender", "baseline")
|
||||||
#try("cap-height", "baseline")
|
#try("cap-height", "baseline")
|
||||||
#try("x-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(4pt, -2pt)
|
||||||
#try(1pt + 0.3em, -0.15em)
|
#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: ())
|
#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: "")
|
#set text(bottom-edge: "")
|
||||||
|
|
||||||
|
---
|
||||||
|
// Error: 24-36 expected "baseline", "descender", "bounds", or length
|
||||||
|
#set text(bottom-edge: "cap-height")
|
||||||
|