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 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<Error>> {
// 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.

View File

@ -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<FontFamily>,
/// Whether the font is italic.
pub italic: bool,
/// Whether the font bold.
pub bold: bool,
pub classes: Vec<FontClass>,
}
/// 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)]
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<FontFamily>,
/// Which classes the font has to be part of.
pub classes: Vec<FontClass>,
/// A sequence of classes. The font matching the leftmost class in this sequence
/// should be returned.
pub fallback: Vec<FontClass>,
}
//------------------------------------------------------------------------------------------------//
@ -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())
],
});
}
}

View File

@ -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<Self> 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<Option<Layout>>;
fn layout(&self, ctx: LayoutContext) -> LayoutResult<Option<Layout>>;
}
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<Box<dyn Function>>;
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<Self> 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<Option<Layout>> {
fn layout(&self, ctx: LayoutContext) -> LayoutResult<Option<Layout>> {
// 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) }
}

View File

@ -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<BoxLayout> {
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<BoxLayout> {
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(())
}
}

View File

@ -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<BoxLayout> {
pub fn layout(text: &str, ctx: TextContext) -> LayoutResult<BoxLayout> {
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<BoxLayout> {
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))?;

View File

@ -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<SyntaxTree> {
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<Font>)> {
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.

View File

@ -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<SyntaxTree> {
pub fn parse(src: &str, ctx: ParseContext) -> ParseResult<SyntaxTree> {
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<Self> 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<Option<Layout>> { Ok(None) }
fn layout(&self, _: LayoutContext) -> LayoutResult<Option<Layout>> { 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<Self> where Self: Sized {
if body.is_none() {
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.
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.

View File

@ -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<FontFamily>,
/// 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<FontClass>,
/// 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<FontClass>,
/// 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,