diff --git a/src/export/pdf.rs b/src/export/pdf.rs index 6a12c42c7..3c718c2ea 100644 --- a/src/export/pdf.rs +++ b/src/export/pdf.rs @@ -19,7 +19,6 @@ use crate::layout::{Layout, LayoutAction, MultiLayout}; use crate::size::Size; /// Exports layouts into _PDFs_. -#[derive(Debug)] pub struct PdfExporter {} impl PdfExporter { diff --git a/src/layout/flex.rs b/src/layout/flex.rs index d6739f1bd..a4b3ed6d9 100644 --- a/src/layout/flex.rs +++ b/src/layout/flex.rs @@ -1,6 +1,6 @@ use super::*; -/// Flex-layouting of boxes. +/// Layouts boxes flex-like. /// /// The boxes are arranged in "lines", each line having the height of its /// biggest box. When a box does not fit on a line anymore horizontally, @@ -74,9 +74,9 @@ impl FlexLayouter { } } - /// Get a reference to this layouter's context. - pub fn ctx(&self) -> &FlexContext { - &self.ctx + /// This layouter's context. + pub fn ctx(&self) -> FlexContext { + self.ctx } /// Add a sublayout. diff --git a/src/layout/mod.rs b/src/layout/mod.rs index b760ca1e9..031226b98 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -2,7 +2,6 @@ use std::borrow::Cow; use std::io::{self, Write}; -use std::mem; use toddle::query::{FontClass, SharedFontLoader}; use toddle::Error as FontError; @@ -13,16 +12,23 @@ use crate::style::TextStyle; use crate::syntax::{FuncCall, Node, SyntaxTree}; mod actions; +mod tree; mod flex; mod stacked; mod text; -pub use actions::{LayoutAction, LayoutActionList}; -pub use flex::{FlexContext, FlexLayouter}; -pub use stacked::{StackContext, StackLayouter}; -pub use text::{layout_text, TextContext}; +/// Different kinds of layouters (fully re-exported). +pub mod layouters { + pub use super::tree::layout_tree; + pub use super::flex::{FlexLayouter, FlexContext}; + pub use super::stacked::{StackLayouter, StackContext}; + pub use super::text::{layout_text, TextContext}; +} -/// A box layout has a fixed width and height and composes of actions. +pub use actions::{LayoutAction, LayoutActionList}; +pub use layouters::*; + +/// A sequence of layouting actions inside a box. #[derive(Debug, Clone)] pub struct Layout { /// The size of the box. @@ -50,19 +56,19 @@ impl Layout { } } -/// A collection of box layouts. +/// A collection of layouts. #[derive(Debug, Clone)] pub struct MultiLayout { pub layouts: Vec, } impl MultiLayout { - /// Create an empty multibox layout. + /// Create an empty multi-layout. pub fn new() -> MultiLayout { MultiLayout { layouts: vec![] } } - /// Extract a single sublayout and panic if this layout does not have + /// Extract the single sublayout. This panics if the layout does not have /// exactly one child. pub fn into_single(mut self) -> Layout { if self.layouts.len() != 1 { @@ -105,8 +111,7 @@ impl<'a> IntoIterator for &'a MultiLayout { } } - -/// The context for layouting. +/// The general context for layouting. #[derive(Copy, Clone)] pub struct LayoutContext<'a, 'p> { pub loader: &'a SharedFontLoader<'p>, @@ -115,7 +120,7 @@ pub struct LayoutContext<'a, 'p> { pub extra_space: Option, } -/// Spacial constraints for layouting. +/// Spacial layouting constraints. #[derive(Debug, Copy, Clone)] pub struct LayoutSpace { /// The maximum size of the box to layout in. @@ -125,12 +130,12 @@ pub struct LayoutSpace { /// The alignment to use for the content. pub alignment: Alignment, /// Whether to shrink the dimensions to fit the content or the keep the - /// original ones. + /// dimensions from the layout space. pub shrink_to_fit: bool, } impl LayoutSpace { - /// The actually usable area. + /// The actually usable area (dimensions minus padding). pub fn usable(&self) -> Size2D { Size2D { x: self.dimensions.x - self.padding.left - self.padding.right, @@ -146,157 +151,6 @@ pub enum Alignment { Right, } -pub fn layout_tree(tree: &SyntaxTree, ctx: LayoutContext) -> LayoutResult { - let mut layouter = Layouter::new(ctx); - layouter.layout(tree)?; - layouter.finish() -} - -/// Transforms a syntax tree into a box layout. -struct Layouter<'a, 'p> { - ctx: LayoutContext<'a, 'p>, - stack_layouter: StackLayouter, - flex_layouter: FlexLayouter, - style: Cow<'a, TextStyle>, -} - -impl<'a, 'p> Layouter<'a, 'p> { - /// Create a new layouter. - fn new(ctx: LayoutContext<'a, 'p>) -> Layouter<'a, 'p> { - Layouter { - ctx, - stack_layouter: StackLayouter::new(StackContext { space: ctx.space }), - flex_layouter: FlexLayouter::new(FlexContext { - space: LayoutSpace { - dimensions: ctx.space.usable(), - padding: SizeBox::zero(), - alignment: ctx.space.alignment, - shrink_to_fit: true, - }, - flex_spacing: (ctx.style.line_spacing - 1.0) * Size::pt(ctx.style.font_size), - }), - style: Cow::Borrowed(ctx.style), - } - } - - /// Layout the tree into a box. - fn layout(&mut self, tree: &SyntaxTree) -> LayoutResult<()> { - // Walk all nodes and layout them. - for node in &tree.nodes { - match node { - // Layout a single piece of text. - Node::Text(text) => self.layout_text(text, false)?, - - // Add a space. - Node::Space => { - if !self.flex_layouter.is_empty() { - self.layout_text(" ", true)?; - } - } - - // Finish the current flex layout and add it to the box layouter. - Node::Newline => { - // Finish the current paragraph into a box and add it. - self.layout_flex()?; - - // Add some paragraph spacing. - let size = Size::pt(self.style.font_size) - * (self.style.line_spacing * self.style.paragraph_spacing - 1.0); - self.stack_layouter.add_space(size)?; - } - - // Toggle the text styles. - Node::ToggleItalics => self.style.to_mut().toggle_class(FontClass::Italic), - Node::ToggleBold => self.style.to_mut().toggle_class(FontClass::Bold), - Node::ToggleMonospace => self.style.to_mut().toggle_class(FontClass::Monospace), - - // Execute a function. - Node::Func(func) => self.layout_func(func)?, - } - } - - Ok(()) - } - - fn finish(mut self) -> LayoutResult { - // If there are remainings, add them to the layout. - if !self.flex_layouter.is_empty() { - self.layout_flex()?; - } - - Ok(MultiLayout { - layouts: vec![self.stack_layouter.finish()], - }) - } - - /// Layout a piece of text into a box. - fn layout_text(&mut self, text: &str, glue: bool) -> LayoutResult<()> { - let boxed = layout_text( - text, - TextContext { - loader: &self.ctx.loader, - style: &self.style, - }, - )?; - - if glue { - self.flex_layouter.add_glue(boxed); - } else { - self.flex_layouter.add(boxed); - } - - Ok(()) - } - - /// Finish the current flex run and return the resulting box. - fn layout_flex(&mut self) -> LayoutResult<()> { - if self.flex_layouter.is_empty() { - return Ok(()); - } - - let mut layout = FlexLayouter::new(FlexContext { - space: LayoutSpace { - dimensions: self.stack_layouter.ctx().space.usable(), - padding: SizeBox::zero(), - alignment: self.ctx.space.alignment, - shrink_to_fit: true, - }, - flex_spacing: (self.style.line_spacing - 1.0) * Size::pt(self.style.font_size), - }); - mem::swap(&mut layout, &mut self.flex_layouter); - - let boxed = layout.finish()?; - - self.stack_layouter.add(boxed) - } - - /// Layout a function. - fn layout_func(&mut self, func: &FuncCall) -> LayoutResult<()> { - let commands = func.body.layout(LayoutContext { - loader: &self.ctx.loader, - style: &self.style, - space: LayoutSpace { - dimensions: self.stack_layouter.remaining(), - padding: SizeBox::zero(), - alignment: self.ctx.space.alignment, - shrink_to_fit: true, - }, - extra_space: self.ctx.extra_space, - })?; - - for command in commands { - match command { - Command::Layout(tree) => self.layout(tree)?, - Command::Add(layout) => self.stack_layouter.add(layout)?, - Command::AddMany(layouts) => self.stack_layouter.add_many(layouts)?, - Command::ToggleStyleClass(class) => self.style.to_mut().toggle_class(class), - } - } - - Ok(()) - } -} - /// The error type for layouting. pub enum LayoutError { /// There is not enough space to add an item. diff --git a/src/layout/stacked.rs b/src/layout/stacked.rs index b710e2035..bfca4e3ef 100644 --- a/src/layout/stacked.rs +++ b/src/layout/stacked.rs @@ -1,6 +1,6 @@ use super::*; -/// Stack-like layouting of boxes. +/// Layouts boxes stack-like. /// /// The boxes are arranged vertically, each layout gettings it's own "line". pub struct StackLayouter { @@ -11,7 +11,7 @@ pub struct StackLayouter { cursor: Size2D, } -/// The context for the [`StackLayouter`]. +/// The context for stack layouting. #[derive(Debug, Copy, Clone)] pub struct StackContext { /// The space to layout the boxes in. @@ -46,9 +46,9 @@ impl StackLayouter { } } - /// Get a reference to this layouter's context. - pub fn ctx(&self) -> &StackContext { - &self.ctx + /// This layouter's context. + pub fn ctx(&self) -> StackContext { + self.ctx } /// Add a sublayout to the bottom. diff --git a/src/layout/tree.rs b/src/layout/tree.rs new file mode 100644 index 000000000..506168836 --- /dev/null +++ b/src/layout/tree.rs @@ -0,0 +1,148 @@ +use super::*; + +/// Layouts syntax trees into boxes. +pub fn layout_tree(tree: &SyntaxTree, ctx: LayoutContext) -> LayoutResult { + let mut layouter = TreeLayouter::new(ctx); + layouter.layout(tree)?; + layouter.finish() +} + +struct TreeLayouter<'a, 'p> { + ctx: LayoutContext<'a, 'p>, + stack: StackLayouter, + flex: FlexLayouter, + style: Cow<'a, TextStyle>, +} + +impl<'a, 'p> TreeLayouter<'a, 'p> { + /// Create a new layouter. + fn new(ctx: LayoutContext<'a, 'p>) -> TreeLayouter<'a, 'p> { + TreeLayouter { + ctx, + stack: StackLayouter::new(StackContext { space: ctx.space }), + flex: FlexLayouter::new(FlexContext { + space: LayoutSpace { + dimensions: ctx.space.usable(), + padding: SizeBox::zero(), + alignment: ctx.space.alignment, + shrink_to_fit: true, + }, + flex_spacing: flex_spacing(&ctx.style), + }), + style: Cow::Borrowed(ctx.style), + } + } + + /// Layout the tree into a box. + fn layout(&mut self, tree: &SyntaxTree) -> LayoutResult<()> { + for node in &tree.nodes { + match node { + Node::Text(text) => self.layout_text(text, false)?, + + Node::Space => { + // Only add a space if there was any content before. + if !self.flex.is_empty() { + self.layout_text(" ", true)?; + } + } + + // Finish the current flex layouting process. + Node::Newline => { + self.layout_flex()?; + + let space = paragraph_spacing(&self.style); + self.stack.add_space(space)?; + } + + // Toggle the text styles. + Node::ToggleItalics => self.style.to_mut().toggle_class(FontClass::Italic), + Node::ToggleBold => self.style.to_mut().toggle_class(FontClass::Bold), + Node::ToggleMonospace => self.style.to_mut().toggle_class(FontClass::Monospace), + + Node::Func(func) => self.layout_func(func)?, + } + } + + Ok(()) + } + + /// Finish the layout. + fn finish(mut self) -> LayoutResult { + // If there are remainings, add them to the layout. + if !self.flex.is_empty() { + self.layout_flex()?; + } + + Ok(MultiLayout { + layouts: vec![self.stack.finish()], + }) + } + + /// Add text to the flex layout. If `glue` is true, the text will be a glue + /// part in the flex layouter. For details, see [`FlexLayouter`]. + fn layout_text(&mut self, text: &str, glue: bool) -> LayoutResult<()> { + let ctx = TextContext { + loader: &self.ctx.loader, + style: &self.style, + }; + + let layout = layout_text(text, ctx)?; + + if glue { + self.flex.add_glue(layout); + } else { + self.flex.add(layout); + } + + Ok(()) + } + + /// Finish the current flex layout and add it the stack. + fn layout_flex(&mut self) -> LayoutResult<()> { + if self.flex.is_empty() { + return Ok(()); + } + + let mut ctx = self.flex.ctx(); + ctx.space.dimensions = self.stack.remaining(); + ctx.flex_spacing = flex_spacing(&self.style); + + let next = FlexLayouter::new(ctx); + let flex = std::mem::replace(&mut self.flex, next); + let boxed = flex.finish()?; + + self.stack.add(boxed) + } + + /// Layout a function. + fn layout_func(&mut self, func: &FuncCall) -> LayoutResult<()> { + let mut ctx = self.ctx; + ctx.style = &self.style; + ctx.space.dimensions = self.stack.remaining(); + ctx.space.padding = SizeBox::zero(); + ctx.space.shrink_to_fit = true; + + let commands = func.body.layout(ctx)?; + + for command in commands { + match command { + Command::Layout(tree) => self.layout(tree)?, + Command::Add(layout) => self.stack.add(layout)?, + Command::AddMany(layouts) => self.stack.add_many(layouts)?, + Command::ToggleStyleClass(class) => self.style.to_mut().toggle_class(class), + } + } + + Ok(()) + } +} + +fn flex_spacing(style: &TextStyle) -> Size { + (style.line_spacing - 1.0) * Size::pt(style.font_size) +} + +fn paragraph_spacing(style: &TextStyle) -> Size { + let line_height = Size::pt(style.font_size); + let space_factor = style.line_spacing * style.paragraph_spacing - 1.0; + line_height * space_factor +} diff --git a/src/parsing/mod.rs b/src/parsing/mod.rs index 0f5ca0f8a..6214bfdb9 100644 --- a/src/parsing/mod.rs +++ b/src/parsing/mod.rs @@ -153,7 +153,7 @@ impl<'s> Parser<'s> { /// Parse the arguments to a function. fn parse_func_args(&mut self) -> ParseResult<(Vec, HashMap)> { - let mut args = vec![]; + let mut args = Vec::new(); let kwargs = HashMap::new(); let mut comma = false;