diff --git a/src/bin/main.rs b/src/bin/main.rs index b2f3d4050..edf639f01 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -6,7 +6,7 @@ use std::path::{Path, PathBuf}; use std::process; use typeset::Typesetter; -use typeset::{font::FileSystemFontProvider, font_info}; +use typeset::{font::FileSystemFontProvider, font}; use typeset::export::pdf::PdfExporter; @@ -50,20 +50,20 @@ fn run() -> Result<(), Box> { // Create a typesetter with a font provider that provides the default fonts. let mut typesetter = Typesetter::new(); - typesetter.add_font_provider(FileSystemFontProvider::new("fonts", vec![ - ("CMU-SansSerif-Regular.ttf", font_info!(["Computer Modern", SansSerif])), - ("CMU-SansSerif-Italic.ttf", font_info!(["Computer Modern", SansSerif], italic)), - ("CMU-SansSerif-Bold.ttf", font_info!(["Computer Modern", SansSerif], bold)), - ("CMU-SansSerif-Bold-Italic.ttf", font_info!(["Computer Modern", SansSerif], bold, italic)), - ("CMU-Serif-Regular.ttf", font_info!(["Computer Modern", Serif])), - ("CMU-Serif-Italic.ttf", font_info!(["Computer Modern", Serif], italic)), - ("CMU-Serif-Bold.ttf", font_info!(["Computer Modern", Serif], bold)), - ("CMU-Serif-Bold-Italic.ttf", font_info!(["Computer Modern", Serif], bold, italic)), - ("CMU-Typewriter-Regular.ttf", font_info!(["Computer Modern", Monospace])), - ("CMU-Typewriter-Italic.ttf", font_info!(["Computer Modern", Monospace], italic)), - ("CMU-Typewriter-Bold.ttf", font_info!(["Computer Modern", Monospace], bold)), - ("CMU-Typewriter-Bold-Italic.ttf", font_info!(["Computer Modern", Monospace], bold, italic)), - ("NotoEmoji-Regular.ttf", font_info!(["NotoEmoji", "Noto", SansSerif, Serif, Monospace])), + typesetter.add_font_provider(FileSystemFontProvider::new("../fonts", vec![ + ("CMU-SansSerif-Regular.ttf", font!["Computer Modern", Regular, SansSerif]), + ("CMU-SansSerif-Italic.ttf", font!["Computer Modern", Italic, SansSerif]), + ("CMU-SansSerif-Bold.ttf", font!["Computer Modern", Bold, SansSerif]), + ("CMU-SansSerif-Bold-Italic.ttf", font!["Computer Modern", Bold, Italic, SansSerif]), + ("CMU-Serif-Regular.ttf", font!["Computer Modern", Regular, Serif]), + ("CMU-Serif-Italic.ttf", font!["Computer Modern", Italic, Serif]), + ("CMU-Serif-Bold.ttf", font!["Computer Modern", Bold, Serif]), + ("CMU-Serif-Bold-Italic.ttf", font!["Computer Modern", Bold, Italic, Serif]), + ("CMU-Typewriter-Regular.ttf", font!["Computer Modern", Regular, Monospace]), + ("CMU-Typewriter-Italic.ttf", font!["Computer Modern", Italic, Monospace]), + ("CMU-Typewriter-Bold.ttf", font!["Computer Modern", Bold, Monospace]), + ("CMU-Typewriter-Bold-Italic.ttf", font!["Computer Modern", Bold, Italic, Monospace]), + ("NotoEmoji-Regular.ttf", font!["Noto", Regular, SansSerif, Serif, Monospace]), ])); // Typeset the source code. diff --git a/src/font.rs b/src/font.rs index d11fe75c2..ce3c37842 100644 --- a/src/font.rs +++ b/src/font.rs @@ -154,94 +154,83 @@ pub struct FontMetrics { /// 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)] pub struct FontInfo { /// The font families this font is part of. - pub families: Vec, - /// Whether the font is italic. - pub italic: bool, - /// Whether the font bold. - pub bold: bool, + pub classes: Vec, } -/// A family of fonts. +impl FontInfo { + /// Create a new font info from an iterator of classes. + pub fn new(classes: I) -> FontInfo where I: IntoIterator { + FontInfo { classes: classes.into_iter().collect() } + } +} + +/// A class of fonts. #[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub enum FontFamily { +pub enum FontClass { Serif, SansSerif, Monospace, - /// A custom class like _Arial_ or _Times_. - Named(String), + Regular, + Bold, + Italic, + /// A custom family like _Arial_ or _Times_. + Family(String), } /// A macro to create [FontInfos](crate::font::FontInfo) easily. /// -/// Accepts first a bracketed, ordered list of font families. Allowed are string expressions as well -/// as the three base families `SansSerif`, `Serif` and `Monospace`. Then there may follow -/// (separated by commas) the keywords `italic` and/or `bold`. +/// Accepts an ordered list of font classes. Strings expressions are parsed +/// into custom `Family`-variants and others can be named directly. /// /// # Examples /// The font _Noto Sans_ in regular typeface. /// ``` -/// # use typeset::font_info; -/// font_info!(["NotoSans", "Noto", SansSerif]); +/// # use typeset::font; +/// font!["NotoSans", "Noto", Regular, SansSerif]; /// ``` /// /// The font _Noto Serif_ in italics and boldface. /// ``` -/// # use typeset::font_info; -/// font_info!(["NotoSerif", "Noto", Serif], italic, bold); +/// # use typeset::font; +/// font!["NotoSerif", "Noto", Bold, Italic, Serif]; /// ``` /// /// The font _Arial_ in italics. /// ``` -/// # use typeset::font_info; -/// font_info!(["Arial", SansSerif], italic); +/// # use typeset::font; +/// font!["Arial", Italic, SansSerif]; /// ``` /// /// The font _Noto Emoji_, which works with all base families. 🙂 /// ``` -/// # use typeset::font_info; -/// font_info!(["NotoEmoji", "Noto", SansSerif, Serif, Monospace]); +/// # use typeset::font; +/// font!["NotoEmoji", "Noto", Regular, SansSerif, Serif, Monospace]; /// ``` #[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 - ([$($tts:tt)*] $(,$style:tt)*) => {{ - let mut families = Vec::new(); - font_info!(@__fam families, $($tts)*); - - #[allow(unused)] let mut italic = false; - #[allow(unused)] let mut bold = false; - $( font_info!(@__sty (italic, bold) $style); )* - - $crate::font::FontInfo { families, italic, bold } + ($($tts:tt)*) => {{ + let mut classes = Vec::new(); + font!(@__cls classes, $($tts)*); + $crate::font::FontInfo { classes } }}; - - // 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 /// `../fonts`. /// ``` - /// # use typeset::{font::FileSystemFontProvider, font_info}; + /// # use typeset::{font::FileSystemFontProvider, font}; /// FileSystemFontProvider::new("../fonts", vec![ - /// ("NotoSans-Regular.ttf", font_info!(["NotoSans", SansSerif])), - /// ("NotoSans-Italic.ttf", font_info!(["NotoSans", SansSerif], italic)), + /// ("NotoSans-Regular.ttf", font!["NotoSans", Regular, SansSerif]), + /// ("NotoSans-Italic.ttf", font!["NotoSans", Italic, SansSerif]), /// ]); /// ``` #[inline] @@ -395,14 +384,17 @@ impl<'p> FontLoader<'p> { } drop(state); - // The outermost loop goes over the families because we want to serve the font that matches - // the first possible family. - for family in &query.families { - // For each family now go over all font infos from all font providers. + // The outermost loop goes over the fallbacks because we want to serve the font that matches + // the first possible class. + for class in &query.fallback { + // 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 info in infos.iter() { - // Proceed only if this font matches the query. - if Self::matches(&query, family, info) { + let matches = info.classes.contains(class) + && 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(); // 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 } }).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<'_> { @@ -510,13 +496,11 @@ impl Debug for FontLoader<'_> { pub struct FontQuery { /// Which character is needed. pub character: char, - /// Whether the font should be in italics. - pub italic: bool, - /// Whether the font should be in boldface. - pub bold: bool, - /// A fallback list of font families to accept. The font matching the first possible family in - /// this list satisfying all other constraints should be returned. - pub families: Vec, + /// Which classes the font has to be part of. + pub classes: Vec, + /// A sequence of classes. The font matching the leftmost class in this sequence + /// should be returned. + pub fallback: Vec, } //------------------------------------------------------------------------------------------------// @@ -626,7 +610,7 @@ impl<'a> Subsetter<'a> { mapping, widths, 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. #[test] - fn font_info_macro() { - use FontFamily::{SansSerif as S, Serif as F, Monospace as M}; - #[allow(non_snake_case)] - fn N(family: &str) -> FontFamily { FontFamily::Named(family.to_string()) } + fn font_macro() { + use FontClass::*; - assert_eq!(font_info!(["NotoSans", "Noto", SansSerif]), FontInfo { - families: vec![N("NotoSans"), N("Noto"), S], - italic: false, - bold: false, + assert_eq!(font!["NotoSans", "Noto", Regular, SansSerif], FontInfo { + classes: vec![ + Family("NotoSans".to_owned()), Family("Noto".to_owned()), + Regular, SansSerif + ] }); - assert_eq!(font_info!(["NotoSerif", Serif, "Noto"], italic), FontInfo { - families: vec![N("NotoSerif"), F, N("Noto")], - italic: true, - bold: false, - }); - - 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, + assert_eq!(font!["NotoSerif", Serif, Italic, "Noto"], FontInfo { + classes: vec![ + Family("NotoSerif".to_owned()), Serif, Italic, + Family("Noto".to_owned()) + ], }); } } diff --git a/src/func.rs b/src/func.rs index 0d2a62749..91f3cf74b 100644 --- a/src/func.rs +++ b/src/func.rs @@ -4,6 +4,7 @@ use std::any::Any; use std::collections::HashMap; use std::fmt::{self, Debug, Formatter}; +use crate::font::FontClass; use crate::layout::{layout, Layout, LayoutContext, LayoutResult}; use crate::layout::flex::FlexLayout; 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`. pub trait Function: FunctionBounds { /// 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 where Self: Sized; /// Layout this function given a context. /// /// Returns optionally the resulting layout and a new context if changes to the context should /// be made. - fn layout(&self, ctx: &LayoutContext) -> LayoutResult>; + fn layout(&self, ctx: LayoutContext) -> LayoutResult>; } impl PartialEq for dyn Function { @@ -67,7 +68,7 @@ pub struct Scope { } /// 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>; impl Scope { @@ -112,12 +113,12 @@ impl Debug for Scope { /// Creates style functions like bold and italic. macro_rules! style_func { ($(#[$outer:meta])* pub struct $struct:ident { $name:expr }, - $new_ctx:ident => $ctx_change:block) => { + $style:ident => $style_change:block) => { $(#[$outer])* #[derive(Debug, PartialEq)] pub struct $struct { body: SyntaxTree } impl Function for $struct { - fn parse(header: &FuncHeader, body: Option<&str>, ctx: &ParseContext) + fn parse(header: &FuncHeader, body: Option<&str>, ctx: ParseContext) -> ParseResult where Self: Sized { // Accept only invocations without arguments and with body. if header.args.is_empty() && header.kwargs.is_empty() { @@ -131,13 +132,16 @@ macro_rules! style_func { } } - fn layout(&self, ctx: &LayoutContext) -> LayoutResult> { + fn layout(&self, ctx: LayoutContext) -> LayoutResult> { // Change the context. - let mut $new_ctx = ctx.clone(); - $ctx_change + let mut $style = ctx.style.clone(); + $style_change // 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); Ok(Some(Layout::Flex(flex))) @@ -149,11 +153,11 @@ macro_rules! style_func { style_func! { /// Typesets text in bold. pub struct BoldFunc { "bold" }, - ctx => { ctx.style.bold = !ctx.style.bold } + style => { style.toggle_class(FontClass::Bold) } } style_func! { /// Typesets text in italics. pub struct ItalicFunc { "italic" }, - ctx => { ctx.style.italic = !ctx.style.italic } + style => { style.toggle_class(FontClass::Italic) } } diff --git a/src/layout/mod.rs b/src/layout/mod.rs index b678ab2bb..3ac23005f 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -1,9 +1,12 @@ //! The layouting engine. +use std::borrow::Cow; +use std::mem; + use crate::doc::LayoutAction; -use crate::font::{FontLoader, FontError}; +use crate::font::{FontLoader, FontClass, FontError}; use crate::size::{Size, Size2D, SizeBox}; -use crate::syntax::{SyntaxTree, Node}; +use crate::syntax::{SyntaxTree, Node, FuncCall}; use crate::style::TextStyle; use self::flex::{FlexLayout, FlexContext}; @@ -24,13 +27,18 @@ pub enum Layout { Flex(FlexLayout), } +/// Layout a syntax tree in a given context. +pub fn layout(tree: &SyntaxTree, ctx: LayoutContext) -> LayoutResult { + Layouter::new(tree, ctx).layout() +} + /// The context for layouting. -#[derive(Debug, Clone)] +#[derive(Debug, Copy, Clone)] pub struct LayoutContext<'a, 'p> { /// Loads fonts matching queries. pub loader: &'a FontLoader<'p>, /// Base style to set text with. - pub style: TextStyle, + pub style: &'a TextStyle, /// The space to layout in. 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 { - Layouter::new(tree, ctx).layout() -} - /// Transforms a syntax tree into a box layout. #[derive(Debug)] struct Layouter<'a, 'p> { tree: &'a SyntaxTree, box_layouter: BoxLayouter, flex_layout: FlexLayout, - flex_ctx: FlexContext, - text_ctx: TextContext<'a, 'p>, - func_ctx: LayoutContext<'a, 'p>, + loader: &'a FontLoader<'p>, + style: Cow<'a, TextStyle>, } impl<'a, 'p> Layouter<'a, 'p> { /// Create a new layouter. - 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, - }, - }; - + fn new(tree: &'a SyntaxTree, ctx: LayoutContext<'a, 'p>) -> Layouter<'a, 'p> { Layouter { tree, - box_layouter: BoxLayouter::new(box_ctx), + box_layouter: BoxLayouter::new(BoxContext { space: ctx.space }), flex_layout: FlexLayout::new(), - flex_ctx, - text_ctx, - func_ctx, + loader: ctx.loader, + style: Cow::Borrowed(ctx.style) } } @@ -122,79 +93,101 @@ impl<'a, 'p> Layouter<'a, 'p> { for node in &self.tree.nodes { match node { // Layout a single piece of text. - Node::Text(text) => { - let boxed = self::text::layout(text, &self.text_ctx)?; - self.flex_layout.add_box(boxed); - }, + Node::Text(text) => self.layout_text(text, false)?, + + // Add a space. Node::Space => { if !self.flex_layout.is_empty() { - let boxed = self::text::layout(" ", &self.text_ctx)?; - self.flex_layout.add_glue(boxed); + self.layout_text(" ", true)?; } }, // Finish the current flex layout and add it to the box layouter. - // Then start a new flex layouting process. Node::Newline => { // Finish the current paragraph into a box and add it. - let boxed = self.flex_layout.finish(self.flex_ctx)?; - self.box_layouter.add_box(boxed)?; + self.layout_flex()?; - // Create a fresh flex layout for the next paragraph. - self.flex_ctx.space.dimensions = self.box_layouter.remaining(); - self.flex_layout = FlexLayout::new(); - self.add_paragraph_spacing()?; + // Add some paragraph spacing. + let size = Size::pt(self.style.font_size) + * (self.style.line_spacing * self.style.paragraph_spacing - 1.0); + self.box_layouter.add_space(size)?; }, // Toggle the text styles. - Node::ToggleItalics => { - self.text_ctx.style.italic = !self.text_ctx.style.italic; - 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; - }, + Node::ToggleItalics => self.style.to_mut().toggle_class(FontClass::Italic), + Node::ToggleBold => self.style.to_mut().toggle_class(FontClass::Bold), // Execute a function. - Node::Func(func) => { - self.func_ctx.space.dimensions = self.box_layouter.remaining(); - let layout = func.body.layout(&self.func_ctx)?; - - // Add the potential layout. - if let Some(layout) = layout { - match layout { - Layout::Boxed(boxed) => { - // Finish the previous flex run before adding the box. - let previous = self.flex_layout.finish(self.flex_ctx)?; - self.box_layouter.add_box(previous)?; - 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), - } - } - }, + Node::Func(func) => self.layout_func(func)?, } } // If there are remainings, add them to the layout. if !self.flex_layout.is_empty() { - let boxed = self.flex_layout.finish(self.flex_ctx)?; - self.box_layouter.add_box(boxed)?; + self.layout_flex()?; } 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) + /// 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. + if let Some(layout) = layout { + match layout { + Layout::Boxed(boxed) => { + // Finish the previous flex run before adding the box. + self.layout_flex()?; + self.box_layouter.add_box(boxed)?; + }, + Layout::Flex(flex) => self.flex_layout.add_flexible(flex), + } + } + + Ok(()) } } diff --git a/src/layout/text.rs b/src/layout/text.rs index 0a0241a06..75a592542 100644 --- a/src/layout/text.rs +++ b/src/layout/text.rs @@ -7,16 +7,16 @@ use super::*; /// The context for text layouting. -#[derive(Debug, Clone)] +#[derive(Debug, Copy, Clone)] pub struct TextContext<'a, 'p> { /// Loads fonts matching queries. pub loader: &'a FontLoader<'p>, /// 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. -pub fn layout(text: &str, ctx: &TextContext) -> LayoutResult { +pub fn layout(text: &str, ctx: TextContext) -> LayoutResult { let mut actions = Vec::new(); let mut active_font = std::usize::MAX; let mut buffer = String::new(); @@ -26,9 +26,8 @@ pub fn layout(text: &str, ctx: &TextContext) -> LayoutResult { for character in text.chars() { // Retrieve the best font for this character. let (index, font) = ctx.loader.get(FontQuery { - families: ctx.style.font_families.clone(), - italic: ctx.style.italic, - bold: ctx.style.bold, + classes: ctx.style.classes.clone(), + fallback: ctx.style.fallback.clone(), character, }).ok_or_else(|| LayoutError::NoSuitableFont(character))?; diff --git a/src/lib.rs b/src/lib.rs index 4799188b0..1c5cf3256 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,7 +16,7 @@ //! ``` //! use std::fs::File; //! use typeset::Typesetter; -//! use typeset::{font::FileSystemFontProvider, font_info}; +//! use typeset::{font::FileSystemFontProvider, font}; //! use typeset::export::pdf::PdfExporter; //! //! // Simple example source code. @@ -26,9 +26,9 @@ //! // (two sans-serif fonts and a fallback for the emoji). //! let mut typesetter = Typesetter::new(); //! typesetter.add_font_provider(FileSystemFontProvider::new("../fonts", vec![ -//! ("CMU-Serif-Regular.ttf", font_info!(["Computer Modern", Serif])), -//! ("CMU-Serif-Italic.ttf", font_info!(["Computer Modern", Serif], italic)), -//! ("NotoEmoji-Regular.ttf", font_info!(["NotoEmoji", "Noto", SansSerif, Serif, Monospace])), +//! ("CMU-Serif-Regular.ttf", font!["Computer Modern", Regular, Serif]), +//! ("CMU-Serif-Italic.ttf", font!["Computer Modern", Italic, Serif]), +//! ("NotoEmoji-Regular.ttf", font!["Noto", Regular, Serif, SansSerif, Monospace]), //! ])); //! // Typeset the source code into a document. //! let document = typesetter.typeset(src).unwrap(); @@ -112,24 +112,21 @@ impl<'p> Typesetter<'p> { #[inline] pub fn parse(&self, src: &str) -> ParseResult { let scope = Scope::with_std(); - let ctx = ParseContext { scope: &scope }; - parse(src, &ctx) + parse(src, ParseContext { scope: &scope }) } /// Layout a syntax tree and return the layout and the referenced font list. pub fn layout(&self, tree: &SyntaxTree) -> LayoutResult<(BoxLayout, Vec)> { let loader = FontLoader::new(&self.font_providers); - let ctx = LayoutContext { + let pages = layout(&tree, LayoutContext { loader: &loader, - style: self.text_style.clone(), + style: &self.text_style, space: LayoutSpace { dimensions: self.page_style.dimensions, padding: self.page_style.margins, shrink_to_fit: false, }, - }; - - let pages = layout(&tree, &ctx)?; + })?; Ok((pages, loader.into_fonts())) } @@ -182,25 +179,25 @@ mod test { use std::io::BufWriter; use crate::Typesetter; use crate::export::pdf::PdfExporter; - use crate::font::FileSystemFontProvider; + use crate::font::{FileSystemFontProvider}; /// Create a _PDF_ with a name from the source code. fn test(name: &str, src: &str) { let mut typesetter = Typesetter::new(); typesetter.add_font_provider(FileSystemFontProvider::new("../fonts", vec![ - ("CMU-SansSerif-Regular.ttf", font_info!(["Computer Modern", SansSerif])), - ("CMU-SansSerif-Italic.ttf", font_info!(["Computer Modern", SansSerif], italic)), - ("CMU-SansSerif-Bold.ttf", font_info!(["Computer Modern", SansSerif], bold)), - ("CMU-SansSerif-Bold-Italic.ttf", font_info!(["Computer Modern", SansSerif], bold, italic)), - ("CMU-Serif-Regular.ttf", font_info!(["Computer Modern", Serif])), - ("CMU-Serif-Italic.ttf", font_info!(["Computer Modern", Serif], italic)), - ("CMU-Serif-Bold.ttf", font_info!(["Computer Modern", Serif], bold)), - ("CMU-Serif-Bold-Italic.ttf", font_info!(["Computer Modern", Serif], bold, italic)), - ("CMU-Typewriter-Regular.ttf", font_info!(["Computer Modern", Monospace])), - ("CMU-Typewriter-Italic.ttf", font_info!(["Computer Modern", Monospace], italic)), - ("CMU-Typewriter-Bold.ttf", font_info!(["Computer Modern", Monospace], bold)), - ("CMU-Typewriter-Bold-Italic.ttf", font_info!(["Computer Modern", Monospace], bold, italic)), - ("NotoEmoji-Regular.ttf", font_info!(["NotoEmoji", "Noto", SansSerif, Serif, Monospace])), + ("CMU-SansSerif-Regular.ttf", font!["Computer Modern", Regular, SansSerif]), + ("CMU-SansSerif-Italic.ttf", font!["Computer Modern", Italic, SansSerif]), + ("CMU-SansSerif-Bold.ttf", font!["Computer Modern", Bold, SansSerif]), + ("CMU-SansSerif-Bold-Italic.ttf", font!["Computer Modern", Bold, Italic, SansSerif]), + ("CMU-Serif-Regular.ttf", font!["Computer Modern", Regular, Serif]), + ("CMU-Serif-Italic.ttf", font!["Computer Modern", Italic, Serif]), + ("CMU-Serif-Bold.ttf", font!["Computer Modern", Bold, Serif]), + ("CMU-Serif-Bold-Italic.ttf", font!["Computer Modern", Bold, Italic, Serif]), + ("CMU-Typewriter-Regular.ttf", font!["Computer Modern", Regular, Monospace]), + ("CMU-Typewriter-Italic.ttf", font!["Computer Modern", Italic, Monospace]), + ("CMU-Typewriter-Bold.ttf", font!["Computer Modern", Bold, Monospace]), + ("CMU-Typewriter-Bold-Italic.ttf", font!["Computer Modern", Bold, Italic, Monospace]), + ("NotoEmoji-Regular.ttf", font!["Noto", Regular, SansSerif, Serif, Monospace]), ])); // Typeset into document. diff --git a/src/parsing.rs b/src/parsing.rs index 81a729ab6..79066e47f 100644 --- a/src/parsing.rs +++ b/src/parsing.rs @@ -326,12 +326,12 @@ impl Iterator for PeekableChars<'_> { /// Parses source code into a syntax tree given a context. #[inline] -pub fn parse(src: &str, ctx: &ParseContext) -> ParseResult { +pub fn parse(src: &str, ctx: ParseContext) -> ParseResult { Parser::new(src, ctx).parse() } /// The context for parsing. -#[derive(Debug)] +#[derive(Debug, Copy, Clone)] pub struct ParseContext<'a> { /// The scope containing function definitions. pub scope: &'a Scope, @@ -343,7 +343,7 @@ struct Parser<'s> { src: &'s str, tokens: PeekableTokens<'s>, state: ParserState, - ctx: &'s ParseContext<'s>, + ctx: ParseContext<'s>, tree: SyntaxTree, } @@ -360,12 +360,12 @@ enum ParserState { impl<'s> Parser<'s> { /// 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 { src, tokens: PeekableTokens::new(tokenize(src)), - ctx, state: ParserState::Body, + ctx, tree: SyntaxTree::new(), } } @@ -813,7 +813,7 @@ mod parse_tests { pub struct TreeFn(pub SyntaxTree); impl Function for TreeFn { - fn parse(_: &FuncHeader, body: Option<&str>, ctx: &ParseContext) + fn parse(_: &FuncHeader, body: Option<&str>, ctx: ParseContext) -> ParseResult where Self: Sized { if let Some(src) = body { parse(src, ctx).map(|tree| TreeFn(tree)) @@ -822,7 +822,7 @@ mod parse_tests { } } - fn layout(&self, _: &LayoutContext) -> LayoutResult> { Ok(None) } + fn layout(&self, _: LayoutContext) -> LayoutResult> { Ok(None) } } /// A testing function without a body. @@ -830,7 +830,7 @@ mod parse_tests { pub struct BodylessFn; impl Function for BodylessFn { - fn parse(_: &FuncHeader, body: Option<&str>, _: &ParseContext) + fn parse(_: &FuncHeader, body: Option<&str>, _: ParseContext) -> ParseResult where Self: Sized { if body.is_none() { Ok(BodylessFn) @@ -839,32 +839,32 @@ mod parse_tests { } } - fn layout(&self, _: &LayoutContext) -> LayoutResult> { Ok(None) } + fn layout(&self, _: LayoutContext) -> LayoutResult> { Ok(None) } } } /// Test if the source code parses into the syntax tree. fn test(src: &str, tree: SyntaxTree) { 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. fn test_scoped(scope: &Scope, src: &str, tree: SyntaxTree) { 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. fn test_err(src: &str, err: &str) { 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. fn test_err_scoped(scope: &Scope, src: &str, err: &str) { 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. diff --git a/src/style.rs b/src/style.rs index cd0a9579e..042042bff 100644 --- a/src/style.rs +++ b/src/style.rs @@ -1,18 +1,17 @@ //! Styles for layouting. -use crate::font::FontFamily; +use crate::font::FontClass; use crate::size::{Size, Size2D, SizeBox}; /// Default styles for text. #[derive(Debug, Clone)] pub struct TextStyle { - /// A fallback list of font families to use. - pub font_families: Vec, - /// Whether the font is in italics. - pub italic: bool, - /// Whether the font is bold. - pub bold: bool, + /// The classes the font we want has to be part of. + pub classes: Vec, + /// A sequence of classes. We need the font to be part of at least one of these + /// and preferably the leftmost possible. + pub fallback: Vec, /// The font size. pub font_size: f32, /// The line spacing (as a multiple of the font size). @@ -21,14 +20,35 @@ pub struct TextStyle { 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 { fn default() -> TextStyle { - use FontFamily::*; + use FontClass::*; TextStyle { - // Default font family, font size and line spacing. - font_families: vec![Serif, SansSerif, Monospace], - italic: false, - bold: false, + classes: vec![Regular], + fallback: vec![Serif], font_size: 11.0, line_spacing: 1.2, paragraph_spacing: 1.5,