Unify font classes + By-value-contexts ⚖
This commit is contained in:
parent
c7ee2b393a
commit
099ce71aba
@ -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.
|
||||
|
176
src/font.rs
176
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<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())
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
26
src/func.rs
26
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<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) }
|
||||
}
|
||||
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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))?;
|
||||
|
||||
|
47
src/lib.rs
47
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<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.
|
||||
|
@ -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.
|
||||
|
44
src/style.rs
44
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<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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user