Merge ce7636b5bd8bfa5aff2490a973ff925f984e3a22 into 9b09146a6b5e936966ed7ee73bce9dd2df3810ae
@ -206,7 +206,7 @@ pub fn collect<'a>(
|
||||
}
|
||||
InlineItem::Frame(mut frame) => {
|
||||
frame.modify(&FrameModifiers::get_in(styles));
|
||||
apply_baseline_shift(&mut frame, styles);
|
||||
apply_shift(&engine.world, &mut frame, styles);
|
||||
collector.push_item(Item::Frame(frame));
|
||||
}
|
||||
}
|
||||
@ -221,7 +221,7 @@ pub fn collect<'a>(
|
||||
let mut frame = layout_and_modify(styles, |styles| {
|
||||
layout_box(elem, engine, loc, styles, region)
|
||||
})?;
|
||||
apply_baseline_shift(&mut frame, styles);
|
||||
apply_shift(&engine.world, &mut frame, styles);
|
||||
collector.push_item(Item::Frame(frame));
|
||||
}
|
||||
} else if let Some(elem) = child.to_packed::<TagElem>() {
|
||||
|
@ -5,7 +5,7 @@ use typst_library::engine::Engine;
|
||||
use typst_library::introspection::{SplitLocator, Tag};
|
||||
use typst_library::layout::{Abs, Dir, Em, Fr, Frame, FrameItem, Point};
|
||||
use typst_library::model::ParLineMarker;
|
||||
use typst_library::text::{Lang, TextElem};
|
||||
use typst_library::text::{variant, Lang, TextElem};
|
||||
use typst_utils::Numeric;
|
||||
|
||||
use super::*;
|
||||
@ -412,9 +412,30 @@ fn should_repeat_hyphen(pred_line: &Line, text: &str) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply the current baseline shift to a frame.
|
||||
pub fn apply_baseline_shift(frame: &mut Frame, styles: StyleChain) {
|
||||
frame.translate(Point::with_y(TextElem::baseline_in(styles)));
|
||||
/// Apply the current baseline shift and italic compensation to a frame.
|
||||
pub fn apply_shift<'a>(
|
||||
world: &Tracked<'a, dyn World + 'a>,
|
||||
frame: &mut Frame,
|
||||
styles: StyleChain,
|
||||
) {
|
||||
let mut baseline = TextElem::baseline_in(styles);
|
||||
let mut compensation = Abs::zero();
|
||||
if let Some(scripts) = TextElem::subperscript_in(styles) {
|
||||
let font_metrics = TextElem::font_in(styles)
|
||||
.into_iter()
|
||||
.find_map(|family| {
|
||||
world
|
||||
.book()
|
||||
.select(family.as_str(), variant(styles))
|
||||
.and_then(|id| world.font(id))
|
||||
})
|
||||
.map_or(scripts.kind.default_metrics(), |f| {
|
||||
scripts.kind.read_metrics(f.metrics())
|
||||
});
|
||||
baseline -= scripts.shift.unwrap_or(font_metrics.vertical_offset).resolve(styles);
|
||||
compensation += font_metrics.horizontal_offset.resolve(styles);
|
||||
}
|
||||
frame.translate(Point::new(compensation, baseline));
|
||||
}
|
||||
|
||||
/// Commit to a line and build its frame.
|
||||
@ -519,7 +540,7 @@ pub fn commit(
|
||||
let mut frame = layout_and_modify(*styles, |styles| {
|
||||
layout_box(elem, engine, loc.relayout(), styles, region)
|
||||
})?;
|
||||
apply_baseline_shift(&mut frame, *styles);
|
||||
apply_shift(&engine.world, &mut frame, *styles);
|
||||
push(&mut offset, frame);
|
||||
} else {
|
||||
offset += amount;
|
||||
|
@ -28,7 +28,7 @@ use typst_utils::{Numeric, SliceExt};
|
||||
use self::collect::{collect, Item, Segment, SpanMapper};
|
||||
use self::deco::decorate;
|
||||
use self::finalize::finalize;
|
||||
use self::line::{apply_baseline_shift, commit, line, Line};
|
||||
use self::line::{apply_shift, commit, line, Line};
|
||||
use self::linebreak::{linebreak, Breakpoint};
|
||||
use self::prepare::{prepare, Preparation};
|
||||
use self::shaping::{
|
||||
|
@ -5,14 +5,15 @@ use std::sync::Arc;
|
||||
|
||||
use az::SaturatingAs;
|
||||
use ecow::EcoString;
|
||||
use rustybuzz::{BufferFlags, ShapePlan, UnicodeBuffer};
|
||||
use rustybuzz::{BufferFlags, Feature, ShapePlan, UnicodeBuffer};
|
||||
use ttf_parser::gsub::SubstitutionSubtable;
|
||||
use ttf_parser::Tag;
|
||||
use typst_library::engine::Engine;
|
||||
use typst_library::foundations::{Smart, StyleChain};
|
||||
use typst_library::layout::{Abs, Dir, Em, Frame, FrameItem, Point, Size};
|
||||
use typst_library::text::{
|
||||
families, features, is_default_ignorable, variant, Font, FontFamily, FontVariant,
|
||||
Glyph, Lang, Region, TextEdgeBounds, TextElem, TextItem,
|
||||
Glyph, Lang, Region, ScriptSettings, TextEdgeBounds, TextElem, TextItem,
|
||||
};
|
||||
use typst_library::World;
|
||||
use typst_utils::SliceExt;
|
||||
@ -64,6 +65,8 @@ pub struct ShapedGlyph {
|
||||
pub x_offset: Em,
|
||||
/// The vertical offset of the glyph.
|
||||
pub y_offset: Em,
|
||||
/// How much to scale the glyph compared to its normal size.
|
||||
pub scale: Em,
|
||||
/// The adjustability of the glyph.
|
||||
pub adjustability: Adjustability,
|
||||
/// The byte range of this glyph's cluster in the full inline layout. A
|
||||
@ -230,8 +233,10 @@ impl<'a> ShapedText<'a> {
|
||||
let stroke = TextElem::stroke_in(self.styles);
|
||||
let span_offset = TextElem::span_offset_in(self.styles);
|
||||
|
||||
for ((font, y_offset), group) in
|
||||
self.glyphs.as_ref().group_by_key(|g| (g.font.clone(), g.y_offset))
|
||||
for ((font, y_offset, scale), group) in self
|
||||
.glyphs
|
||||
.as_ref()
|
||||
.group_by_key(|g| (g.font.clone(), g.y_offset, g.scale))
|
||||
{
|
||||
let mut range = group[0].range.clone();
|
||||
for glyph in group {
|
||||
@ -304,7 +309,7 @@ impl<'a> ShapedText<'a> {
|
||||
|
||||
let item = TextItem {
|
||||
font,
|
||||
size: self.size,
|
||||
size: scale.at(self.size),
|
||||
lang: self.lang,
|
||||
region: self.region,
|
||||
fill: fill.clone(),
|
||||
@ -420,7 +425,7 @@ impl<'a> ShapedText<'a> {
|
||||
styles: self.styles,
|
||||
size: self.size,
|
||||
variant: self.variant,
|
||||
width: glyphs.iter().map(|g| g.x_advance).sum::<Em>().at(self.size),
|
||||
width: glyphs_width(glyphs).at(self.size),
|
||||
glyphs: Cow::Borrowed(glyphs),
|
||||
}
|
||||
} else {
|
||||
@ -491,6 +496,7 @@ impl<'a> ShapedText<'a> {
|
||||
x_advance,
|
||||
x_offset: Em::zero(),
|
||||
y_offset: Em::zero(),
|
||||
scale: Em::one(),
|
||||
adjustability: Adjustability::default(),
|
||||
range,
|
||||
safe_to_break: true,
|
||||
@ -666,6 +672,7 @@ fn shape<'a>(
|
||||
region: Option<Region>,
|
||||
) -> ShapedText<'a> {
|
||||
let size = TextElem::size_in(styles);
|
||||
let script_settings = TextElem::subperscript_in(styles);
|
||||
let mut ctx = ShapingContext {
|
||||
engine,
|
||||
size,
|
||||
@ -679,7 +686,7 @@ fn shape<'a>(
|
||||
};
|
||||
|
||||
if !text.is_empty() {
|
||||
shape_segment(&mut ctx, base, text, families(styles));
|
||||
shape_segment(&mut ctx, base, text, families(styles), script_settings);
|
||||
}
|
||||
|
||||
track_and_space(&mut ctx);
|
||||
@ -699,11 +706,17 @@ fn shape<'a>(
|
||||
styles,
|
||||
variant: ctx.variant,
|
||||
size,
|
||||
width: ctx.glyphs.iter().map(|g| g.x_advance).sum::<Em>().at(size),
|
||||
width: glyphs_width(&ctx.glyphs).at(size),
|
||||
glyphs: Cow::Owned(ctx.glyphs),
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes the width of a run of glyphs relative to the font size, accounting
|
||||
/// for their individual scaling factors and other font metrics.
|
||||
fn glyphs_width(glyphs: &[ShapedGlyph]) -> Em {
|
||||
glyphs.iter().map(|g| g.x_advance * g.scale.get()).sum()
|
||||
}
|
||||
|
||||
/// Holds shaping results and metadata common to all shaped segments.
|
||||
struct ShapingContext<'a, 'v> {
|
||||
engine: &'a Engine<'v>,
|
||||
@ -723,6 +736,7 @@ fn shape_segment<'a>(
|
||||
base: usize,
|
||||
text: &str,
|
||||
mut families: impl Iterator<Item = &'a FontFamily> + Clone,
|
||||
script_settings: Option<ScriptSettings>,
|
||||
) {
|
||||
// Don't try shaping newlines, tabs, or default ignorables.
|
||||
if text
|
||||
@ -789,6 +803,52 @@ fn shape_segment<'a>(
|
||||
// text extraction.
|
||||
buffer.set_flags(BufferFlags::REMOVE_DEFAULT_IGNORABLES);
|
||||
|
||||
// Those values determine how the rendered text should be transformed to
|
||||
// display sub-/super-scripts properly. If the text is not scripted, or if
|
||||
// the OpenType feature can be used, the rendered text should not be
|
||||
// transformed in any way, and so those values are neutral (`(0, 0, 1)`). If
|
||||
// scripts should be synthesized, those values determine how to transform
|
||||
// the rendered text to display scripts as expected.
|
||||
let (script_shift, script_compensation, scale) = match script_settings {
|
||||
None => (Em::zero(), Em::zero(), Em::one()),
|
||||
Some(settings) => settings
|
||||
.typographic
|
||||
.then(|| {
|
||||
// If typographic scripts are enabled (i.e., we want to use the
|
||||
// OpenType feature instead of synthesizing if possible), we add
|
||||
// "subs"/"sups" to the feature list if supported by the font.
|
||||
// In case of a problem, we just early exit
|
||||
let gsub = font.rusty().tables().gsub?;
|
||||
let subtable_index =
|
||||
gsub.features.find(settings.kind.feature())?.lookup_indices.get(0)?;
|
||||
let coverage = gsub
|
||||
.lookups
|
||||
.get(subtable_index)?
|
||||
.subtables
|
||||
.get::<SubstitutionSubtable>(0)?
|
||||
.coverage();
|
||||
text.chars()
|
||||
.all(|c| {
|
||||
font.rusty().glyph_index(c).is_some_and(|i| coverage.contains(i))
|
||||
})
|
||||
.then(|| {
|
||||
ctx.features.push(Feature::new(settings.kind.feature(), 1, ..));
|
||||
(Em::zero(), Em::zero(), Em::one())
|
||||
})
|
||||
})
|
||||
// Reunite the cases where `typographic` is `false` or where using
|
||||
// the OpenType feature would not work.
|
||||
.flatten()
|
||||
.unwrap_or_else(|| {
|
||||
let script_metrics = settings.kind.read_metrics(font.metrics());
|
||||
(
|
||||
settings.shift.unwrap_or(script_metrics.vertical_offset),
|
||||
script_metrics.horizontal_offset,
|
||||
settings.size.unwrap_or(script_metrics.height),
|
||||
)
|
||||
}),
|
||||
};
|
||||
|
||||
// Prepare the shape plan. This plan depends on direction, script, language,
|
||||
// and features, but is independent from the text and can thus be memoized.
|
||||
let plan = create_shape_plan(
|
||||
@ -869,8 +929,9 @@ fn shape_segment<'a>(
|
||||
glyph_id: info.glyph_id as u16,
|
||||
// TODO: Don't ignore y_advance.
|
||||
x_advance,
|
||||
x_offset: font.to_em(pos[i].x_offset),
|
||||
y_offset: font.to_em(pos[i].y_offset),
|
||||
x_offset: font.to_em(pos[i].x_offset) + script_compensation,
|
||||
y_offset: font.to_em(pos[i].y_offset) + script_shift,
|
||||
scale,
|
||||
adjustability: Adjustability::default(),
|
||||
range: start..end,
|
||||
safe_to_break: !info.unsafe_to_break(),
|
||||
@ -923,7 +984,13 @@ fn shape_segment<'a>(
|
||||
}
|
||||
|
||||
// Recursively shape the tofu sequence with the next family.
|
||||
shape_segment(ctx, base + start, &text[start..end], families.clone());
|
||||
shape_segment(
|
||||
ctx,
|
||||
base + start,
|
||||
&text[start..end],
|
||||
families.clone(),
|
||||
script_settings,
|
||||
);
|
||||
}
|
||||
|
||||
i += 1;
|
||||
@ -963,6 +1030,7 @@ fn shape_tofus(ctx: &mut ShapingContext, base: usize, text: &str, font: Font) {
|
||||
x_advance,
|
||||
x_offset: Em::zero(),
|
||||
y_offset: Em::zero(),
|
||||
scale: Em::one(),
|
||||
adjustability: Adjustability::default(),
|
||||
range: start..end,
|
||||
safe_to_break: true,
|
||||
|
@ -91,6 +91,10 @@ impl Length {
|
||||
hint: "or use `length.abs.{unit}()` instead to ignore its em component"
|
||||
)
|
||||
}
|
||||
|
||||
pub fn to_em(&self, text_size: Abs) -> Em {
|
||||
self.em + Em::new(self.abs / text_size)
|
||||
}
|
||||
}
|
||||
|
||||
#[scope]
|
||||
|
@ -215,6 +215,10 @@ pub struct FontMetrics {
|
||||
pub underline: LineMetrics,
|
||||
/// Recommended metrics for an overline.
|
||||
pub overline: LineMetrics,
|
||||
/// Metrics for subscripts.
|
||||
pub subscript: ScriptMetrics,
|
||||
/// Metrics for superscripts.
|
||||
pub superscript: ScriptMetrics,
|
||||
}
|
||||
|
||||
impl FontMetrics {
|
||||
@ -227,6 +231,7 @@ impl FontMetrics {
|
||||
let cap_height = ttf.capital_height().filter(|&h| h > 0).map_or(ascender, to_em);
|
||||
let x_height = ttf.x_height().filter(|&h| h > 0).map_or(ascender, to_em);
|
||||
let descender = to_em(ttf.typographic_descender().unwrap_or(ttf.descender()));
|
||||
|
||||
let strikeout = ttf.strikeout_metrics();
|
||||
let underline = ttf.underline_metrics();
|
||||
|
||||
@ -249,6 +254,26 @@ impl FontMetrics {
|
||||
thickness: underline.thickness,
|
||||
};
|
||||
|
||||
let subscript = match ttf.subscript_metrics() {
|
||||
None => ScriptMetrics::default_subscript(),
|
||||
Some(metrics) => ScriptMetrics {
|
||||
width: to_em(metrics.x_size),
|
||||
height: to_em(metrics.y_size),
|
||||
horizontal_offset: to_em(metrics.x_offset),
|
||||
vertical_offset: -to_em(metrics.y_offset),
|
||||
},
|
||||
};
|
||||
|
||||
let superscript = match ttf.superscript_metrics() {
|
||||
None => ScriptMetrics::default_superscript(),
|
||||
Some(metrics) => ScriptMetrics {
|
||||
width: to_em(metrics.x_size),
|
||||
height: to_em(metrics.y_size),
|
||||
horizontal_offset: to_em(metrics.x_offset),
|
||||
vertical_offset: to_em(metrics.y_offset),
|
||||
},
|
||||
};
|
||||
|
||||
Self {
|
||||
units_per_em,
|
||||
ascender,
|
||||
@ -258,6 +283,8 @@ impl FontMetrics {
|
||||
strikethrough,
|
||||
underline,
|
||||
overline,
|
||||
superscript,
|
||||
subscript,
|
||||
}
|
||||
}
|
||||
|
||||
@ -283,6 +310,34 @@ pub struct LineMetrics {
|
||||
pub thickness: Em,
|
||||
}
|
||||
|
||||
/// Metrics for subscripts or superscripts.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct ScriptMetrics {
|
||||
pub width: Em,
|
||||
pub height: Em,
|
||||
pub horizontal_offset: Em,
|
||||
pub vertical_offset: Em,
|
||||
}
|
||||
|
||||
impl ScriptMetrics {
|
||||
pub const fn default_with_vertical_offset(offset: Em) -> Self {
|
||||
Self {
|
||||
width: Em::new(0.6),
|
||||
height: Em::new(0.6),
|
||||
horizontal_offset: Em::zero(),
|
||||
vertical_offset: offset,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn default_subscript() -> Self {
|
||||
Self::default_with_vertical_offset(Em::new(-0.2))
|
||||
}
|
||||
|
||||
pub const fn default_superscript() -> Self {
|
||||
Self::default_with_vertical_offset(Em::new(0.5))
|
||||
}
|
||||
}
|
||||
|
||||
/// Identifies a vertical metric of a font.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
|
||||
pub enum VerticalFontMetric {
|
||||
|
@ -752,6 +752,11 @@ pub struct TextElem {
|
||||
#[internal]
|
||||
#[ghost]
|
||||
pub smallcaps: Option<Smallcaps>,
|
||||
|
||||
/// Whether subscripts or superscripts are enabled.
|
||||
#[internal]
|
||||
#[ghost]
|
||||
pub subperscript: Option<ScriptSettings>,
|
||||
}
|
||||
|
||||
impl TextElem {
|
||||
|
@ -1,11 +1,10 @@
|
||||
use ecow::EcoString;
|
||||
|
||||
use crate::diag::SourceResult;
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{elem, Content, Packed, SequenceElem, Show, StyleChain};
|
||||
use crate::foundations::{elem, Content, Packed, Show, Smart, StyleChain};
|
||||
use crate::layout::{Em, Length};
|
||||
use crate::text::{variant, SpaceElem, TextElem, TextSize};
|
||||
use crate::World;
|
||||
use crate::text::{FontMetrics, TextElem, TextSize};
|
||||
use ttf_parser::Tag;
|
||||
use typst_library::text::ScriptMetrics;
|
||||
|
||||
/// Renders text in subscript.
|
||||
///
|
||||
@ -17,11 +16,17 @@ use crate::World;
|
||||
/// ```
|
||||
#[elem(title = "Subscript", Show)]
|
||||
pub struct SubElem {
|
||||
/// Whether to prefer the dedicated subscript characters of the font.
|
||||
/// Whether to create artificial subscripts by shifting and scaling down
|
||||
/// regular glyphs.
|
||||
///
|
||||
/// If this is enabled, Typst first tries to transform the text to subscript
|
||||
/// codepoints. If that fails, it falls back to rendering lowered and shrunk
|
||||
/// normal letters.
|
||||
/// Ideally, subscripts glyphs should be provided by the font (using the
|
||||
/// `subs` OpenType feature). Otherwise, Typst is able to synthesize
|
||||
/// subscripts as explained above.
|
||||
///
|
||||
/// When this is set to `{false}`, synthesized glyphs will be used
|
||||
/// regardless of whether the font provides dedicated subscript glyphs. When
|
||||
/// `{true}`, synthesized glyphs may still be used in case the font does not
|
||||
/// provide the necessary subscript glyphs.
|
||||
///
|
||||
/// ```example
|
||||
/// N#sub(typographic: true)[1]
|
||||
@ -30,17 +35,19 @@ pub struct SubElem {
|
||||
#[default(true)]
|
||||
pub typographic: bool,
|
||||
|
||||
/// The baseline shift for synthetic subscripts. Does not apply if
|
||||
/// `typographic` is true and the font has subscript codepoints for the
|
||||
/// given `body`.
|
||||
#[default(Em::new(0.2).into())]
|
||||
pub baseline: Length,
|
||||
/// The baseline shift for synthesized subscripts.
|
||||
///
|
||||
/// This only applies to synthesized subscripts. In other words, this has no
|
||||
/// effect if `typographic` is `{true}` and the font provides the necessary
|
||||
/// subscript glyphs.
|
||||
pub baseline: Smart<Length>,
|
||||
|
||||
/// The font size for synthetic subscripts. Does not apply if
|
||||
/// `typographic` is true and the font has subscript codepoints for the
|
||||
/// given `body`.
|
||||
#[default(TextSize(Em::new(0.6).into()))]
|
||||
pub size: TextSize,
|
||||
/// The font size for synthesized subscripts.
|
||||
///
|
||||
/// This only applies to synthesized subscripts. In other words, this has no
|
||||
/// effect if `typographic` is `{true}` and the font provides the necessary
|
||||
/// subscript glyphs.
|
||||
pub size: Smart<TextSize>,
|
||||
|
||||
/// The text to display in subscript.
|
||||
#[required]
|
||||
@ -49,20 +56,17 @@ pub struct SubElem {
|
||||
|
||||
impl Show for Packed<SubElem> {
|
||||
#[typst_macros::time(name = "sub", span = self.span())]
|
||||
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
|
||||
let body = self.body.clone();
|
||||
|
||||
if self.typographic(styles) {
|
||||
if let Some(text) = convert_script(&body, true) {
|
||||
if is_shapable(engine, &text, styles) {
|
||||
return Ok(TextElem::packed(text));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(body
|
||||
.styled(TextElem::set_baseline(self.baseline(styles)))
|
||||
.styled(TextElem::set_size(self.size(styles))))
|
||||
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
|
||||
let outer_text_size = TextElem::size_in(styles);
|
||||
Ok(self
|
||||
.body
|
||||
.clone()
|
||||
.styled(TextElem::set_subperscript(Some(ScriptSettings {
|
||||
typographic: self.typographic(styles),
|
||||
shift: self.baseline(styles).map(|l| -l.to_em(outer_text_size)),
|
||||
size: self.size(styles).map(|t| t.0.to_em(outer_text_size)),
|
||||
kind: ScriptKind::Sub,
|
||||
}))))
|
||||
}
|
||||
}
|
||||
|
||||
@ -76,11 +80,17 @@ impl Show for Packed<SubElem> {
|
||||
/// ```
|
||||
#[elem(title = "Superscript", Show)]
|
||||
pub struct SuperElem {
|
||||
/// Whether to prefer the dedicated superscript characters of the font.
|
||||
/// Whether to create artificial superscripts by shifting and scaling down
|
||||
/// regular glyphs.
|
||||
///
|
||||
/// If this is enabled, Typst first tries to transform the text to
|
||||
/// superscript codepoints. If that fails, it falls back to rendering
|
||||
/// raised and shrunk normal letters.
|
||||
/// Ideally, superscripts glyphs should be provided by the font (using the
|
||||
/// `subs` OpenType feature). Otherwise, Typst is able to synthesize
|
||||
/// superscripts as explained above.
|
||||
///
|
||||
/// When this is set to `{false}`, synthesized glyphs will be used
|
||||
/// regardless of whether the font provides dedicated superscript glyphs.
|
||||
/// When `{true}`, synthesized glyphs may still be used in case the font
|
||||
/// does not provide the necessary superscript glyphs.
|
||||
///
|
||||
/// ```example
|
||||
/// N#super(typographic: true)[1]
|
||||
@ -89,17 +99,19 @@ pub struct SuperElem {
|
||||
#[default(true)]
|
||||
pub typographic: bool,
|
||||
|
||||
/// The baseline shift for synthetic superscripts. Does not apply if
|
||||
/// `typographic` is true and the font has superscript codepoints for the
|
||||
/// given `body`.
|
||||
#[default(Em::new(-0.5).into())]
|
||||
pub baseline: Length,
|
||||
/// The baseline shift for synthesized superscripts.
|
||||
///
|
||||
/// This only applies to synthesized superscripts. In other words, this has
|
||||
/// no effect if `typographic` is `{true}` and the font provides the
|
||||
/// necessary superscript glyphs.
|
||||
pub baseline: Smart<Length>,
|
||||
|
||||
/// The font size for synthetic superscripts. Does not apply if
|
||||
/// `typographic` is true and the font has superscript codepoints for the
|
||||
/// given `body`.
|
||||
#[default(TextSize(Em::new(0.6).into()))]
|
||||
pub size: TextSize,
|
||||
/// The font size for synthesized superscripts.
|
||||
///
|
||||
/// This only applies to synthesized superscripts. In other words, this has
|
||||
/// no effect if `typographic` is `{true}` and the font provides the
|
||||
/// necessary superscript glyphs.
|
||||
pub size: Smart<TextSize>,
|
||||
|
||||
/// The text to display in superscript.
|
||||
#[required]
|
||||
@ -108,107 +120,55 @@ pub struct SuperElem {
|
||||
|
||||
impl Show for Packed<SuperElem> {
|
||||
#[typst_macros::time(name = "super", span = self.span())]
|
||||
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
|
||||
let body = self.body.clone();
|
||||
|
||||
if self.typographic(styles) {
|
||||
if let Some(text) = convert_script(&body, false) {
|
||||
if is_shapable(engine, &text, styles) {
|
||||
return Ok(TextElem::packed(text));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(body
|
||||
.styled(TextElem::set_baseline(self.baseline(styles)))
|
||||
.styled(TextElem::set_size(self.size(styles))))
|
||||
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
|
||||
let outer_text_size = TextElem::size_in(styles);
|
||||
Ok(self
|
||||
.body
|
||||
.clone()
|
||||
.styled(TextElem::set_subperscript(Some(ScriptSettings {
|
||||
typographic: self.typographic(styles),
|
||||
shift: self.baseline(styles).map(|l| -l.to_em(outer_text_size)),
|
||||
size: self.size(styles).map(|t| t.0.to_em(outer_text_size)),
|
||||
kind: ScriptKind::Super,
|
||||
}))))
|
||||
}
|
||||
}
|
||||
|
||||
/// Find and transform the text contained in `content` to the given script kind
|
||||
/// if and only if it only consists of `Text`, `Space`, and `Empty` leaves.
|
||||
fn convert_script(content: &Content, sub: bool) -> Option<EcoString> {
|
||||
if content.is::<SpaceElem>() {
|
||||
Some(' '.into())
|
||||
} else if let Some(elem) = content.to_packed::<TextElem>() {
|
||||
if sub {
|
||||
elem.text.chars().map(to_subscript_codepoint).collect()
|
||||
} else {
|
||||
elem.text.chars().map(to_superscript_codepoint).collect()
|
||||
}
|
||||
} else if let Some(sequence) = content.to_packed::<SequenceElem>() {
|
||||
sequence
|
||||
.children
|
||||
.iter()
|
||||
.map(|item| convert_script(item, sub))
|
||||
.collect()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum ScriptKind {
|
||||
Sub,
|
||||
Super,
|
||||
}
|
||||
|
||||
/// Checks whether the first retrievable family contains all code points of the
|
||||
/// given string.
|
||||
fn is_shapable(engine: &Engine, text: &str, styles: StyleChain) -> bool {
|
||||
let world = engine.world;
|
||||
for family in TextElem::font_in(styles) {
|
||||
if let Some(font) = world
|
||||
.book()
|
||||
.select(family.as_str(), variant(styles))
|
||||
.and_then(|id| world.font(id))
|
||||
{
|
||||
let covers = family.covers();
|
||||
return text.chars().all(|c| {
|
||||
covers.is_none_or(|cov| cov.is_match(c.encode_utf8(&mut [0; 4])))
|
||||
&& font.ttf().glyph_index(c).is_some()
|
||||
});
|
||||
impl ScriptKind {
|
||||
pub const fn default_metrics(self) -> ScriptMetrics {
|
||||
match self {
|
||||
Self::Sub => ScriptMetrics::default_subscript(),
|
||||
Self::Super => ScriptMetrics::default_superscript(),
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
pub const fn read_metrics(self, font_metrics: &FontMetrics) -> ScriptMetrics {
|
||||
match self {
|
||||
Self::Sub => font_metrics.subscript,
|
||||
Self::Super => font_metrics.superscript,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a character to its corresponding Unicode superscript.
|
||||
fn to_superscript_codepoint(c: char) -> Option<char> {
|
||||
match c {
|
||||
'1' => Some('¹'),
|
||||
'2' => Some('²'),
|
||||
'3' => Some('³'),
|
||||
'0' | '4'..='9' => char::from_u32(c as u32 - '0' as u32 + '⁰' as u32),
|
||||
'+' => Some('⁺'),
|
||||
'−' => Some('⁻'),
|
||||
'=' => Some('⁼'),
|
||||
'(' => Some('⁽'),
|
||||
')' => Some('⁾'),
|
||||
'n' => Some('ⁿ'),
|
||||
'i' => Some('ⁱ'),
|
||||
' ' => Some(' '),
|
||||
_ => None,
|
||||
/// The corresponding OpenType feature.
|
||||
pub const fn feature(self) -> Tag {
|
||||
match self {
|
||||
Self::Sub => Tag::from_bytes(b"subs"),
|
||||
Self::Super => Tag::from_bytes(b"sups"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a character to its corresponding Unicode subscript.
|
||||
fn to_subscript_codepoint(c: char) -> Option<char> {
|
||||
match c {
|
||||
'0'..='9' => char::from_u32(c as u32 - '0' as u32 + '₀' as u32),
|
||||
'+' => Some('₊'),
|
||||
'−' => Some('₋'),
|
||||
'=' => Some('₌'),
|
||||
'(' => Some('₍'),
|
||||
')' => Some('₎'),
|
||||
'a' => Some('ₐ'),
|
||||
'e' => Some('ₑ'),
|
||||
'o' => Some('ₒ'),
|
||||
'x' => Some('ₓ'),
|
||||
'h' => Some('ₕ'),
|
||||
'k' => Some('ₖ'),
|
||||
'l' => Some('ₗ'),
|
||||
'm' => Some('ₘ'),
|
||||
'n' => Some('ₙ'),
|
||||
'p' => Some('ₚ'),
|
||||
's' => Some('ₛ'),
|
||||
't' => Some('ₜ'),
|
||||
' ' => Some(' '),
|
||||
_ => None,
|
||||
}
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct ScriptSettings {
|
||||
/// Whether the OpenType feature should be used if possible.
|
||||
pub typographic: bool,
|
||||
pub shift: Smart<Em>,
|
||||
pub size: Smart<Em>,
|
||||
pub kind: ScriptKind,
|
||||
}
|
||||
|
Before Width: | Height: | Size: 802 B After Width: | Height: | Size: 815 B |
BIN
tests/ref/long-scripts.png
Normal file
After Width: | Height: | Size: 956 B |
BIN
tests/ref/scripts-with-bundeled-fonts.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
tests/ref/sub-super-italic-compensation.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 311 B After Width: | Height: | Size: 318 B |
BIN
tests/ref/sub-super-typographic.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.2 KiB |
BIN
tests/ref/super-1em.png
Normal file
After Width: | Height: | Size: 265 B |
BIN
tests/ref/super-highlight.png
Normal file
After Width: | Height: | Size: 6.3 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 3.9 KiB |
@ -1,19 +1,78 @@
|
||||
// Test sub- and superscript shifts.
|
||||
|
||||
--- sub-super ---
|
||||
#let sq = box(square(size: 4pt))
|
||||
#table(
|
||||
columns: 3,
|
||||
[Typo.], [Fallb.], [Synth],
|
||||
[x#super[1]], [x#super[5n]], [x#super[2 #box(square(size: 6pt))]],
|
||||
[x#sub[1]], [x#sub[5n]], [x#sub[2 #box(square(size: 6pt))]],
|
||||
[Typo.], [Fallb.], [Synth.],
|
||||
[x#super[1 #sq]], [x#super[5: #sq]], [x#super(typographic: false)[2 #sq]],
|
||||
[x#sub[1 #sq]], [x#sub[5: #sq]], [x#sub(typographic: false)[2 #sq]],
|
||||
)
|
||||
|
||||
--- sub-super-typographic ---
|
||||
#set text(size: 20pt)
|
||||
// Libertinus Serif supports "subs" and "sups" for `typo`, but not for `synth`.
|
||||
#let synth = [1,2,3]
|
||||
#let typo = [123]
|
||||
#let sq = [1#box(square(size: 4pt))2]
|
||||
x#super(synth) x#super(typo) x#super(sq) \
|
||||
x#sub(synth) x#sub(typo) x#sub(sq)
|
||||
|
||||
--- sub-super-italic-compensation ---
|
||||
#set text(size: 20pt, style: "italic")
|
||||
// Libertinus Serif supports "subs" and "sups" for `typo`, but not for `synth`.
|
||||
#let synth = [1,2,3]
|
||||
#let typo = [123]
|
||||
#let sq = [1#box(square(size: 4pt))2]
|
||||
x#super(synth) x#super(typo) x#super(sq) \
|
||||
x#sub(synth) x#sub(typo) x#sub(sq)
|
||||
|
||||
--- sub-super-non-typographic ---
|
||||
#set super(typographic: false, baseline: -0.25em, size: 0.7em)
|
||||
n#super[1], n#sub[2], ... n#super[N]
|
||||
|
||||
--- super-underline ---
|
||||
#set underline(stroke: 0.5pt, offset: 0.15em)
|
||||
#underline[The claim#super[\[4\]]] has been disputed. \
|
||||
The claim#super[#underline[\[4\]]] has been disputed. \
|
||||
It really has been#super(box(text(baseline: 0pt, underline[\[4\]]))) \
|
||||
#set super(typographic: false)
|
||||
#underline[The claim#super[4]] has been disputed. \
|
||||
The claim#super[#underline[4]] has been disputed. \
|
||||
The claim #underline(super[4]) has been disputed. \
|
||||
#set super(typographic: true)
|
||||
#underline[The claim#super[4]] has been disputed. \
|
||||
The claim#super[#underline[4]] has been disputed. \
|
||||
The claim #underline(super[4]) has been disputed.
|
||||
|
||||
--- super-highlight ---
|
||||
#set super(typographic: false)
|
||||
#highlight[The claim#super[4]] has been disputed. \
|
||||
The claim#super[#highlight[4]] has been disputed. \
|
||||
It really has been#super(highlight[4]) \
|
||||
#set super(typographic: true)
|
||||
#highlight[The claim#super[4]] has been disputed. \
|
||||
The claim#super[#highlight[4]] has been disputed. \
|
||||
It really has been#super(highlight[4])
|
||||
|
||||
--- super-1em ---
|
||||
Test#super[#box(rect(height: 1em))]#box(rect(height: 1em))
|
||||
|
||||
--- long-scripts ---
|
||||
|longscript| \
|
||||
|#super(typographic: true)[longscript]| \
|
||||
|#super(typographic: false)[longscript]| \
|
||||
|#sub(typographic: true)[longscript]| \
|
||||
|#sub(typographic: false)[longscript]|
|
||||
|
||||
--- scripts-with-bundeled-fonts ---
|
||||
#let test(font, weights, styles) = {
|
||||
for weight in weights {
|
||||
for style in styles {
|
||||
text(font: font, weight: weight, style: style)[Xx#super[Xx]#sub[Xx]]
|
||||
linebreak()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#test("DejaVu Sans Mono", ("regular", "bold"), ("normal", "oblique"))
|
||||
#test("Libertinus Serif", ("regular", "semibold", "bold"), ("normal", "italic"))
|
||||
#test("New Computer Modern", ("regular", "bold"), ("normal", "italic"))
|
||||
#test("New Computer Modern Math", (400, 450, "bold"), ("normal",))
|
||||
|