Unify font classes + By-value-contexts ⚖

This commit is contained in:
Laurenz 2019-06-22 15:32:19 +02:00
parent c7ee2b393a
commit 099ce71aba
8 changed files with 265 additions and 280 deletions

View File

@ -6,7 +6,7 @@ use std::path::{Path, PathBuf};
use std::process; use std::process;
use typeset::Typesetter; use typeset::Typesetter;
use typeset::{font::FileSystemFontProvider, font_info}; use typeset::{font::FileSystemFontProvider, font};
use typeset::export::pdf::PdfExporter; use typeset::export::pdf::PdfExporter;
@ -50,20 +50,20 @@ fn run() -> Result<(), Box<Error>> {
// Create a typesetter with a font provider that provides the default fonts. // Create a typesetter with a font provider that provides the default fonts.
let mut typesetter = Typesetter::new(); let mut typesetter = Typesetter::new();
typesetter.add_font_provider(FileSystemFontProvider::new("fonts", vec![ typesetter.add_font_provider(FileSystemFontProvider::new("../fonts", vec![
("CMU-SansSerif-Regular.ttf", font_info!(["Computer Modern", SansSerif])), ("CMU-SansSerif-Regular.ttf", font!["Computer Modern", Regular, SansSerif]),
("CMU-SansSerif-Italic.ttf", font_info!(["Computer Modern", SansSerif], italic)), ("CMU-SansSerif-Italic.ttf", font!["Computer Modern", Italic, SansSerif]),
("CMU-SansSerif-Bold.ttf", font_info!(["Computer Modern", SansSerif], bold)), ("CMU-SansSerif-Bold.ttf", font!["Computer Modern", Bold, SansSerif]),
("CMU-SansSerif-Bold-Italic.ttf", font_info!(["Computer Modern", SansSerif], bold, italic)), ("CMU-SansSerif-Bold-Italic.ttf", font!["Computer Modern", Bold, Italic, SansSerif]),
("CMU-Serif-Regular.ttf", font_info!(["Computer Modern", Serif])), ("CMU-Serif-Regular.ttf", font!["Computer Modern", Regular, Serif]),
("CMU-Serif-Italic.ttf", font_info!(["Computer Modern", Serif], italic)), ("CMU-Serif-Italic.ttf", font!["Computer Modern", Italic, Serif]),
("CMU-Serif-Bold.ttf", font_info!(["Computer Modern", Serif], bold)), ("CMU-Serif-Bold.ttf", font!["Computer Modern", Bold, Serif]),
("CMU-Serif-Bold-Italic.ttf", font_info!(["Computer Modern", Serif], bold, italic)), ("CMU-Serif-Bold-Italic.ttf", font!["Computer Modern", Bold, Italic, Serif]),
("CMU-Typewriter-Regular.ttf", font_info!(["Computer Modern", Monospace])), ("CMU-Typewriter-Regular.ttf", font!["Computer Modern", Regular, Monospace]),
("CMU-Typewriter-Italic.ttf", font_info!(["Computer Modern", Monospace], italic)), ("CMU-Typewriter-Italic.ttf", font!["Computer Modern", Italic, Monospace]),
("CMU-Typewriter-Bold.ttf", font_info!(["Computer Modern", Monospace], bold)), ("CMU-Typewriter-Bold.ttf", font!["Computer Modern", Bold, Monospace]),
("CMU-Typewriter-Bold-Italic.ttf", font_info!(["Computer Modern", Monospace], bold, italic)), ("CMU-Typewriter-Bold-Italic.ttf", font!["Computer Modern", Bold, Italic, Monospace]),
("NotoEmoji-Regular.ttf", font_info!(["NotoEmoji", "Noto", SansSerif, Serif, Monospace])), ("NotoEmoji-Regular.ttf", font!["Noto", Regular, SansSerif, Serif, Monospace]),
])); ]));
// Typeset the source code. // Typeset the source code.

View File

@ -154,94 +154,83 @@ pub struct FontMetrics {
/// Categorizes a font. /// Categorizes a font.
/// ///
/// Can be constructed conveniently with the [`font_info`] macro. /// Can be constructed conveniently with the [`font`] macro.
#[derive(Debug, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct FontInfo { pub struct FontInfo {
/// The font families this font is part of. /// The font families this font is part of.
pub families: Vec<FontFamily>, pub classes: Vec<FontClass>,
/// Whether the font is italic.
pub italic: bool,
/// Whether the font bold.
pub bold: bool,
} }
/// A family of fonts. impl FontInfo {
/// Create a new font info from an iterator of classes.
pub fn new<I>(classes: I) -> FontInfo where I: IntoIterator<Item=FontClass> {
FontInfo { classes: classes.into_iter().collect() }
}
}
/// A class of fonts.
#[derive(Debug, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum FontFamily { pub enum FontClass {
Serif, Serif,
SansSerif, SansSerif,
Monospace, Monospace,
/// A custom class like _Arial_ or _Times_. Regular,
Named(String), Bold,
Italic,
/// A custom family like _Arial_ or _Times_.
Family(String),
} }
/// A macro to create [FontInfos](crate::font::FontInfo) easily. /// A macro to create [FontInfos](crate::font::FontInfo) easily.
/// ///
/// Accepts first a bracketed, ordered list of font families. Allowed are string expressions as well /// Accepts an ordered list of font classes. Strings expressions are parsed
/// as the three base families `SansSerif`, `Serif` and `Monospace`. Then there may follow /// into custom `Family`-variants and others can be named directly.
/// (separated by commas) the keywords `italic` and/or `bold`.
/// ///
/// # Examples /// # Examples
/// The font _Noto Sans_ in regular typeface. /// The font _Noto Sans_ in regular typeface.
/// ``` /// ```
/// # use typeset::font_info; /// # use typeset::font;
/// font_info!(["NotoSans", "Noto", SansSerif]); /// font!["NotoSans", "Noto", Regular, SansSerif];
/// ``` /// ```
/// ///
/// The font _Noto Serif_ in italics and boldface. /// The font _Noto Serif_ in italics and boldface.
/// ``` /// ```
/// # use typeset::font_info; /// # use typeset::font;
/// font_info!(["NotoSerif", "Noto", Serif], italic, bold); /// font!["NotoSerif", "Noto", Bold, Italic, Serif];
/// ``` /// ```
/// ///
/// The font _Arial_ in italics. /// The font _Arial_ in italics.
/// ``` /// ```
/// # use typeset::font_info; /// # use typeset::font;
/// font_info!(["Arial", SansSerif], italic); /// font!["Arial", Italic, SansSerif];
/// ``` /// ```
/// ///
/// The font _Noto Emoji_, which works with all base families. 🙂 /// The font _Noto Emoji_, which works with all base families. 🙂
/// ``` /// ```
/// # use typeset::font_info; /// # use typeset::font;
/// font_info!(["NotoEmoji", "Noto", SansSerif, Serif, Monospace]); /// font!["NotoEmoji", "Noto", Regular, SansSerif, Serif, Monospace];
/// ``` /// ```
#[macro_export] #[macro_export]
macro_rules! font_info { macro_rules! font {
// Parse class list one by one.
(@__cls $v:expr) => {};
(@__cls $v:expr, $c:ident) => { $v.push($crate::font::FontClass::$c); };
(@__cls $v:expr, $c:ident, $($tts:tt)*) => {
font!(@__cls $v, $c);
font!(@__cls $v, $($tts)*)
};
(@__cls $v:expr, $f:expr) => { $v.push( $crate::font::FontClass::Family($f.to_string())); };
(@__cls $v:expr, $f:expr, $($tts:tt)*) => {
font!(@__cls $v, $f);
font!(@__cls $v, $($tts)*)
};
// Entry point // Entry point
([$($tts:tt)*] $(,$style:tt)*) => {{ ($($tts:tt)*) => {{
let mut families = Vec::new(); let mut classes = Vec::new();
font_info!(@__fam families, $($tts)*); font!(@__cls classes, $($tts)*);
$crate::font::FontInfo { classes }
#[allow(unused)] let mut italic = false;
#[allow(unused)] let mut bold = false;
$( font_info!(@__sty (italic, bold) $style); )*
$crate::font::FontInfo { families, italic, bold }
}}; }};
// Parse family list
(@__fam $v:expr) => {};
(@__fam $v:expr, $f:ident) => { $v.push(font_info!(@__gen $f)); };
(@__fam $v:expr, $f:ident, $($tts:tt)*) => {
font_info!(@__fam $v, $f);
font_info!(@__fam $v, $($tts)*)
};
(@__fam $v:expr, $f:expr) => {
$v.push( $crate::font::FontFamily::Named($f.to_string()));
};
(@__fam $v:expr, $f:expr, $($tts:tt)*) => {
font_info!(@__fam $v, $f);
font_info!(@__fam $v, $($tts)*)
};
// Parse styles (italic/bold)
(@__sty ($i:ident, $b:ident) italic) => { $i = true; };
(@__sty ($i:ident, $b:ident) bold) => { $b = true; };
// Parse enum variants
(@__gen SansSerif) => { $crate::font::FontFamily::SansSerif };
(@__gen Serif) => { $crate::font::FontFamily::Serif };
(@__gen Monospace) => { $crate::font::FontFamily::Monospace };
} }
//------------------------------------------------------------------------------------------------// //------------------------------------------------------------------------------------------------//
@ -282,10 +271,10 @@ impl FileSystemFontProvider {
/// Serve the two fonts `NotoSans-Regular` and `NotoSans-Italic` from the local folder /// Serve the two fonts `NotoSans-Regular` and `NotoSans-Italic` from the local folder
/// `../fonts`. /// `../fonts`.
/// ``` /// ```
/// # use typeset::{font::FileSystemFontProvider, font_info}; /// # use typeset::{font::FileSystemFontProvider, font};
/// FileSystemFontProvider::new("../fonts", vec![ /// FileSystemFontProvider::new("../fonts", vec![
/// ("NotoSans-Regular.ttf", font_info!(["NotoSans", SansSerif])), /// ("NotoSans-Regular.ttf", font!["NotoSans", Regular, SansSerif]),
/// ("NotoSans-Italic.ttf", font_info!(["NotoSans", SansSerif], italic)), /// ("NotoSans-Italic.ttf", font!["NotoSans", Italic, SansSerif]),
/// ]); /// ]);
/// ``` /// ```
#[inline] #[inline]
@ -395,14 +384,17 @@ impl<'p> FontLoader<'p> {
} }
drop(state); drop(state);
// The outermost loop goes over the families because we want to serve the font that matches // The outermost loop goes over the fallbacks because we want to serve the font that matches
// the first possible family. // the first possible class.
for family in &query.families { for class in &query.fallback {
// For each family now go over all font infos from all font providers. // For each class now go over all font infos from all font providers.
for (provider, infos) in self.providers.iter().zip(&self.provider_fonts) { for (provider, infos) in self.providers.iter().zip(&self.provider_fonts) {
for info in infos.iter() { for info in infos.iter() {
// Proceed only if this font matches the query. let matches = info.classes.contains(class)
if Self::matches(&query, family, info) { && query.classes.iter().all(|class| info.classes.contains(class));
// Proceed only if this font matches the query up to now.
if matches {
let mut state = self.state.borrow_mut(); let mut state = self.state.borrow_mut();
// Check if we have already loaded this font before, otherwise, we will load // Check if we have already loaded this font before, otherwise, we will load
@ -483,12 +475,6 @@ impl<'p> FontLoader<'p> {
if maybe_index.is_some() { Some(font) } else { None } if maybe_index.is_some() { Some(font) } else { None }
}).collect() }).collect()
} }
/// Checks whether the query and the family match the info.
fn matches(query: &FontQuery, family: &FontFamily, info: &FontInfo) -> bool {
info.italic == query.italic && info.bold == query.bold
&& info.families.contains(family)
}
} }
impl Debug for FontLoader<'_> { impl Debug for FontLoader<'_> {
@ -510,13 +496,11 @@ impl Debug for FontLoader<'_> {
pub struct FontQuery { pub struct FontQuery {
/// Which character is needed. /// Which character is needed.
pub character: char, pub character: char,
/// Whether the font should be in italics. /// Which classes the font has to be part of.
pub italic: bool, pub classes: Vec<FontClass>,
/// Whether the font should be in boldface. /// A sequence of classes. The font matching the leftmost class in this sequence
pub bold: bool, /// should be returned.
/// A fallback list of font families to accept. The font matching the first possible family in pub fallback: Vec<FontClass>,
/// this list satisfying all other constraints should be returned.
pub families: Vec<FontFamily>,
} }
//------------------------------------------------------------------------------------------------// //------------------------------------------------------------------------------------------------//
@ -626,7 +610,7 @@ impl<'a> Subsetter<'a> {
mapping, mapping,
widths, widths,
default_glyph: self.font.default_glyph, default_glyph: self.font.default_glyph,
metrics: self.font.metrics.clone(), metrics: self.font.metrics,
}) })
} }
@ -1031,33 +1015,21 @@ mod tests {
/// Tests the font info macro. /// Tests the font info macro.
#[test] #[test]
fn font_info_macro() { fn font_macro() {
use FontFamily::{SansSerif as S, Serif as F, Monospace as M}; use FontClass::*;
#[allow(non_snake_case)]
fn N(family: &str) -> FontFamily { FontFamily::Named(family.to_string()) }
assert_eq!(font_info!(["NotoSans", "Noto", SansSerif]), FontInfo { assert_eq!(font!["NotoSans", "Noto", Regular, SansSerif], FontInfo {
families: vec![N("NotoSans"), N("Noto"), S], classes: vec![
italic: false, Family("NotoSans".to_owned()), Family("Noto".to_owned()),
bold: false, Regular, SansSerif
]
}); });
assert_eq!(font_info!(["NotoSerif", Serif, "Noto"], italic), FontInfo { assert_eq!(font!["NotoSerif", Serif, Italic, "Noto"], FontInfo {
families: vec![N("NotoSerif"), F, N("Noto")], classes: vec![
italic: true, Family("NotoSerif".to_owned()), Serif, Italic,
bold: false, Family("Noto".to_owned())
}); ],
assert_eq!(font_info!(["NotoSans", "Noto", SansSerif], italic, bold), FontInfo {
families: vec![N("NotoSans"), N("Noto"), S],
italic: true,
bold: true,
});
assert_eq!(font_info!(["NotoEmoji", "Noto", SansSerif, Serif, Monospace]), FontInfo {
families: vec![N("NotoEmoji"), N("Noto"), S, F, M],
italic: false,
bold: false,
}); });
} }
} }

View File

@ -4,6 +4,7 @@ use std::any::Any;
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt::{self, Debug, Formatter}; use std::fmt::{self, Debug, Formatter};
use crate::font::FontClass;
use crate::layout::{layout, Layout, LayoutContext, LayoutResult}; use crate::layout::{layout, Layout, LayoutContext, LayoutResult};
use crate::layout::flex::FlexLayout; use crate::layout::flex::FlexLayout;
use crate::parsing::{parse, ParseContext, ParseError, ParseResult}; use crate::parsing::{parse, ParseContext, ParseError, ParseResult};
@ -19,14 +20,14 @@ use crate::syntax::{SyntaxTree, FuncHeader};
/// functions, that is they fulfill the bounds `Debug + PartialEq + 'static`. /// functions, that is they fulfill the bounds `Debug + PartialEq + 'static`.
pub trait Function: FunctionBounds { pub trait Function: FunctionBounds {
/// Parse the header and body into this function given a context. /// Parse the header and body into this function given a context.
fn parse(header: &FuncHeader, body: Option<&str>, ctx: &ParseContext) fn parse(header: &FuncHeader, body: Option<&str>, ctx: ParseContext)
-> ParseResult<Self> where Self: Sized; -> ParseResult<Self> where Self: Sized;
/// Layout this function given a context. /// Layout this function given a context.
/// ///
/// Returns optionally the resulting layout and a new context if changes to the context should /// Returns optionally the resulting layout and a new context if changes to the context should
/// be made. /// be made.
fn layout(&self, ctx: &LayoutContext) -> LayoutResult<Option<Layout>>; fn layout(&self, ctx: LayoutContext) -> LayoutResult<Option<Layout>>;
} }
impl PartialEq for dyn Function { impl PartialEq for dyn Function {
@ -67,7 +68,7 @@ pub struct Scope {
} }
/// A function which parses a function invocation into a function type. /// A function which parses a function invocation into a function type.
type ParseFunc = dyn Fn(&FuncHeader, Option<&str>, &ParseContext) type ParseFunc = dyn Fn(&FuncHeader, Option<&str>, ParseContext)
-> ParseResult<Box<dyn Function>>; -> ParseResult<Box<dyn Function>>;
impl Scope { impl Scope {
@ -112,12 +113,12 @@ impl Debug for Scope {
/// Creates style functions like bold and italic. /// Creates style functions like bold and italic.
macro_rules! style_func { macro_rules! style_func {
($(#[$outer:meta])* pub struct $struct:ident { $name:expr }, ($(#[$outer:meta])* pub struct $struct:ident { $name:expr },
$new_ctx:ident => $ctx_change:block) => { $style:ident => $style_change:block) => {
$(#[$outer])* $(#[$outer])*
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub struct $struct { body: SyntaxTree } pub struct $struct { body: SyntaxTree }
impl Function for $struct { impl Function for $struct {
fn parse(header: &FuncHeader, body: Option<&str>, ctx: &ParseContext) fn parse(header: &FuncHeader, body: Option<&str>, ctx: ParseContext)
-> ParseResult<Self> where Self: Sized { -> ParseResult<Self> where Self: Sized {
// Accept only invocations without arguments and with body. // Accept only invocations without arguments and with body.
if header.args.is_empty() && header.kwargs.is_empty() { if header.args.is_empty() && header.kwargs.is_empty() {
@ -131,13 +132,16 @@ macro_rules! style_func {
} }
} }
fn layout(&self, ctx: &LayoutContext) -> LayoutResult<Option<Layout>> { fn layout(&self, ctx: LayoutContext) -> LayoutResult<Option<Layout>> {
// Change the context. // Change the context.
let mut $new_ctx = ctx.clone(); let mut $style = ctx.style.clone();
$ctx_change $style_change
// Create a box and put it into a flex layout. // Create a box and put it into a flex layout.
let boxed = layout(&self.body, &$new_ctx)?; let boxed = layout(&self.body, LayoutContext {
style: &$style,
.. ctx
})?;
let flex = FlexLayout::from_box(boxed); let flex = FlexLayout::from_box(boxed);
Ok(Some(Layout::Flex(flex))) Ok(Some(Layout::Flex(flex)))
@ -149,11 +153,11 @@ macro_rules! style_func {
style_func! { style_func! {
/// Typesets text in bold. /// Typesets text in bold.
pub struct BoldFunc { "bold" }, pub struct BoldFunc { "bold" },
ctx => { ctx.style.bold = !ctx.style.bold } style => { style.toggle_class(FontClass::Bold) }
} }
style_func! { style_func! {
/// Typesets text in italics. /// Typesets text in italics.
pub struct ItalicFunc { "italic" }, pub struct ItalicFunc { "italic" },
ctx => { ctx.style.italic = !ctx.style.italic } style => { style.toggle_class(FontClass::Italic) }
} }

View File

@ -1,9 +1,12 @@
//! The layouting engine. //! The layouting engine.
use std::borrow::Cow;
use std::mem;
use crate::doc::LayoutAction; use crate::doc::LayoutAction;
use crate::font::{FontLoader, FontError}; use crate::font::{FontLoader, FontClass, FontError};
use crate::size::{Size, Size2D, SizeBox}; use crate::size::{Size, Size2D, SizeBox};
use crate::syntax::{SyntaxTree, Node}; use crate::syntax::{SyntaxTree, Node, FuncCall};
use crate::style::TextStyle; use crate::style::TextStyle;
use self::flex::{FlexLayout, FlexContext}; use self::flex::{FlexLayout, FlexContext};
@ -24,13 +27,18 @@ pub enum Layout {
Flex(FlexLayout), Flex(FlexLayout),
} }
/// Layout a syntax tree in a given context.
pub fn layout(tree: &SyntaxTree, ctx: LayoutContext) -> LayoutResult<BoxLayout> {
Layouter::new(tree, ctx).layout()
}
/// The context for layouting. /// The context for layouting.
#[derive(Debug, Clone)] #[derive(Debug, Copy, Clone)]
pub struct LayoutContext<'a, 'p> { pub struct LayoutContext<'a, 'p> {
/// Loads fonts matching queries. /// Loads fonts matching queries.
pub loader: &'a FontLoader<'p>, pub loader: &'a FontLoader<'p>,
/// Base style to set text with. /// Base style to set text with.
pub style: TextStyle, pub style: &'a TextStyle,
/// The space to layout in. /// The space to layout in.
pub space: LayoutSpace, pub space: LayoutSpace,
} }
@ -57,62 +65,25 @@ impl LayoutSpace {
} }
} }
/// Layout a syntax tree in a given context.
pub fn layout(tree: &SyntaxTree, ctx: &LayoutContext) -> LayoutResult<BoxLayout> {
Layouter::new(tree, ctx).layout()
}
/// Transforms a syntax tree into a box layout. /// Transforms a syntax tree into a box layout.
#[derive(Debug)] #[derive(Debug)]
struct Layouter<'a, 'p> { struct Layouter<'a, 'p> {
tree: &'a SyntaxTree, tree: &'a SyntaxTree,
box_layouter: BoxLayouter, box_layouter: BoxLayouter,
flex_layout: FlexLayout, flex_layout: FlexLayout,
flex_ctx: FlexContext, loader: &'a FontLoader<'p>,
text_ctx: TextContext<'a, 'p>, style: Cow<'a, TextStyle>,
func_ctx: LayoutContext<'a, 'p>,
} }
impl<'a, 'p> Layouter<'a, 'p> { impl<'a, 'p> Layouter<'a, 'p> {
/// Create a new layouter. /// Create a new layouter.
fn new(tree: &'a SyntaxTree, ctx: &LayoutContext<'a, 'p>) -> Layouter<'a, 'p> { fn new(tree: &'a SyntaxTree, ctx: LayoutContext<'a, 'p>) -> Layouter<'a, 'p> {
// The top-level context for arranging paragraphs.
let box_ctx = BoxContext { space: ctx.space };
// The sub-level context for arranging pieces of text.
let flex_ctx = FlexContext {
space: LayoutSpace {
dimensions: ctx.space.usable(),
padding: SizeBox::zero(),
shrink_to_fit: true,
},
flex_spacing: (ctx.style.line_spacing - 1.0) * Size::pt(ctx.style.font_size),
};
// The mutable context for layouting single pieces of text.
let text_ctx = TextContext {
loader: &ctx.loader,
style: ctx.style.clone(),
};
// The mutable context for layouting single functions.
let func_ctx = LayoutContext {
loader: &ctx.loader,
style: ctx.style.clone(),
space: LayoutSpace {
dimensions: ctx.space.usable(),
padding: SizeBox::zero(),
shrink_to_fit: true,
},
};
Layouter { Layouter {
tree, tree,
box_layouter: BoxLayouter::new(box_ctx), box_layouter: BoxLayouter::new(BoxContext { space: ctx.space }),
flex_layout: FlexLayout::new(), flex_layout: FlexLayout::new(),
flex_ctx, loader: ctx.loader,
text_ctx, style: Cow::Borrowed(ctx.style)
func_ctx,
} }
} }
@ -122,79 +93,101 @@ impl<'a, 'p> Layouter<'a, 'p> {
for node in &self.tree.nodes { for node in &self.tree.nodes {
match node { match node {
// Layout a single piece of text. // Layout a single piece of text.
Node::Text(text) => { Node::Text(text) => self.layout_text(text, false)?,
let boxed = self::text::layout(text, &self.text_ctx)?;
self.flex_layout.add_box(boxed); // Add a space.
},
Node::Space => { Node::Space => {
if !self.flex_layout.is_empty() { if !self.flex_layout.is_empty() {
let boxed = self::text::layout(" ", &self.text_ctx)?; self.layout_text(" ", true)?;
self.flex_layout.add_glue(boxed);
} }
}, },
// Finish the current flex layout and add it to the box layouter. // Finish the current flex layout and add it to the box layouter.
// Then start a new flex layouting process.
Node::Newline => { Node::Newline => {
// Finish the current paragraph into a box and add it. // Finish the current paragraph into a box and add it.
let boxed = self.flex_layout.finish(self.flex_ctx)?; self.layout_flex()?;
self.box_layouter.add_box(boxed)?;
// Create a fresh flex layout for the next paragraph. // Add some paragraph spacing.
self.flex_ctx.space.dimensions = self.box_layouter.remaining(); let size = Size::pt(self.style.font_size)
self.flex_layout = FlexLayout::new(); * (self.style.line_spacing * self.style.paragraph_spacing - 1.0);
self.add_paragraph_spacing()?; self.box_layouter.add_space(size)?;
}, },
// Toggle the text styles. // Toggle the text styles.
Node::ToggleItalics => { Node::ToggleItalics => self.style.to_mut().toggle_class(FontClass::Italic),
self.text_ctx.style.italic = !self.text_ctx.style.italic; Node::ToggleBold => self.style.to_mut().toggle_class(FontClass::Bold),
self.func_ctx.style.italic = !self.func_ctx.style.italic;
},
Node::ToggleBold => {
self.text_ctx.style.bold = !self.text_ctx.style.bold;
self.func_ctx.style.bold = !self.func_ctx.style.bold;
},
// Execute a function. // Execute a function.
Node::Func(func) => { Node::Func(func) => self.layout_func(func)?,
self.func_ctx.space.dimensions = self.box_layouter.remaining(); }
let layout = func.body.layout(&self.func_ctx)?; }
// If there are remainings, add them to the layout.
if !self.flex_layout.is_empty() {
self.layout_flex()?;
}
Ok(self.box_layouter.finish())
}
/// Layout a piece of text into a box.
fn layout_text(&mut self, text: &str, glue: bool) -> LayoutResult<()> {
let boxed = self::text::layout(text, TextContext {
loader: &self.loader,
style: &self.style,
})?;
if glue {
self.flex_layout.add_glue(boxed);
} else {
self.flex_layout.add_box(boxed);
}
Ok(())
}
/// Finish the current flex run and return the resulting box.
fn layout_flex(&mut self) -> LayoutResult<()> {
let mut layout = FlexLayout::new();
mem::swap(&mut layout, &mut self.flex_layout);
let boxed = layout.finish(FlexContext {
space: LayoutSpace {
dimensions: self.box_layouter.remaining(),
padding: SizeBox::zero(),
shrink_to_fit: true,
},
flex_spacing: (self.style.line_spacing - 1.0) * Size::pt(self.style.font_size),
})?;
self.box_layouter.add_box(boxed)
}
/// Layout a function.
fn layout_func(&mut self, func: &FuncCall) -> LayoutResult<()> {
let layout = func.body.layout(LayoutContext {
loader: &self.loader,
style: &self.style,
space: LayoutSpace {
dimensions: self.box_layouter.remaining(),
padding: SizeBox::zero(),
shrink_to_fit: true,
},
})?;
// Add the potential layout. // Add the potential layout.
if let Some(layout) = layout { if let Some(layout) = layout {
match layout { match layout {
Layout::Boxed(boxed) => { Layout::Boxed(boxed) => {
// Finish the previous flex run before adding the box. // Finish the previous flex run before adding the box.
let previous = self.flex_layout.finish(self.flex_ctx)?; self.layout_flex()?;
self.box_layouter.add_box(previous)?;
self.box_layouter.add_box(boxed)?; self.box_layouter.add_box(boxed)?;
// Create a fresh flex layout for the following content.
self.flex_ctx.space.dimensions = self.box_layouter.remaining();
self.flex_layout = FlexLayout::new();
}, },
Layout::Flex(flex) => self.flex_layout.add_flexible(flex), Layout::Flex(flex) => self.flex_layout.add_flexible(flex),
} }
} }
},
}
}
// If there are remainings, add them to the layout. Ok(())
if !self.flex_layout.is_empty() {
let boxed = self.flex_layout.finish(self.flex_ctx)?;
self.box_layouter.add_box(boxed)?;
}
Ok(self.box_layouter.finish())
}
/// Add the spacing between two paragraphs.
fn add_paragraph_spacing(&mut self) -> LayoutResult<()> {
let size = Size::pt(self.text_ctx.style.font_size)
* (self.text_ctx.style.line_spacing * self.text_ctx.style.paragraph_spacing - 1.0);
self.box_layouter.add_space(size)
} }
} }

View File

@ -7,16 +7,16 @@ use super::*;
/// The context for text layouting. /// The context for text layouting.
#[derive(Debug, Clone)] #[derive(Debug, Copy, Clone)]
pub struct TextContext<'a, 'p> { pub struct TextContext<'a, 'p> {
/// Loads fonts matching queries. /// Loads fonts matching queries.
pub loader: &'a FontLoader<'p>, pub loader: &'a FontLoader<'p>,
/// Base style to set text with. /// Base style to set text with.
pub style: TextStyle, pub style: &'a TextStyle,
} }
/// Layout one piece of text without any breaks as one continous box. /// Layout one piece of text without any breaks as one continous box.
pub fn layout(text: &str, ctx: &TextContext) -> LayoutResult<BoxLayout> { pub fn layout(text: &str, ctx: TextContext) -> LayoutResult<BoxLayout> {
let mut actions = Vec::new(); let mut actions = Vec::new();
let mut active_font = std::usize::MAX; let mut active_font = std::usize::MAX;
let mut buffer = String::new(); let mut buffer = String::new();
@ -26,9 +26,8 @@ pub fn layout(text: &str, ctx: &TextContext) -> LayoutResult<BoxLayout> {
for character in text.chars() { for character in text.chars() {
// Retrieve the best font for this character. // Retrieve the best font for this character.
let (index, font) = ctx.loader.get(FontQuery { let (index, font) = ctx.loader.get(FontQuery {
families: ctx.style.font_families.clone(), classes: ctx.style.classes.clone(),
italic: ctx.style.italic, fallback: ctx.style.fallback.clone(),
bold: ctx.style.bold,
character, character,
}).ok_or_else(|| LayoutError::NoSuitableFont(character))?; }).ok_or_else(|| LayoutError::NoSuitableFont(character))?;

View File

@ -16,7 +16,7 @@
//! ``` //! ```
//! use std::fs::File; //! use std::fs::File;
//! use typeset::Typesetter; //! use typeset::Typesetter;
//! use typeset::{font::FileSystemFontProvider, font_info}; //! use typeset::{font::FileSystemFontProvider, font};
//! use typeset::export::pdf::PdfExporter; //! use typeset::export::pdf::PdfExporter;
//! //!
//! // Simple example source code. //! // Simple example source code.
@ -26,9 +26,9 @@
//! // (two sans-serif fonts and a fallback for the emoji). //! // (two sans-serif fonts and a fallback for the emoji).
//! let mut typesetter = Typesetter::new(); //! let mut typesetter = Typesetter::new();
//! typesetter.add_font_provider(FileSystemFontProvider::new("../fonts", vec![ //! typesetter.add_font_provider(FileSystemFontProvider::new("../fonts", vec![
//! ("CMU-Serif-Regular.ttf", font_info!(["Computer Modern", Serif])), //! ("CMU-Serif-Regular.ttf", font!["Computer Modern", Regular, Serif]),
//! ("CMU-Serif-Italic.ttf", font_info!(["Computer Modern", Serif], italic)), //! ("CMU-Serif-Italic.ttf", font!["Computer Modern", Italic, Serif]),
//! ("NotoEmoji-Regular.ttf", font_info!(["NotoEmoji", "Noto", SansSerif, Serif, Monospace])), //! ("NotoEmoji-Regular.ttf", font!["Noto", Regular, Serif, SansSerif, Monospace]),
//! ])); //! ]));
//! // Typeset the source code into a document. //! // Typeset the source code into a document.
//! let document = typesetter.typeset(src).unwrap(); //! let document = typesetter.typeset(src).unwrap();
@ -112,24 +112,21 @@ impl<'p> Typesetter<'p> {
#[inline] #[inline]
pub fn parse(&self, src: &str) -> ParseResult<SyntaxTree> { pub fn parse(&self, src: &str) -> ParseResult<SyntaxTree> {
let scope = Scope::with_std(); let scope = Scope::with_std();
let ctx = ParseContext { scope: &scope }; parse(src, ParseContext { scope: &scope })
parse(src, &ctx)
} }
/// Layout a syntax tree and return the layout and the referenced font list. /// Layout a syntax tree and return the layout and the referenced font list.
pub fn layout(&self, tree: &SyntaxTree) -> LayoutResult<(BoxLayout, Vec<Font>)> { pub fn layout(&self, tree: &SyntaxTree) -> LayoutResult<(BoxLayout, Vec<Font>)> {
let loader = FontLoader::new(&self.font_providers); let loader = FontLoader::new(&self.font_providers);
let ctx = LayoutContext { let pages = layout(&tree, LayoutContext {
loader: &loader, loader: &loader,
style: self.text_style.clone(), style: &self.text_style,
space: LayoutSpace { space: LayoutSpace {
dimensions: self.page_style.dimensions, dimensions: self.page_style.dimensions,
padding: self.page_style.margins, padding: self.page_style.margins,
shrink_to_fit: false, shrink_to_fit: false,
}, },
}; })?;
let pages = layout(&tree, &ctx)?;
Ok((pages, loader.into_fonts())) Ok((pages, loader.into_fonts()))
} }
@ -182,25 +179,25 @@ mod test {
use std::io::BufWriter; use std::io::BufWriter;
use crate::Typesetter; use crate::Typesetter;
use crate::export::pdf::PdfExporter; use crate::export::pdf::PdfExporter;
use crate::font::FileSystemFontProvider; use crate::font::{FileSystemFontProvider};
/// Create a _PDF_ with a name from the source code. /// Create a _PDF_ with a name from the source code.
fn test(name: &str, src: &str) { fn test(name: &str, src: &str) {
let mut typesetter = Typesetter::new(); let mut typesetter = Typesetter::new();
typesetter.add_font_provider(FileSystemFontProvider::new("../fonts", vec![ typesetter.add_font_provider(FileSystemFontProvider::new("../fonts", vec![
("CMU-SansSerif-Regular.ttf", font_info!(["Computer Modern", SansSerif])), ("CMU-SansSerif-Regular.ttf", font!["Computer Modern", Regular, SansSerif]),
("CMU-SansSerif-Italic.ttf", font_info!(["Computer Modern", SansSerif], italic)), ("CMU-SansSerif-Italic.ttf", font!["Computer Modern", Italic, SansSerif]),
("CMU-SansSerif-Bold.ttf", font_info!(["Computer Modern", SansSerif], bold)), ("CMU-SansSerif-Bold.ttf", font!["Computer Modern", Bold, SansSerif]),
("CMU-SansSerif-Bold-Italic.ttf", font_info!(["Computer Modern", SansSerif], bold, italic)), ("CMU-SansSerif-Bold-Italic.ttf", font!["Computer Modern", Bold, Italic, SansSerif]),
("CMU-Serif-Regular.ttf", font_info!(["Computer Modern", Serif])), ("CMU-Serif-Regular.ttf", font!["Computer Modern", Regular, Serif]),
("CMU-Serif-Italic.ttf", font_info!(["Computer Modern", Serif], italic)), ("CMU-Serif-Italic.ttf", font!["Computer Modern", Italic, Serif]),
("CMU-Serif-Bold.ttf", font_info!(["Computer Modern", Serif], bold)), ("CMU-Serif-Bold.ttf", font!["Computer Modern", Bold, Serif]),
("CMU-Serif-Bold-Italic.ttf", font_info!(["Computer Modern", Serif], bold, italic)), ("CMU-Serif-Bold-Italic.ttf", font!["Computer Modern", Bold, Italic, Serif]),
("CMU-Typewriter-Regular.ttf", font_info!(["Computer Modern", Monospace])), ("CMU-Typewriter-Regular.ttf", font!["Computer Modern", Regular, Monospace]),
("CMU-Typewriter-Italic.ttf", font_info!(["Computer Modern", Monospace], italic)), ("CMU-Typewriter-Italic.ttf", font!["Computer Modern", Italic, Monospace]),
("CMU-Typewriter-Bold.ttf", font_info!(["Computer Modern", Monospace], bold)), ("CMU-Typewriter-Bold.ttf", font!["Computer Modern", Bold, Monospace]),
("CMU-Typewriter-Bold-Italic.ttf", font_info!(["Computer Modern", Monospace], bold, italic)), ("CMU-Typewriter-Bold-Italic.ttf", font!["Computer Modern", Bold, Italic, Monospace]),
("NotoEmoji-Regular.ttf", font_info!(["NotoEmoji", "Noto", SansSerif, Serif, Monospace])), ("NotoEmoji-Regular.ttf", font!["Noto", Regular, SansSerif, Serif, Monospace]),
])); ]));
// Typeset into document. // Typeset into document.

View File

@ -326,12 +326,12 @@ impl Iterator for PeekableChars<'_> {
/// Parses source code into a syntax tree given a context. /// Parses source code into a syntax tree given a context.
#[inline] #[inline]
pub fn parse(src: &str, ctx: &ParseContext) -> ParseResult<SyntaxTree> { pub fn parse(src: &str, ctx: ParseContext) -> ParseResult<SyntaxTree> {
Parser::new(src, ctx).parse() Parser::new(src, ctx).parse()
} }
/// The context for parsing. /// The context for parsing.
#[derive(Debug)] #[derive(Debug, Copy, Clone)]
pub struct ParseContext<'a> { pub struct ParseContext<'a> {
/// The scope containing function definitions. /// The scope containing function definitions.
pub scope: &'a Scope, pub scope: &'a Scope,
@ -343,7 +343,7 @@ struct Parser<'s> {
src: &'s str, src: &'s str,
tokens: PeekableTokens<'s>, tokens: PeekableTokens<'s>,
state: ParserState, state: ParserState,
ctx: &'s ParseContext<'s>, ctx: ParseContext<'s>,
tree: SyntaxTree, tree: SyntaxTree,
} }
@ -360,12 +360,12 @@ enum ParserState {
impl<'s> Parser<'s> { impl<'s> Parser<'s> {
/// Create a new parser from the source and the context. /// Create a new parser from the source and the context.
fn new(src: &'s str, ctx: &'s ParseContext) -> Parser<'s> { fn new(src: &'s str, ctx: ParseContext<'s>) -> Parser<'s> {
Parser { Parser {
src, src,
tokens: PeekableTokens::new(tokenize(src)), tokens: PeekableTokens::new(tokenize(src)),
ctx,
state: ParserState::Body, state: ParserState::Body,
ctx,
tree: SyntaxTree::new(), tree: SyntaxTree::new(),
} }
} }
@ -813,7 +813,7 @@ mod parse_tests {
pub struct TreeFn(pub SyntaxTree); pub struct TreeFn(pub SyntaxTree);
impl Function for TreeFn { impl Function for TreeFn {
fn parse(_: &FuncHeader, body: Option<&str>, ctx: &ParseContext) fn parse(_: &FuncHeader, body: Option<&str>, ctx: ParseContext)
-> ParseResult<Self> where Self: Sized { -> ParseResult<Self> where Self: Sized {
if let Some(src) = body { if let Some(src) = body {
parse(src, ctx).map(|tree| TreeFn(tree)) parse(src, ctx).map(|tree| TreeFn(tree))
@ -822,7 +822,7 @@ mod parse_tests {
} }
} }
fn layout(&self, _: &LayoutContext) -> LayoutResult<Option<Layout>> { Ok(None) } fn layout(&self, _: LayoutContext) -> LayoutResult<Option<Layout>> { Ok(None) }
} }
/// A testing function without a body. /// A testing function without a body.
@ -830,7 +830,7 @@ mod parse_tests {
pub struct BodylessFn; pub struct BodylessFn;
impl Function for BodylessFn { impl Function for BodylessFn {
fn parse(_: &FuncHeader, body: Option<&str>, _: &ParseContext) fn parse(_: &FuncHeader, body: Option<&str>, _: ParseContext)
-> ParseResult<Self> where Self: Sized { -> ParseResult<Self> where Self: Sized {
if body.is_none() { if body.is_none() {
Ok(BodylessFn) Ok(BodylessFn)
@ -839,32 +839,32 @@ mod parse_tests {
} }
} }
fn layout(&self, _: &LayoutContext) -> LayoutResult<Option<Layout>> { Ok(None) } fn layout(&self, _: LayoutContext) -> LayoutResult<Option<Layout>> { Ok(None) }
} }
} }
/// Test if the source code parses into the syntax tree. /// Test if the source code parses into the syntax tree.
fn test(src: &str, tree: SyntaxTree) { fn test(src: &str, tree: SyntaxTree) {
let ctx = ParseContext { scope: &Scope::new() }; let ctx = ParseContext { scope: &Scope::new() };
assert_eq!(parse(src, &ctx).unwrap(), tree); assert_eq!(parse(src, ctx).unwrap(), tree);
} }
/// Test with a scope containing function definitions. /// Test with a scope containing function definitions.
fn test_scoped(scope: &Scope, src: &str, tree: SyntaxTree) { fn test_scoped(scope: &Scope, src: &str, tree: SyntaxTree) {
let ctx = ParseContext { scope }; let ctx = ParseContext { scope };
assert_eq!(parse(src, &ctx).unwrap(), tree); assert_eq!(parse(src, ctx).unwrap(), tree);
} }
/// Test if the source parses into the error. /// Test if the source parses into the error.
fn test_err(src: &str, err: &str) { fn test_err(src: &str, err: &str) {
let ctx = ParseContext { scope: &Scope::new() }; let ctx = ParseContext { scope: &Scope::new() };
assert_eq!(parse(src, &ctx).unwrap_err().to_string(), err); assert_eq!(parse(src, ctx).unwrap_err().to_string(), err);
} }
/// Test with a scope if the source parses into the error. /// Test with a scope if the source parses into the error.
fn test_err_scoped(scope: &Scope, src: &str, err: &str) { fn test_err_scoped(scope: &Scope, src: &str, err: &str) {
let ctx = ParseContext { scope }; let ctx = ParseContext { scope };
assert_eq!(parse(src, &ctx).unwrap_err().to_string(), err); assert_eq!(parse(src, ctx).unwrap_err().to_string(), err);
} }
/// Create a text node. /// Create a text node.

View File

@ -1,18 +1,17 @@
//! Styles for layouting. //! Styles for layouting.
use crate::font::FontFamily; use crate::font::FontClass;
use crate::size::{Size, Size2D, SizeBox}; use crate::size::{Size, Size2D, SizeBox};
/// Default styles for text. /// Default styles for text.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct TextStyle { pub struct TextStyle {
/// A fallback list of font families to use. /// The classes the font we want has to be part of.
pub font_families: Vec<FontFamily>, pub classes: Vec<FontClass>,
/// Whether the font is in italics. /// A sequence of classes. We need the font to be part of at least one of these
pub italic: bool, /// and preferably the leftmost possible.
/// Whether the font is bold. pub fallback: Vec<FontClass>,
pub bold: bool,
/// The font size. /// The font size.
pub font_size: f32, pub font_size: f32,
/// The line spacing (as a multiple of the font size). /// The line spacing (as a multiple of the font size).
@ -21,14 +20,35 @@ pub struct TextStyle {
pub paragraph_spacing: f32, pub paragraph_spacing: f32,
} }
impl TextStyle {
/// Toggle a class.
///
/// If the class was one of _italic_ or _bold_, then:
/// - If it was not present, the _regular_ class will be removed.
/// - If it was present, the _regular_ class will be added in case the
/// other style class is not present.
pub fn toggle_class(&mut self, class: FontClass) {
if self.classes.contains(&class) {
self.classes.retain(|x| x != &class);
if (class == FontClass::Italic && !self.classes.contains(&FontClass::Bold))
|| (class == FontClass::Bold && !self.classes.contains(&FontClass::Italic)) {
self.classes.push(FontClass::Regular);
}
} else {
if class == FontClass::Italic || class == FontClass::Bold {
self.classes.retain(|x| x != &FontClass::Regular);
}
self.classes.push(class);
}
}
}
impl Default for TextStyle { impl Default for TextStyle {
fn default() -> TextStyle { fn default() -> TextStyle {
use FontFamily::*; use FontClass::*;
TextStyle { TextStyle {
// Default font family, font size and line spacing. classes: vec![Regular],
font_families: vec![Serif, SansSerif, Monospace], fallback: vec![Serif],
italic: false,
bold: false,
font_size: 11.0, font_size: 11.0,
line_spacing: 1.2, line_spacing: 1.2,
paragraph_spacing: 1.5, paragraph_spacing: 1.5,