diff --git a/src/bin/main.rs b/src/bin/main.rs index 6ca1ba56e..b2f3d4050 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -48,16 +48,22 @@ fn run() -> Result<(), Box> { let mut src = String::new(); file.read_to_string(&mut src).map_err(|_| "failed to read from source file")?; - // Create a typesetter with a font provider that provides three fonts - // (two sans-serif fonts and a fallback for the emoji). + // 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![ - ("NotoSans-Regular.ttf", font_info!(["NotoSans", "Noto", SansSerif])), - ("NotoSans-Italic.ttf", font_info!(["NotoSans", "Noto", SansSerif], italic)), - ("NotoSans-Bold.ttf", font_info!(["NotoSans", "Noto", SansSerif], bold)), - ("NotoSans-BoldItalic.ttf", font_info!(["NotoSans", "Noto", SansSerif], italic, bold)), - ("NotoSansMath-Regular.ttf", font_info!(["NotoSansMath", "Noto", SansSerif])), - ("NotoEmoji-Regular.ttf", font_info!(["NotoEmoji", "Noto", SansSerif, Serif, Monospace])), + ("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])), ])); // Typeset the source code. diff --git a/src/func.rs b/src/func.rs index 402bf6fa3..0d2a62749 100644 --- a/src/func.rs +++ b/src/func.rs @@ -4,9 +4,10 @@ use std::any::Any; use std::collections::HashMap; use std::fmt::{self, Debug, Formatter}; -use crate::layout::{Layout, LayoutContext, LayoutResult}; -use crate::parsing::{ParseContext, ParseResult}; -use crate::syntax::FuncHeader; +use crate::layout::{layout, Layout, LayoutContext, LayoutResult}; +use crate::layout::flex::FlexLayout; +use crate::parsing::{parse, ParseContext, ParseError, ParseResult}; +use crate::syntax::{SyntaxTree, FuncHeader}; /// Typesetting function types. @@ -25,8 +26,7 @@ pub trait Function: FunctionBounds { /// /// Returns optionally the resulting layout and a new context if changes to the context should /// be made. - fn layout(&self, ctx: &LayoutContext) - -> LayoutResult<(Option, Option)>; + fn layout(&self, ctx: &LayoutContext) -> LayoutResult>; } impl PartialEq for dyn Function { @@ -76,9 +76,14 @@ impl Scope { Scope { parsers: HashMap::new() } } - /// Create a new scope with the standard functions contained. + /// Create a new scope with the standard functions contained: + /// - `italic` + /// - `bold` pub fn with_std() -> Scope { - Scope::new() + let mut std = Scope::new(); + std.add::("bold"); + std.add::("italic"); + std } /// Add a function type to the scope giving it a name. @@ -103,3 +108,52 @@ impl Debug for Scope { write!(f, "{:?}", self.parsers.keys()) } } + +/// 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) => { + $(#[$outer])* + #[derive(Debug, PartialEq)] + pub struct $struct { body: SyntaxTree } + impl Function for $struct { + 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() { + if let Some(body) = body { + Ok($struct { body: parse(body, ctx)? }) + } else { + Err(ParseError::new(format!("expected body for function `{}`", $name))) + } + } else { + Err(ParseError::new(format!("unexpected arguments to function `{}`", $name))) + } + } + + fn layout(&self, ctx: &LayoutContext) -> LayoutResult> { + // Change the context. + let mut $new_ctx = ctx.clone(); + $ctx_change + + // Create a box and put it into a flex layout. + let boxed = layout(&self.body, &$new_ctx)?; + let flex = FlexLayout::from_box(boxed); + + Ok(Some(Layout::Flex(flex))) + } + } + }; +} + +style_func! { + /// Typesets text in bold. + pub struct BoldFunc { "bold" }, + ctx => { ctx.style.bold = !ctx.style.bold } +} + +style_func! { + /// Typesets text in italics. + pub struct ItalicFunc { "italic" }, + ctx => { ctx.style.italic = !ctx.style.italic } +} diff --git a/src/layout/boxed.rs b/src/layout/boxed.rs index 89c641bcf..e712f650c 100644 --- a/src/layout/boxed.rs +++ b/src/layout/boxed.rs @@ -86,6 +86,15 @@ impl BoxLayouter { Ok(()) } + /// Add a sublayout at an absolute position. + pub fn add_box_absolute(&mut self, position: Size2D, layout: BoxLayout) { + // Move all actions into this layout and translate absolute positions. + self.actions.reset_origin(); + self.actions.add(TextAction::MoveAbsolute(position)); + self.actions.set_origin(position); + self.actions.extend(layout.actions); + } + /// Add some space in between two boxes. pub fn add_space(&mut self, space: Size) -> LayoutResult<()> { // Check whether this space fits. @@ -100,20 +109,6 @@ impl BoxLayouter { Ok(()) } - /// Add a sublayout at an absolute position. - pub fn add_box_absolute(&mut self, position: Size2D, layout: BoxLayout) { - // Move all actions into this layout and translate absolute positions. - self.actions.reset_origin(); - self.actions.add(TextAction::MoveAbsolute(position)); - self.actions.set_origin(position); - self.actions.extend(layout.actions); - } - - /// Whether this layouter contains any items. - pub fn is_empty(&self) -> bool { - self.actions.is_empty() - } - /// The remaining space for new boxes. pub fn remaining(&self) -> Size2D { Size2D { @@ -122,6 +117,11 @@ impl BoxLayouter { } } + /// Whether this layouter contains any items. + pub fn is_empty(&self) -> bool { + self.actions.is_empty() + } + /// Finish the layouting and create a box layout from this. pub fn finish(self) -> BoxLayout { BoxLayout { diff --git a/src/layout/flex.rs b/src/layout/flex.rs index 5fd7b1579..f966eae27 100644 --- a/src/layout/flex.rs +++ b/src/layout/flex.rs @@ -1,7 +1,7 @@ //! Flexible and lazy layouting of boxes. use crate::doc::TextAction; -use crate::size::Size2D; +use crate::size::{Size, Size2D}; use super::{BoxLayout, ActionList, LayoutSpace, LayoutResult, LayoutError}; @@ -10,8 +10,6 @@ use super::{BoxLayout, ActionList, LayoutSpace, LayoutResult, LayoutError}; pub struct FlexLayout { /// The sublayouts composing this layout. pub units: Vec, - /// The layout space to arrange in. - pub ctx: FlexContext, } /// A unit in a flex layout. @@ -25,14 +23,20 @@ pub enum FlexUnit { } impl FlexLayout { - /// Create a new flex layouter. - pub fn new(ctx: FlexContext) -> FlexLayout { + /// Create a new flex layout. + pub fn new() -> FlexLayout { FlexLayout { - ctx, units: vec![], } } + /// Create a new flex layout containing just one box. + pub fn from_box(boxed: BoxLayout) -> FlexLayout { + FlexLayout { + units: vec![FlexUnit::Boxed(boxed)], + } + } + /// Add a sublayout. pub fn add_box(&mut self, layout: BoxLayout) { self.units.push(FlexUnit::Boxed(layout)); @@ -54,8 +58,8 @@ impl FlexLayout { } /// Compute the justified layout. - pub fn into_box(self) -> LayoutResult { - FlexFinisher::new(self).finish() + pub fn finish(self, ctx: FlexContext) -> LayoutResult { + FlexFinisher::new(self, ctx).finish() } } @@ -64,8 +68,8 @@ impl FlexLayout { pub struct FlexContext { /// The space to layout the boxes in. pub space: LayoutSpace, - /// The flex spacing (like line spacing). - pub flex_spacing: f32, + /// The flex spacing between two lines of boxes. + pub flex_spacing: Size, } /// Finishes a flex layout by justifying the positions of the individual boxes. @@ -82,11 +86,11 @@ struct FlexFinisher { impl FlexFinisher { /// Create the finisher from the layout. - fn new(layout: FlexLayout) -> FlexFinisher { - let space = layout.ctx.space; + fn new(layout: FlexLayout, ctx: FlexContext) -> FlexFinisher { + let space = ctx.space; FlexFinisher { units: layout.units, - ctx: layout.ctx, + ctx, actions: ActionList::new(), dimensions: Size2D::zero(), usable: space.usable(), @@ -165,11 +169,13 @@ impl FlexFinisher { /// Move to the next line. fn newline(&mut self) { - self.line.y *= self.ctx.flex_spacing; self.dimensions.x = crate::size::max(self.dimensions.x, self.line.x); + if self.dimensions.y > Size::zero() { + self.dimensions.y += self.ctx.flex_spacing; + } self.dimensions.y += self.line.y; self.cursor.x = self.ctx.space.padding.left; - self.cursor.y += self.line.y; + self.cursor.y += self.line.y + self.ctx.flex_spacing; self.line = Size2D::zero(); } } diff --git a/src/layout/mod.rs b/src/layout/mod.rs index f8819d0be..40948a13e 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -70,6 +70,7 @@ struct Layouter<'a, 'p> { flex_layout: FlexLayout, flex_ctx: FlexContext, text_ctx: TextContext<'a, 'p>, + func_ctx: LayoutContext<'a, 'p>, } impl<'a, 'p> Layouter<'a, 'p> { @@ -85,7 +86,7 @@ impl<'a, 'p> Layouter<'a, 'p> { padding: SizeBox::zero(), shrink_to_fit: true, }, - flex_spacing: ctx.style.line_spacing, + flex_spacing: (ctx.style.line_spacing - 1.0) * Size::points(ctx.style.font_size), }; // The mutable context for layouting single pieces of text. @@ -94,12 +95,24 @@ impl<'a, 'p> Layouter<'a, 'p> { style: ctx.style.clone(), }; + // The mutable context for layouting single functions. + let func_ctx = LayoutContext { + loader: &ctx.loader, + style: ctx.style.clone(), + space: LayoutSpace { + dimensions: ctx.space.usable(), + padding: SizeBox::zero(), + shrink_to_fit: true, + }, + }; + Layouter { tree, box_layouter: BoxLayouter::new(box_ctx), - flex_layout: FlexLayout::new(flex_ctx), + flex_layout: FlexLayout::new(), flex_ctx, text_ctx, + func_ctx, } } @@ -124,28 +137,53 @@ impl<'a, 'p> Layouter<'a, 'p> { // Then start a new flex layouting process. Node::Newline => { // Finish the current paragraph into a box and add it. - self.add_paragraph_spacing()?; - let boxed = self.flex_layout.into_box()?; + let boxed = self.flex_layout.finish(self.flex_ctx)?; self.box_layouter.add_box(boxed)?; // Create a fresh flex layout for the next paragraph. self.flex_ctx.space.dimensions = self.box_layouter.remaining(); - self.flex_layout = FlexLayout::new(self.flex_ctx); + self.flex_layout = FlexLayout::new(); + self.add_paragraph_spacing()?; }, // Toggle the text styles. - Node::ToggleItalics => self.text_ctx.style.italic = !self.text_ctx.style.italic, - Node::ToggleBold => self.text_ctx.style.bold = !self.text_ctx.style.bold, + 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; + }, // Execute a function. - Node::Func(_) => unimplemented!(), + 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), + } + } + }, } } // If there are remainings, add them to the layout. if !self.flex_layout.is_empty() { - self.add_paragraph_spacing()?; - let boxed = self.flex_layout.into_box()?; + let boxed = self.flex_layout.finish(self.flex_ctx)?; self.box_layouter.add_box(boxed)?; } diff --git a/src/lib.rs b/src/lib.rs index 334f20b96..4799188b0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -216,7 +216,7 @@ mod test { #[test] fn features() { test("features", r" - **FEATURES TEST PAGE** + **Features Test Page** __Multiline:__ Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy @@ -226,7 +226,10 @@ mod test { __Emoji:__ Hello World! 🌍 - __Styles:__ This is **bold** and that is __italic__! + __Styles:__ This is made **bold** and that __italic__ using the built-in syntax! + + __Styles with functions:__ This is in [bold][boldface] and that is in [italic][italics] + using library functions! "); } diff --git a/src/parsing.rs b/src/parsing.rs index 727a06d8b..81a729ab6 100644 --- a/src/parsing.rs +++ b/src/parsing.rs @@ -822,8 +822,7 @@ mod parse_tests { } } - fn layout(&self, _: &LayoutContext) - -> LayoutResult<(Option, Option)> { Ok((None, None)) } + fn layout(&self, _: &LayoutContext) -> LayoutResult> { Ok(None) } } /// A testing function without a body. @@ -840,8 +839,7 @@ mod parse_tests { } } - fn layout(&self, _: &LayoutContext) - -> LayoutResult<(Option, Option)> { Ok((None, None)) } + fn layout(&self, _: &LayoutContext) -> LayoutResult> { Ok(None) } } } diff --git a/src/style.rs b/src/style.rs index 97f9552c7..cd0a9579e 100644 --- a/src/style.rs +++ b/src/style.rs @@ -30,7 +30,7 @@ impl Default for TextStyle { italic: false, bold: false, font_size: 11.0, - line_spacing: 1.25, + line_spacing: 1.2, paragraph_spacing: 1.5, } }