Configurable font edges ⚙

Adds top-edge and bottom-edge parameters to the font function. These define how
the box around a word is computed. The possible values are:
- ascender
- cap-height (default top edge)
- x-height
- baseline (default bottom edge)
- descender

The defaults are chosen so that it's easy to create good-looking designs with
vertical alignment. Since they are much tighter than what most other software
uses by default, the default leading had to be increased to 50% of the font size
and paragraph spacing to 100% of the font size.

The values cap-height and x-height fall back to ascender in case they are zero
because this value may occur in fonts that don't have glyphs with cap- or
x-height (like Twitter Color Emoji). Since cap-height is the default top edge,
doing no fallback would break things badly.

Removes softness in favor of a simple boolean for pages and a more finegread u8
for spacing. This is needed to make paragraph spacing consume line spacing
created by hard line breaks.
This commit is contained in:
Laurenz 2021-03-19 13:20:58 +01:00
parent bd12d135ca
commit 54a9ccb1a5
50 changed files with 231 additions and 111 deletions

View File

@ -38,7 +38,7 @@ impl<'a> ExecContext<'a> {
env, env,
diags: DiagSet::new(), diags: DiagSet::new(),
tree: Tree { runs: vec![] }, tree: Tree { runs: vec![] },
page: Some(PageInfo::new(&state, Softness::Hard)), page: Some(PageInfo::new(&state, true)),
stack: NodeStack::new(&state), stack: NodeStack::new(&state),
par: NodePar::new(&state), par: NodePar::new(&state),
state, state,
@ -77,7 +77,8 @@ impl<'a> ExecContext<'a> {
/// Push a layout node into the active paragraph. /// Push a layout node into the active paragraph.
/// ///
/// Spacing nodes will be handled according to their [`Softness`]. /// Spacing nodes will be handled according to their
/// [`softness`](NodeSpacing::softness).
pub fn push(&mut self, node: impl Into<Node>) { pub fn push(&mut self, node: impl Into<Node>) {
push(&mut self.par.children, node.into()); push(&mut self.par.children, node.into());
} }
@ -87,7 +88,7 @@ impl<'a> ExecContext<'a> {
let em = self.state.font.font_size(); let em = self.state.font.font_size();
self.push(NodeSpacing { self.push(NodeSpacing {
amount: self.state.par.word_spacing.resolve(em), amount: self.state.par.word_spacing.resolve(em),
softness: Softness::Soft, softness: 1,
}); });
} }
@ -109,15 +110,19 @@ impl<'a> ExecContext<'a> {
/// Apply a forced line break. /// Apply a forced line break.
pub fn push_linebreak(&mut self) { pub fn push_linebreak(&mut self) {
self.finish_par(); let em = self.state.font.font_size();
self.push_into_stack(NodeSpacing {
amount: self.state.par.leading.resolve(em),
softness: 2,
});
} }
/// Apply a forced paragraph break. /// Apply a forced paragraph break.
pub fn push_parbreak(&mut self) { pub fn push_parbreak(&mut self) {
let em = self.state.font.font_size(); let em = self.state.font.font_size();
self.push_into_stack(NodeSpacing { self.push_into_stack(NodeSpacing {
amount: self.state.par.par_spacing.resolve(em), amount: self.state.par.spacing.resolve(em),
softness: Softness::Soft, softness: 1,
}); });
} }
@ -163,11 +168,13 @@ impl<'a> ExecContext<'a> {
NodeText { NodeText {
text, text,
aligns: self.state.aligns,
dir: self.state.dirs.cross, dir: self.state.dirs.cross,
font_size: self.state.font.font_size(), aligns: self.state.aligns,
families: Rc::clone(&self.state.font.families), families: Rc::clone(&self.state.font.families),
variant, variant,
font_size: self.state.font.font_size(),
top_edge: self.state.font.top_edge,
bottom_edge: self.state.font.bottom_edge,
} }
} }
@ -192,12 +199,12 @@ impl<'a> ExecContext<'a> {
} }
/// Finish the active page. /// Finish the active page.
pub fn finish_page(&mut self, keep: bool, new_softness: Softness, source: Span) { pub fn finish_page(&mut self, keep: bool, hard: bool, source: Span) {
if let Some(info) = &mut self.page { if let Some(info) = &mut self.page {
let info = mem::replace(info, PageInfo::new(&self.state, new_softness)); let info = mem::replace(info, PageInfo::new(&self.state, hard));
let stack = self.finish_stack(); let stack = self.finish_stack();
if !stack.children.is_empty() || (keep && info.softness == Softness::Hard) { if !stack.children.is_empty() || (keep && info.hard) {
self.tree.runs.push(NodePages { self.tree.runs.push(NodePages {
size: info.size, size: info.size,
child: NodePad { child: NodePad {
@ -215,7 +222,7 @@ impl<'a> ExecContext<'a> {
/// Finish execution and return the created layout tree. /// Finish execution and return the created layout tree.
pub fn finish(mut self) -> Pass<Tree> { pub fn finish(mut self) -> Pass<Tree> {
assert!(self.page.is_some()); assert!(self.page.is_some());
self.finish_page(true, Softness::Soft, Span::default()); self.finish_page(true, false, Span::default());
Pass::new(self.tree, self.diags) Pass::new(self.tree, self.diags)
} }
} }
@ -223,16 +230,18 @@ impl<'a> ExecContext<'a> {
/// Push a node into a list, taking care of spacing softness. /// Push a node into a list, taking care of spacing softness.
fn push(nodes: &mut Vec<Node>, node: Node) { fn push(nodes: &mut Vec<Node>, node: Node) {
if let Node::Spacing(spacing) = node { if let Node::Spacing(spacing) = node {
if spacing.softness == Softness::Soft && nodes.is_empty() { if nodes.is_empty() && spacing.softness > 0 {
return; return;
} }
if let Some(&Node::Spacing(other)) = nodes.last() { if let Some(&Node::Spacing(other)) = nodes.last() {
if spacing.softness > other.softness { if spacing.softness > 0 && spacing.softness >= other.softness {
nodes.pop();
} else if spacing.softness == Softness::Soft {
return; return;
} }
if spacing.softness < other.softness {
nodes.pop();
}
} }
} }
@ -242,7 +251,7 @@ fn push(nodes: &mut Vec<Node>, node: Node) {
/// Remove trailing soft spacing from a node list. /// Remove trailing soft spacing from a node list.
fn trim(nodes: &mut Vec<Node>) { fn trim(nodes: &mut Vec<Node>) {
if let Some(&Node::Spacing(spacing)) = nodes.last() { if let Some(&Node::Spacing(spacing)) = nodes.last() {
if spacing.softness == Softness::Soft { if spacing.softness > 0 {
nodes.pop(); nodes.pop();
} }
} }
@ -252,15 +261,15 @@ fn trim(nodes: &mut Vec<Node>) {
struct PageInfo { struct PageInfo {
size: Size, size: Size,
padding: Sides<Linear>, padding: Sides<Linear>,
softness: Softness, hard: bool,
} }
impl PageInfo { impl PageInfo {
fn new(state: &State, softness: Softness) -> Self { fn new(state: &State, hard: bool) -> Self {
Self { Self {
size: state.page.size, size: state.page.size,
padding: state.page.margins(), padding: state.page.margins(),
softness, hard,
} }
} }
} }
@ -281,7 +290,7 @@ impl NodePar {
Self { Self {
dirs: state.dirs, dirs: state.dirs,
aligns: state.aligns, aligns: state.aligns,
line_spacing: state.par.line_spacing.resolve(em), line_spacing: state.par.leading.resolve(em),
children: vec![], children: vec![],
} }
} }

View File

@ -35,15 +35,6 @@ pub fn exec(
ctx.finish() ctx.finish()
} }
/// Defines how an item interacts with surrounding items.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub enum Softness {
/// A soft item can be skipped in some circumstances.
Soft,
/// A hard item is always retained.
Hard,
}
/// Execute a node. /// Execute a node.
/// ///
/// This manipulates active styling and document state and produces layout /// This manipulates active styling and document state and produces layout
@ -106,15 +97,15 @@ impl Exec for NodeRaw {
ctx.set_monospace(); ctx.set_monospace();
let em = ctx.state.font.font_size(); let em = ctx.state.font.font_size();
let line_spacing = ctx.state.par.line_spacing.resolve(em); let leading = ctx.state.par.leading.resolve(em);
let mut children = vec![]; let mut children = vec![];
let mut newline = false; let mut newline = false;
for line in &self.lines { for line in &self.lines {
if newline { if newline {
children.push(layout::Node::Spacing(NodeSpacing { children.push(layout::Node::Spacing(NodeSpacing {
amount: line_spacing, amount: leading,
softness: Softness::Soft, softness: 2,
})); }));
} }

View File

@ -2,10 +2,9 @@ use std::rc::Rc;
use fontdock::{fallback, FallbackTree, FontStretch, FontStyle, FontVariant, FontWeight}; use fontdock::{fallback, FallbackTree, FontStretch, FontStyle, FontVariant, FontWeight};
use crate::geom::{ use crate::geom::*;
Align, Dir, LayoutAligns, LayoutDirs, Length, Linear, Relative, Sides, Size,
};
use crate::paper::{Paper, PaperClass, PAPER_A4}; use crate::paper::{Paper, PaperClass, PAPER_A4};
use crate::shaping::VerticalFontMetric;
/// The evaluation state. /// The evaluation state.
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
@ -77,20 +76,20 @@ impl Default for PageState {
/// Defines paragraph properties. /// Defines paragraph properties.
#[derive(Debug, Copy, Clone, PartialEq)] #[derive(Debug, Copy, Clone, PartialEq)]
pub struct ParState { pub struct ParState {
/// The spacing between paragraphs (dependent on scaled font size).
pub spacing: Linear,
/// The spacing between lines (dependent on scaled font size).
pub leading: Linear,
/// The spacing between words (dependent on scaled font size). /// The spacing between words (dependent on scaled font size).
pub word_spacing: Linear, pub word_spacing: Linear,
/// The spacing between lines (dependent on scaled font size).
pub line_spacing: Linear,
/// The spacing between paragraphs (dependent on scaled font size).
pub par_spacing: Linear,
} }
impl Default for ParState { impl Default for ParState {
fn default() -> Self { fn default() -> Self {
Self { Self {
spacing: Relative::new(1.0).into(),
leading: Relative::new(0.5).into(),
word_spacing: Relative::new(0.25).into(), word_spacing: Relative::new(0.25).into(),
line_spacing: Linear::ZERO,
par_spacing: Relative::new(0.5).into(),
} }
} }
} }
@ -106,6 +105,10 @@ pub struct FontState {
pub size: Length, pub size: Length,
/// The linear to apply on the base font size. /// The linear to apply on the base font size.
pub scale: Linear, pub scale: Linear,
/// The top end of the text bounding box.
pub top_edge: VerticalFontMetric,
/// The bottom end of the text bounding box.
pub bottom_edge: VerticalFontMetric,
/// Whether the strong toggle is active or inactive. This determines /// Whether the strong toggle is active or inactive. This determines
/// whether the next `*` adds or removes font weight. /// whether the next `*` adds or removes font weight.
pub strong: bool, pub strong: bool,
@ -141,6 +144,8 @@ impl Default for FontState {
stretch: FontStretch::Normal, stretch: FontStretch::Normal,
}, },
size: Length::pt(11.0), size: Length::pt(11.0),
top_edge: VerticalFontMetric::CapHeight,
bottom_edge: VerticalFontMetric::Baseline,
scale: Linear::ONE, scale: Linear::ONE,
strong: false, strong: false,
emph: false, emph: false,

View File

@ -180,6 +180,7 @@ impl<'a> PdfExporter<'a> {
Element::Text(shaped) => { Element::Text(shaped) => {
let mut text = content.text(); let mut text = content.text();
// Check if we need to issue a font switching action. // Check if we need to issue a font switching action.
if shaped.face != face || shaped.font_size != size { if shaped.face != face || shaped.font_size != size {
face = shaped.face; face = shaped.face;

View File

@ -1,7 +1,6 @@
use std::fmt::{self, Debug, Formatter}; use std::fmt::{self, Debug, Formatter};
use super::*; use super::*;
use crate::exec::Softness;
/// A spacing node. /// A spacing node.
#[derive(Copy, Clone, PartialEq)] #[derive(Copy, Clone, PartialEq)]
@ -10,13 +9,11 @@ pub struct NodeSpacing {
pub amount: Length, pub amount: Length,
/// Defines how spacing interacts with surrounding spacing. /// Defines how spacing interacts with surrounding spacing.
/// ///
/// Hard spacing assures that a fixed amount of spacing will always be /// Hard spacing (`softness = 0`) assures that a fixed amount of spacing
/// inserted. Soft spacing will be consumed by previous soft spacing or /// will always be inserted. Soft spacing (`softness >= 1`) will be consumed
/// neighbouring hard spacing and can be used to insert overridable spacing, /// by other spacing with lower softness and can be used to insert
/// e.g. between words or paragraphs. /// overridable spacing, e.g. between words or paragraphs.
/// pub softness: u8,
/// This field is only used in evaluation, not in layouting.
pub softness: Softness,
} }
impl Layout for NodeSpacing { impl Layout for NodeSpacing {
@ -27,10 +24,7 @@ impl Layout for NodeSpacing {
impl Debug for NodeSpacing { impl Debug for NodeSpacing {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self.softness { write!(f, "Spacing({}, {})", self.amount, self.softness)
Softness::Soft => write!(f, "Soft({})", self.amount),
Softness::Hard => write!(f, "Hard({})", self.amount),
}
} }
} }

View File

@ -4,35 +4,41 @@ use std::rc::Rc;
use fontdock::{FallbackTree, FontVariant}; use fontdock::{FallbackTree, FontVariant};
use super::*; use super::*;
use crate::shaping; use crate::shaping::{shape, VerticalFontMetric};
/// A text node. /// A text node.
#[derive(Clone, PartialEq)] #[derive(Clone, PartialEq)]
pub struct NodeText { pub struct NodeText {
/// The text.
pub text: String,
/// The text direction. /// The text direction.
pub dir: Dir, pub dir: Dir,
/// How to align this text node in its parent. /// How to align this text node in its parent.
pub aligns: LayoutAligns, pub aligns: LayoutAligns,
/// The text.
pub text: String,
/// The font size.
pub font_size: Length,
/// The families used for font fallback. /// The families used for font fallback.
pub families: Rc<FallbackTree>, pub families: Rc<FallbackTree>,
/// The font variant, /// The font variant,
pub variant: FontVariant, pub variant: FontVariant,
/// The font size.
pub font_size: Length,
/// The top end of the text bounding box.
pub top_edge: VerticalFontMetric,
/// The bottom end of the text bounding box.
pub bottom_edge: VerticalFontMetric,
} }
impl Layout for NodeText { impl Layout for NodeText {
fn layout(&self, ctx: &mut LayoutContext, _: &Areas) -> Layouted { fn layout(&self, ctx: &mut LayoutContext, _: &Areas) -> Layouted {
Layouted::Frame( Layouted::Frame(
shaping::shape( shape(
&self.text, &self.text,
self.dir, self.dir,
self.font_size,
&mut ctx.env.fonts,
&self.families, &self.families,
self.variant, self.variant,
self.font_size,
self.top_edge,
self.bottom_edge,
&mut ctx.env.fonts,
), ),
self.aligns, self.aligns,
) )

View File

@ -1,6 +1,7 @@
use fontdock::{FontStretch, FontStyle, FontWeight}; use fontdock::{FontStretch, FontStyle, FontWeight};
use super::*; use super::*;
use crate::shaping::VerticalFontMetric;
/// `font`: Configure the font. /// `font`: Configure the font.
/// ///
@ -13,6 +14,8 @@ use super::*;
/// - Font Style: `style`, of type `font-style`. /// - Font Style: `style`, of type `font-style`.
/// - Font Weight: `weight`, of type `font-weight`. /// - Font Weight: `weight`, of type `font-weight`.
/// - Font Stretch: `stretch`, of type `relative`, between 0.5 and 2.0. /// - Font Stretch: `stretch`, of type `relative`, between 0.5 and 2.0.
/// - Top edge of the font: `top-edge`, of type `vertical-font-metric`.
/// - Bottom edge of the font: `bottom-edge`, of type `vertical-font-metric`.
/// - Serif family definition: `serif`, of type `font-familiy-list`. /// - Serif family definition: `serif`, of type `font-familiy-list`.
/// - Sans-serif family definition: `sans-serif`, of type `font-familiy-list`. /// - Sans-serif family definition: `sans-serif`, of type `font-familiy-list`.
/// - Monospace family definition: `monospace`, of type `font-familiy-list`. /// - Monospace family definition: `monospace`, of type `font-familiy-list`.
@ -22,15 +25,15 @@ use super::*;
/// if present. /// if present.
/// ///
/// # Relevant types and constants /// # Relevant types and constants
/// - Type `font-family-list`
/// - coerces from `string`
/// - coerces from `array`
/// - coerces from `font-family`
/// - Type `font-family` /// - Type `font-family`
/// - `serif` /// - `serif`
/// - `sans-serif` /// - `sans-serif`
/// - `monospace` /// - `monospace`
/// - coerces from `string` /// - coerces from `string`
/// - Type `font-family-list`
/// - coerces from `string`
/// - coerces from `array`
/// - coerces from `font-family`
/// - Type `font-style` /// - Type `font-style`
/// - `normal` /// - `normal`
/// - `italic` /// - `italic`
@ -46,12 +49,20 @@ use super::*;
/// - `extrabold` (800) /// - `extrabold` (800)
/// - `black` (900) /// - `black` (900)
/// - coerces from `integer` /// - coerces from `integer`
/// - Type `vertical-font-metric`
/// - `ascender`
/// - `cap-height`
/// - `x-height`
/// - `baseline`
/// - `descender`
pub fn font(ctx: &mut EvalContext, args: &mut ValueArgs) -> Value { pub fn font(ctx: &mut EvalContext, args: &mut ValueArgs) -> Value {
let size = args.find::<Linear>(ctx); let size = args.find::<Linear>(ctx);
let list: Vec<_> = args.filter::<FontFamily>(ctx).map(|f| f.to_string()).collect(); let list: Vec<_> = args.filter::<FontFamily>(ctx).map(|f| f.to_string()).collect();
let style = args.get(ctx, "style"); let style = args.get(ctx, "style");
let weight = args.get(ctx, "weight"); let weight = args.get(ctx, "weight");
let stretch = args.get(ctx, "stretch"); let stretch = args.get(ctx, "stretch");
let top_edge = args.get(ctx, "top-edge");
let bottom_edge = args.get(ctx, "bottom-edge");
let serif = args.get(ctx, "serif"); let serif = args.get(ctx, "serif");
let sans_serif = args.get(ctx, "sans-serif"); let sans_serif = args.get(ctx, "sans-serif");
let monospace = args.get(ctx, "monospace"); let monospace = args.get(ctx, "monospace");
@ -87,6 +98,14 @@ pub fn font(ctx: &mut EvalContext, args: &mut ValueArgs) -> Value {
ctx.state.font.variant.stretch = stretch; ctx.state.font.variant.stretch = stretch;
} }
if let Some(top_edge) = top_edge {
ctx.state.font.top_edge = top_edge;
}
if let Some(bottom_edge) = bottom_edge {
ctx.state.font.bottom_edge = bottom_edge;
}
for (variant, arg) in &[ for (variant, arg) in &[
(FontFamily::Serif, &serif), (FontFamily::Serif, &serif),
(FontFamily::SansSerif, &sans_serif), (FontFamily::SansSerif, &sans_serif),
@ -185,3 +204,7 @@ typify! {
}; };
}, },
} }
typify! {
VerticalFontMetric: "vertical font metric",
}

View File

@ -26,9 +26,9 @@ use std::fmt::{self, Display, Formatter};
use fontdock::{FontStyle, FontWeight}; use fontdock::{FontStyle, FontWeight};
use crate::eval::{Scope, ValueAny, ValueFunc}; use crate::eval::{Scope, ValueAny, ValueFunc};
use crate::exec::Softness;
use crate::layout::*; use crate::layout::*;
use crate::prelude::*; use crate::prelude::*;
use crate::shaping::VerticalFontMetric;
/// Construct a scope containing all standard library definitions. /// Construct a scope containing all standard library definitions.
pub fn new() -> Scope { pub fn new() -> Scope {
@ -81,6 +81,11 @@ pub fn new() -> Scope {
set!(any: "bold", FontWeight::BOLD); set!(any: "bold", FontWeight::BOLD);
set!(any: "extrabold", FontWeight::EXTRABOLD); set!(any: "extrabold", FontWeight::EXTRABOLD);
set!(any: "black", FontWeight::BLACK); set!(any: "black", FontWeight::BLACK);
set!(any: "ascender", VerticalFontMetric::Ascender);
set!(any: "cap-height", VerticalFontMetric::CapHeight);
set!(any: "x-height", VerticalFontMetric::XHeight);
set!(any: "baseline", VerticalFontMetric::Baseline);
set!(any: "descender", VerticalFontMetric::Descender);
std std
} }

View File

@ -95,13 +95,13 @@ pub fn page(ctx: &mut EvalContext, args: &mut ValueArgs) -> Value {
} }
ctx.set_dirs(Gen::new(main, cross)); ctx.set_dirs(Gen::new(main, cross));
ctx.finish_page(false, Softness::Hard, span); ctx.finish_page(false, true, span);
if let Some(body) = &body { if let Some(body) = &body {
// TODO: Restrict body to a single page? // TODO: Restrict body to a single page?
body.exec(ctx); body.exec(ctx);
ctx.state = snapshot; ctx.state = snapshot;
ctx.finish_page(true, Softness::Soft, span); ctx.finish_page(true, false, span);
} }
}) })
} }
@ -113,6 +113,6 @@ pub fn page(ctx: &mut EvalContext, args: &mut ValueArgs) -> Value {
pub fn pagebreak(_: &mut EvalContext, args: &mut ValueArgs) -> Value { pub fn pagebreak(_: &mut EvalContext, args: &mut ValueArgs) -> Value {
let span = args.span; let span = args.span;
Value::template("pagebreak", move |ctx| { Value::template("pagebreak", move |ctx| {
ctx.finish_page(true, Softness::Hard, span); ctx.finish_page(true, true, span);
}) })
} }

View File

@ -28,7 +28,7 @@ fn spacing(ctx: &mut EvalContext, args: &mut ValueArgs, axis: SpecAxis) -> Value
Value::template("spacing", move |ctx| { Value::template("spacing", move |ctx| {
if let Some(linear) = spacing { if let Some(linear) = spacing {
let amount = linear.resolve(ctx.state.font.font_size()); let amount = linear.resolve(ctx.state.font.font_size());
let spacing = NodeSpacing { amount, softness: Softness::Hard }; let spacing = NodeSpacing { amount, softness: 0 };
if axis == ctx.state.dirs.main.axis() { if axis == ctx.state.dirs.main.axis() {
ctx.push_into_stack(spacing); ctx.push_into_stack(spacing);
} else { } else {

View File

@ -4,7 +4,7 @@
//! font for each individual character. When the direction is right-to-left, the //! font for each individual character. When the direction is right-to-left, the
//! word is spelled backwards. Vertical shaping is not supported. //! word is spelled backwards. Vertical shaping is not supported.
use std::fmt::{self, Debug, Formatter}; use std::fmt::{self, Debug, Display, Formatter};
use fontdock::{FaceId, FaceQuery, FallbackTree, FontVariant}; use fontdock::{FaceId, FaceQuery, FallbackTree, FontVariant};
use ttf_parser::{Face, GlyphId}; use ttf_parser::{Face, GlyphId};
@ -58,20 +58,55 @@ impl Debug for Shaped {
} }
} }
/// Identifies a vertical metric of a font.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub enum VerticalFontMetric {
/// The distance from the baseline to the typographic ascender.
///
/// Corresponds to the typographic ascender from the `OS/2` table if present
/// and falls back to the ascender from the `hhea` table otherwise.
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 distance from the baseline to the typographic descender.
///
/// Corresponds to the typographic descender from the `OS/2` table if
/// present and falls back to the descender from the `hhea` table otherwise.
Descender,
}
impl Display for VerticalFontMetric {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.pad(match self {
Self::Ascender => "ascender",
Self::CapHeight => "cap-height",
Self::XHeight => "x-height",
Self::Baseline => "baseline",
Self::Descender => "descender",
})
}
}
/// Shape text into a frame containing [`Shaped`] runs. /// Shape text into a frame containing [`Shaped`] runs.
pub fn shape( pub fn shape(
text: &str, text: &str,
dir: Dir, dir: Dir,
font_size: Length,
loader: &mut FontLoader,
fallback: &FallbackTree, fallback: &FallbackTree,
variant: FontVariant, variant: FontVariant,
font_size: Length,
top_edge: VerticalFontMetric,
bottom_edge: VerticalFontMetric,
loader: &mut FontLoader,
) -> Frame { ) -> Frame {
let mut frame = Frame::new(Size::new(Length::ZERO, font_size)); let mut frame = Frame::new(Size::new(Length::ZERO, Length::ZERO));
let mut shaped = Shaped::new(FaceId::MAX, font_size); let mut shaped = Shaped::new(FaceId::MAX, font_size);
let mut offset = Length::ZERO; let mut width = Length::ZERO;
let mut ascender = Length::ZERO; let mut top = Length::ZERO;
let mut descender = Length::ZERO; let mut bottom = Length::ZERO;
// Create an iterator with conditional direction. // Create an iterator with conditional direction.
let mut forwards = text.chars(); let mut forwards = text.chars();
@ -86,7 +121,7 @@ pub fn shape(
let query = FaceQuery { fallback: fallback.iter(), variant, c }; let query = FaceQuery { fallback: fallback.iter(), variant, c };
if let Some(id) = loader.query(query) { if let Some(id) = loader.query(query) {
let face = loader.face(id).get(); let face = loader.face(id).get();
let (glyph, width) = match lookup_glyph(face, c) { let (glyph, glyph_width) = match lookup_glyph(face, c) {
Some(v) => v, Some(v) => v,
None => continue, None => continue,
}; };
@ -96,27 +131,35 @@ pub fn shape(
// Flush the buffer and reset the metrics if we use a new font face. // Flush the buffer and reset the metrics if we use a new font face.
if shaped.face != id { if shaped.face != id {
place(&mut frame, shaped, offset, ascender, descender); place(&mut frame, shaped, width, top, bottom);
shaped = Shaped::new(id, font_size); shaped = Shaped::new(id, font_size);
offset = Length::ZERO; width = Length::ZERO;
ascender = convert(f64::from(face.ascender())); top = convert(f64::from(lookup_metric(face, top_edge)));
descender = convert(f64::from(face.descender())); bottom = convert(f64::from(lookup_metric(face, bottom_edge)));
} }
shaped.text.push(c); shaped.text.push(c);
shaped.glyphs.push(glyph); shaped.glyphs.push(glyph);
shaped.offsets.push(offset); shaped.offsets.push(width);
offset += convert(f64::from(width)); width += convert(f64::from(glyph_width));
} }
} }
// Flush the last buffered parts of the word. place(&mut frame, shaped, width, top, bottom);
place(&mut frame, shaped, offset, ascender, descender);
frame frame
} }
/// Place shaped text into a frame.
fn place(frame: &mut Frame, shaped: Shaped, width: Length, top: Length, bottom: Length) {
if !shaped.text.is_empty() {
frame.push(Point::new(frame.size.width, top), Element::Text(shaped));
frame.size.width += width;
frame.size.height = frame.size.height.max(top - bottom);
}
}
/// Look up the glyph for `c` and returns its index alongside its advance width. /// Look up the glyph for `c` and returns its index alongside its advance width.
fn lookup_glyph(face: &Face, c: char) -> Option<(GlyphId, u16)> { fn lookup_glyph(face: &Face, c: char) -> Option<(GlyphId, u16)> {
let glyph = face.glyph_index(c)?; let glyph = face.glyph_index(c)?;
@ -124,18 +167,32 @@ fn lookup_glyph(face: &Face, c: char) -> Option<(GlyphId, u16)> {
Some((glyph, width)) Some((glyph, width))
} }
/// Place shaped text into a frame. /// Look up a vertical metric.
fn place( fn lookup_metric(face: &Face, metric: VerticalFontMetric) -> i16 {
frame: &mut Frame, match metric {
shaped: Shaped, VerticalFontMetric::Ascender => lookup_ascender(face),
offset: Length, VerticalFontMetric::CapHeight => face
ascender: Length, .capital_height()
descender: Length, .filter(|&h| h > 0)
) { .unwrap_or_else(|| lookup_ascender(face)),
if !shaped.text.is_empty() { VerticalFontMetric::XHeight => face
let pos = Point::new(frame.size.width, ascender); .x_height()
frame.push(pos, Element::Text(shaped)); .filter(|&h| h > 0)
frame.size.width += offset; .unwrap_or_else(|| lookup_ascender(face)),
frame.size.height = frame.size.height.max(ascender - descender); VerticalFontMetric::Baseline => 0,
VerticalFontMetric::Descender => lookup_descender(face),
} }
} }
/// The ascender of the face.
fn lookup_ascender(face: &Face) -> i16 {
// We prefer the typographic ascender over the Windows ascender because
// it can be overly large if the font has large glyphs.
face.typographic_ascender().unwrap_or_else(|| face.ascender())
}
/// The descender of the face.
fn lookup_descender(face: &Face) -> i16 {
// See `lookup_ascender` for reason.
face.typographic_descender().unwrap_or_else(|| face.descender())
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 746 B

After

Width:  |  Height:  |  Size: 706 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 929 B

After

Width:  |  Height:  |  Size: 880 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 522 B

After

Width:  |  Height:  |  Size: 512 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 827 B

After

Width:  |  Height:  |  Size: 776 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 214 KiB

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 840 B

After

Width:  |  Height:  |  Size: 803 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

@ -17,8 +17,8 @@
--- ---
// Syntax sugar for function definitions. // Syntax sugar for function definitions.
#let background = #239dad #let background = #9feb52
#let rect(body) = rect(width: 2cm, height: 1cm, fill: background, body) #let rect(body) = rect(width: 2cm, fill: background, pad(5pt, body))
#rect[Hi!] #rect[Hi!]
// Error: 13 expected body // Error: 13 expected body

View File

@ -30,8 +30,8 @@
// the parentheses. // the parentheses.
#align(center)[ #align(center)[
// Markdown-like syntax for headings. // Markdown-like syntax for headings.
==== 3. Übungsblatt Computerorientierte Mathematik II #v(2mm) ==== 3. Übungsblatt Computerorientierte Mathematik II #v(4mm)
*Abgabe: 03.05.2019* (bis 10:10 Uhr in MA 001) #v(2mm) *Abgabe: 03.05.2019* (bis 10:10 Uhr in MA 001) #v(4mm)
*Alle Antworten sind zu beweisen.* *Alle Antworten sind zu beweisen.*
] ]

View File

@ -31,6 +31,20 @@ Emoji: 🐪, 🌋, 🏞
𝛼 + 3𝛽 d𝑡 𝛼 + 3𝛽 d𝑡
] ]
---
// Test top and bottom edge.
#page(width: 170pt)
#let try(top, bottom) = rect(fill: #9feb52)[
#font(top-edge: top, bottom-edge: bottom)
`From `#top` to `#bottom
]
#try(ascender, descender)
#try(ascender, baseline)
#try(cap-height, baseline)
#try(x-height, baseline)
--- ---
// Ref: false // Ref: false

View File

@ -2,7 +2,7 @@
--- ---
// Ends paragraphs. // Ends paragraphs.
Tightly #v(-5pt) packed Tightly #v(0pt) packed
// Eating up soft spacing. // Eating up soft spacing.
Inv #h(0pt) isible Inv #h(0pt) isible

View File

@ -28,3 +28,7 @@
// Unterminated. // Unterminated.
// Error: 6 expected closing brace // Error: 6 expected closing brace
\u{41*Bold* \u{41*Bold*
---
// Some code stuff in text.
let f() , ; : | + - /= == 12 "string"

View File

@ -7,7 +7,7 @@
--- ---
// Typst syntax inside. // Typst syntax inside.
`#let x = 1` \ `#let x = 1` \
`#[f 1]` `#f(1)`
--- ---
// Multiline block splits paragraphs. // Multiline block splits paragraphs.

View File

@ -1,8 +1,19 @@
// Test simple text. // Test simple text.
--- #page(width: 250pt)
Hello 🌏!
--- But, soft! what light through yonder window breaks? It is the east, and Juliet
// Some code stuff in text. is the sun. Arise, fair sun, and kill the envious moon, Who is already sick and
let f() , ; : | + - /= == 12 "string" pale with grief, That thou her maid art far more fair than she: Be not her maid,
since she is envious; Her vestal livery is but sick and green And none but fools
do wear it; cast it off. It is my lady, O, it is my love! O, that she knew she
were! She speaks yet she says nothing: what of that? Her eye discourses; I will
answer it.
I am too bold, 'tis not to me she speaks: Two of the fairest stars in all the
heaven, Having some business, do entreat her eyes To twinkle in their spheres
till they return. What if her eyes were there, they in her head? The brightness
of her cheek would shame those stars, As daylight doth a lamp; her eyes in
heaven Would through the airy region stream so bright That birds would sing and
think it were not night. See, how she leans her cheek upon her hand! O, that I
were a glove upon that hand, That I might touch that cheek!