diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/exec/context.rs | 51 | ||||
| -rw-r--r-- | src/exec/mod.rs | 15 | ||||
| -rw-r--r-- | src/exec/state.rs | 23 | ||||
| -rw-r--r-- | src/export/pdf.rs | 1 | ||||
| -rw-r--r-- | src/layout/spacing.rs | 18 | ||||
| -rw-r--r-- | src/layout/text.rs | 22 | ||||
| -rw-r--r-- | src/library/font.rs | 31 | ||||
| -rw-r--r-- | src/library/mod.rs | 7 | ||||
| -rw-r--r-- | src/library/page.rs | 6 | ||||
| -rw-r--r-- | src/library/spacing.rs | 2 | ||||
| -rw-r--r-- | src/shaping.rs | 115 |
11 files changed, 191 insertions, 100 deletions
diff --git a/src/exec/context.rs b/src/exec/context.rs index 6a1c2416..7fd72fe3 100644 --- a/src/exec/context.rs +++ b/src/exec/context.rs @@ -38,7 +38,7 @@ impl<'a> ExecContext<'a> { env, diags: DiagSet::new(), tree: Tree { runs: vec![] }, - page: Some(PageInfo::new(&state, Softness::Hard)), + page: Some(PageInfo::new(&state, true)), stack: NodeStack::new(&state), par: NodePar::new(&state), state, @@ -77,7 +77,8 @@ impl<'a> ExecContext<'a> { /// 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>) { push(&mut self.par.children, node.into()); } @@ -87,7 +88,7 @@ impl<'a> ExecContext<'a> { let em = self.state.font.font_size(); self.push(NodeSpacing { 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. 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. pub fn push_parbreak(&mut self) { let em = self.state.font.font_size(); self.push_into_stack(NodeSpacing { - amount: self.state.par.par_spacing.resolve(em), - softness: Softness::Soft, + amount: self.state.par.spacing.resolve(em), + softness: 1, }); } @@ -163,11 +168,13 @@ impl<'a> ExecContext<'a> { NodeText { text, - aligns: self.state.aligns, dir: self.state.dirs.cross, - font_size: self.state.font.font_size(), + aligns: self.state.aligns, families: Rc::clone(&self.state.font.families), 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. - 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 { - 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(); - if !stack.children.is_empty() || (keep && info.softness == Softness::Hard) { + if !stack.children.is_empty() || (keep && info.hard) { self.tree.runs.push(NodePages { size: info.size, child: NodePad { @@ -215,7 +222,7 @@ impl<'a> ExecContext<'a> { /// Finish execution and return the created layout tree. pub fn finish(mut self) -> Pass<Tree> { 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) } } @@ -223,16 +230,18 @@ impl<'a> ExecContext<'a> { /// Push a node into a list, taking care of spacing softness. fn push(nodes: &mut Vec<Node>, node: Node) { if let Node::Spacing(spacing) = node { - if spacing.softness == Softness::Soft && nodes.is_empty() { + if nodes.is_empty() && spacing.softness > 0 { return; } if let Some(&Node::Spacing(other)) = nodes.last() { - if spacing.softness > other.softness { - nodes.pop(); - } else if spacing.softness == Softness::Soft { + if spacing.softness > 0 && spacing.softness >= other.softness { 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. fn trim(nodes: &mut Vec<Node>) { if let Some(&Node::Spacing(spacing)) = nodes.last() { - if spacing.softness == Softness::Soft { + if spacing.softness > 0 { nodes.pop(); } } @@ -252,15 +261,15 @@ fn trim(nodes: &mut Vec<Node>) { struct PageInfo { size: Size, padding: Sides<Linear>, - softness: Softness, + hard: bool, } impl PageInfo { - fn new(state: &State, softness: Softness) -> Self { + fn new(state: &State, hard: bool) -> Self { Self { size: state.page.size, padding: state.page.margins(), - softness, + hard, } } } @@ -281,7 +290,7 @@ impl NodePar { Self { dirs: state.dirs, aligns: state.aligns, - line_spacing: state.par.line_spacing.resolve(em), + line_spacing: state.par.leading.resolve(em), children: vec![], } } diff --git a/src/exec/mod.rs b/src/exec/mod.rs index 45abca02..35ffa2b6 100644 --- a/src/exec/mod.rs +++ b/src/exec/mod.rs @@ -35,15 +35,6 @@ pub fn exec( 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. /// /// This manipulates active styling and document state and produces layout @@ -106,15 +97,15 @@ impl Exec for NodeRaw { ctx.set_monospace(); 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 newline = false; for line in &self.lines { if newline { children.push(layout::Node::Spacing(NodeSpacing { - amount: line_spacing, - softness: Softness::Soft, + amount: leading, + softness: 2, })); } diff --git a/src/exec/state.rs b/src/exec/state.rs index 65f26439..aa2dde1c 100644 --- a/src/exec/state.rs +++ b/src/exec/state.rs @@ -2,10 +2,9 @@ use std::rc::Rc; use fontdock::{fallback, FallbackTree, FontStretch, FontStyle, FontVariant, FontWeight}; -use crate::geom::{ - Align, Dir, LayoutAligns, LayoutDirs, Length, Linear, Relative, Sides, Size, -}; +use crate::geom::*; use crate::paper::{Paper, PaperClass, PAPER_A4}; +use crate::shaping::VerticalFontMetric; /// The evaluation state. #[derive(Debug, Clone, PartialEq)] @@ -77,20 +76,20 @@ impl Default for PageState { /// Defines paragraph properties. #[derive(Debug, Copy, Clone, PartialEq)] 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). 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 { fn default() -> Self { Self { + spacing: Relative::new(1.0).into(), + leading: Relative::new(0.5).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, /// The linear to apply on the base font size. 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 next `*` adds or removes font weight. pub strong: bool, @@ -141,6 +144,8 @@ impl Default for FontState { stretch: FontStretch::Normal, }, size: Length::pt(11.0), + top_edge: VerticalFontMetric::CapHeight, + bottom_edge: VerticalFontMetric::Baseline, scale: Linear::ONE, strong: false, emph: false, diff --git a/src/export/pdf.rs b/src/export/pdf.rs index f86f977c..43c58403 100644 --- a/src/export/pdf.rs +++ b/src/export/pdf.rs @@ -180,6 +180,7 @@ impl<'a> PdfExporter<'a> { Element::Text(shaped) => { let mut text = content.text(); + // Check if we need to issue a font switching action. if shaped.face != face || shaped.font_size != size { face = shaped.face; diff --git a/src/layout/spacing.rs b/src/layout/spacing.rs index 4b564a2b..2bcb7ac1 100644 --- a/src/layout/spacing.rs +++ b/src/layout/spacing.rs @@ -1,7 +1,6 @@ use std::fmt::{self, Debug, Formatter}; use super::*; -use crate::exec::Softness; /// A spacing node. #[derive(Copy, Clone, PartialEq)] @@ -10,13 +9,11 @@ pub struct NodeSpacing { pub amount: Length, /// Defines how spacing interacts with surrounding spacing. /// - /// Hard spacing assures that a fixed amount of spacing will always be - /// inserted. Soft spacing will be consumed by previous soft spacing or - /// neighbouring hard spacing and can be used to insert overridable spacing, - /// e.g. between words or paragraphs. - /// - /// This field is only used in evaluation, not in layouting. - pub softness: Softness, + /// Hard spacing (`softness = 0`) assures that a fixed amount of spacing + /// will always be inserted. Soft spacing (`softness >= 1`) will be consumed + /// by other spacing with lower softness and can be used to insert + /// overridable spacing, e.g. between words or paragraphs. + pub softness: u8, } impl Layout for NodeSpacing { @@ -27,10 +24,7 @@ impl Layout for NodeSpacing { impl Debug for NodeSpacing { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match self.softness { - Softness::Soft => write!(f, "Soft({})", self.amount), - Softness::Hard => write!(f, "Hard({})", self.amount), - } + write!(f, "Spacing({}, {})", self.amount, self.softness) } } diff --git a/src/layout/text.rs b/src/layout/text.rs index 256a6e6d..7b4eb08e 100644 --- a/src/layout/text.rs +++ b/src/layout/text.rs @@ -4,35 +4,41 @@ use std::rc::Rc; use fontdock::{FallbackTree, FontVariant}; use super::*; -use crate::shaping; +use crate::shaping::{shape, VerticalFontMetric}; /// A text node. #[derive(Clone, PartialEq)] pub struct NodeText { + /// The text. + pub text: String, /// The text direction. pub dir: Dir, /// How to align this text node in its parent. pub aligns: LayoutAligns, - /// The text. - pub text: String, - /// The font size. - pub font_size: Length, /// The families used for font fallback. pub families: Rc<FallbackTree>, /// The font variant, 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 { fn layout(&self, ctx: &mut LayoutContext, _: &Areas) -> Layouted { Layouted::Frame( - shaping::shape( + shape( &self.text, self.dir, - self.font_size, - &mut ctx.env.fonts, &self.families, self.variant, + self.font_size, + self.top_edge, + self.bottom_edge, + &mut ctx.env.fonts, ), self.aligns, ) diff --git a/src/library/font.rs b/src/library/font.rs index 460cfba1..ecc15d96 100644 --- a/src/library/font.rs +++ b/src/library/font.rs @@ -1,6 +1,7 @@ use fontdock::{FontStretch, FontStyle, FontWeight}; use super::*; +use crate::shaping::VerticalFontMetric; /// `font`: Configure the font. /// @@ -13,6 +14,8 @@ use super::*; /// - Font Style: `style`, of type `font-style`. /// - Font Weight: `weight`, of type `font-weight`. /// - 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`. /// - Sans-serif family definition: `sans-serif`, of type `font-familiy-list`. /// - Monospace family definition: `monospace`, of type `font-familiy-list`. @@ -22,15 +25,15 @@ use super::*; /// if present. /// /// # Relevant types and constants -/// - Type `font-family-list` -/// - coerces from `string` -/// - coerces from `array` -/// - coerces from `font-family` /// - Type `font-family` /// - `serif` /// - `sans-serif` /// - `monospace` /// - coerces from `string` +/// - Type `font-family-list` +/// - coerces from `string` +/// - coerces from `array` +/// - coerces from `font-family` /// - Type `font-style` /// - `normal` /// - `italic` @@ -46,12 +49,20 @@ use super::*; /// - `extrabold` (800) /// - `black` (900) /// - coerces from `integer` +/// - Type `vertical-font-metric` +/// - `ascender` +/// - `cap-height` +/// - `x-height` +/// - `baseline` +/// - `descender` pub fn font(ctx: &mut EvalContext, args: &mut ValueArgs) -> Value { let size = args.find::<Linear>(ctx); let list: Vec<_> = args.filter::<FontFamily>(ctx).map(|f| f.to_string()).collect(); let style = args.get(ctx, "style"); let weight = args.get(ctx, "weight"); 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 sans_serif = args.get(ctx, "sans-serif"); 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; } + 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 &[ (FontFamily::Serif, &serif), (FontFamily::SansSerif, &sans_serif), @@ -185,3 +204,7 @@ typify! { }; }, } + +typify! { + VerticalFontMetric: "vertical font metric", +} diff --git a/src/library/mod.rs b/src/library/mod.rs index 09fda20f..453eab26 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -26,9 +26,9 @@ use std::fmt::{self, Display, Formatter}; use fontdock::{FontStyle, FontWeight}; use crate::eval::{Scope, ValueAny, ValueFunc}; -use crate::exec::Softness; use crate::layout::*; use crate::prelude::*; +use crate::shaping::VerticalFontMetric; /// Construct a scope containing all standard library definitions. pub fn new() -> Scope { @@ -81,6 +81,11 @@ pub fn new() -> Scope { set!(any: "bold", FontWeight::BOLD); set!(any: "extrabold", FontWeight::EXTRABOLD); 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 } diff --git a/src/library/page.rs b/src/library/page.rs index 17021607..f7d76eaf 100644 --- a/src/library/page.rs +++ b/src/library/page.rs @@ -95,13 +95,13 @@ pub fn page(ctx: &mut EvalContext, args: &mut ValueArgs) -> Value { } 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 { // TODO: Restrict body to a single page? body.exec(ctx); 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 { let span = args.span; Value::template("pagebreak", move |ctx| { - ctx.finish_page(true, Softness::Hard, span); + ctx.finish_page(true, true, span); }) } diff --git a/src/library/spacing.rs b/src/library/spacing.rs index d8f18f41..4965a220 100644 --- a/src/library/spacing.rs +++ b/src/library/spacing.rs @@ -28,7 +28,7 @@ fn spacing(ctx: &mut EvalContext, args: &mut ValueArgs, axis: SpecAxis) -> Value Value::template("spacing", move |ctx| { if let Some(linear) = spacing { 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() { ctx.push_into_stack(spacing); } else { diff --git a/src/shaping.rs b/src/shaping.rs index a8d2b2bf..41119639 100644 --- a/src/shaping.rs +++ b/src/shaping.rs @@ -4,7 +4,7 @@ //! font for each individual character. When the direction is right-to-left, the //! 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 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. pub fn shape( text: &str, dir: Dir, - font_size: Length, - loader: &mut FontLoader, fallback: &FallbackTree, variant: FontVariant, + font_size: Length, + top_edge: VerticalFontMetric, + bottom_edge: VerticalFontMetric, + loader: &mut FontLoader, ) -> 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 offset = Length::ZERO; - let mut ascender = Length::ZERO; - let mut descender = Length::ZERO; + let mut width = Length::ZERO; + let mut top = Length::ZERO; + let mut bottom = Length::ZERO; // Create an iterator with conditional direction. let mut forwards = text.chars(); @@ -86,7 +121,7 @@ pub fn shape( let query = FaceQuery { fallback: fallback.iter(), variant, c }; if let Some(id) = loader.query(query) { 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, None => continue, }; @@ -96,27 +131,35 @@ pub fn shape( // Flush the buffer and reset the metrics if we use a new font face. if shaped.face != id { - place(&mut frame, shaped, offset, ascender, descender); + place(&mut frame, shaped, width, top, bottom); shaped = Shaped::new(id, font_size); - offset = Length::ZERO; - ascender = convert(f64::from(face.ascender())); - descender = convert(f64::from(face.descender())); + width = Length::ZERO; + top = convert(f64::from(lookup_metric(face, top_edge))); + bottom = convert(f64::from(lookup_metric(face, bottom_edge))); } shaped.text.push(c); shaped.glyphs.push(glyph); - shaped.offsets.push(offset); - offset += convert(f64::from(width)); + shaped.offsets.push(width); + width += convert(f64::from(glyph_width)); } } - // Flush the last buffered parts of the word. - place(&mut frame, shaped, offset, ascender, descender); + place(&mut frame, shaped, width, top, bottom); 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. fn lookup_glyph(face: &Face, c: char) -> Option<(GlyphId, u16)> { let glyph = face.glyph_index(c)?; @@ -124,18 +167,32 @@ fn lookup_glyph(face: &Face, c: char) -> Option<(GlyphId, u16)> { Some((glyph, width)) } -/// Place shaped text into a frame. -fn place( - frame: &mut Frame, - shaped: Shaped, - offset: Length, - ascender: Length, - descender: Length, -) { - if !shaped.text.is_empty() { - let pos = Point::new(frame.size.width, ascender); - frame.push(pos, Element::Text(shaped)); - frame.size.width += offset; - frame.size.height = frame.size.height.max(ascender - descender); +/// Look up a vertical metric. +fn lookup_metric(face: &Face, metric: VerticalFontMetric) -> i16 { + match metric { + VerticalFontMetric::Ascender => lookup_ascender(face), + VerticalFontMetric::CapHeight => face + .capital_height() + .filter(|&h| h > 0) + .unwrap_or_else(|| lookup_ascender(face)), + VerticalFontMetric::XHeight => face + .x_height() + .filter(|&h| h > 0) + .unwrap_or_else(|| lookup_ascender(face)), + 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()) +} |
