diff --git a/src/func.rs b/src/func.rs index c8e464f8f..776f0e98d 100644 --- a/src/func.rs +++ b/src/func.rs @@ -3,12 +3,10 @@ use std::any::Any; use std::collections::HashMap; use std::fmt::{self, Debug, Formatter}; -use toddle::query::FontClass; -use crate::layout::{layout, Layout, LayoutContext, LayoutResult}; -use crate::layout::flex::FlexLayout; -use crate::parsing::{parse, ParseContext, ParseError, ParseResult}; -use crate::syntax::{SyntaxTree, FuncHeader}; +use crate::layout::{Layout, LayoutContext, LayoutResult}; +use crate::parsing::{ParseContext, ParseResult}; +use crate::syntax::FuncHeader; /// Typesetting function types. @@ -79,11 +77,7 @@ impl Scope { /// Create a new scope with the standard functions contained. pub fn with_std() -> Scope { - let mut std = Scope::new(); - std.add::("bold"); - std.add::("italic"); - std.add::("mono"); - std + crate::library::std() } /// Add a function type to the scope giving it a name. @@ -108,61 +102,3 @@ 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 }, - $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) - -> 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 $style = ctx.style.clone(); - $style_change - - // Create a box and put it into a flex layout. - let boxed = layout(&self.body, LayoutContext { - style: &$style, - .. ctx - })?; - let flex = FlexLayout::from_box(boxed); - - Ok(Some(Layout::Flex(flex))) - } - } - }; -} - -style_func! { - /// Typesets text in bold. - pub struct BoldFunc { "bold" }, - style => { style.toggle_class(FontClass::Bold) } -} - -style_func! { - /// Typesets text in italics. - pub struct ItalicFunc { "italic" }, - style => { style.toggle_class(FontClass::Italic) } -} - -style_func! { - /// Typesets text in monospace. - pub struct MonospaceFunc { "mono" }, - style => { style.toggle_class(FontClass::Monospace) } -} diff --git a/src/layout/boxed.rs b/src/layout/boxed.rs index a860b2675..afcd52781 100644 --- a/src/layout/boxed.rs +++ b/src/layout/boxed.rs @@ -2,7 +2,7 @@ use crate::doc::{Document, Page, LayoutAction}; use crate::size::{Size, Size2D}; -use super::{ActionList, LayoutSpace, LayoutResult, LayoutError}; +use super::{ActionList, LayoutSpace, Alignment, LayoutResult, LayoutError}; /// A box layout has a fixed width and height and composes of actions. @@ -37,7 +37,7 @@ pub struct BoxContext { /// Layouts boxes block-style. #[derive(Debug)] pub struct BoxLayouter { - ctx: BoxContext, + pub ctx: BoxContext, actions: ActionList, dimensions: Size2D, usable: Size2D, @@ -51,9 +51,15 @@ impl BoxLayouter { BoxLayouter { ctx, actions: ActionList::new(), - dimensions: Size2D::zero(), + dimensions: match ctx.space.alignment { + Alignment::Left => Size2D::zero(), + Alignment::Right => Size2D::with_x(space.usable().x), + }, usable: space.usable(), - cursor: Size2D::new(space.padding.left, space.padding.right), + cursor: Size2D::new(match ctx.space.alignment { + Alignment::Left => space.padding.left, + Alignment::Right => space.dimensions.x - space.padding.right, + }, space.padding.top), } } @@ -71,12 +77,18 @@ impl BoxLayouter { return Err(LayoutError::NotEnoughSpace); } - // Apply the dimensions as they fit. - let height = layout.dimensions.y; + // Apply the dimensions if they fit. self.dimensions = new; + let width = layout.dimensions.x; + let height = layout.dimensions.y; + + let position = match self.ctx.space.alignment { + Alignment::Left => self.cursor, + Alignment::Right => self.cursor - Size2D::with_x(width), + }; // Add the box. - self.add_box_absolute(self.cursor, layout); + self.add_box_absolute(position, layout); // Adjust the cursor. self.cursor.y += height; @@ -86,11 +98,7 @@ impl BoxLayouter { /// 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(LayoutAction::MoveAbsolute(position)); - self.actions.set_origin(position); - self.actions.extend(layout.actions); + self.actions.add_box_absolute(position, layout); } /// Add some space in between two boxes. diff --git a/src/layout/flex.rs b/src/layout/flex.rs index 730b876e6..8b6926910 100644 --- a/src/layout/flex.rs +++ b/src/layout/flex.rs @@ -1,8 +1,7 @@ //! Flexible and lazy layouting of boxes. -use crate::doc::LayoutAction; use crate::size::{Size, Size2D}; -use super::{BoxLayout, ActionList, LayoutSpace, LayoutResult, LayoutError}; +use super::{BoxLayout, ActionList, LayoutSpace, Alignment, LayoutResult, LayoutError}; /// A flex layout consists of a yet unarranged list of boxes. @@ -81,7 +80,9 @@ struct FlexFinisher { dimensions: Size2D, usable: Size2D, cursor: Size2D, - line: Size2D, + line_metrics: Size2D, + line_content: Vec<(Size2D, BoxLayout)>, + glue: Option, } impl FlexFinisher { @@ -92,10 +93,15 @@ impl FlexFinisher { units: layout.units, ctx, actions: ActionList::new(), - dimensions: Size2D::zero(), + dimensions: match ctx.space.alignment { + Alignment::Left => Size2D::zero(), + Alignment::Right => Size2D::with_x(space.usable().x), + }, usable: space.usable(), cursor: Size2D::new(space.padding.left, space.padding.top), - line: Size2D::zero(), + line_metrics: Size2D::zero(), + line_content: vec![], + glue: None, } } @@ -128,14 +134,20 @@ impl FlexFinisher { /// Layout the box. fn boxed(&mut self, boxed: BoxLayout) -> LayoutResult<()> { + let last_glue_x = self.glue.as_ref() + .map(|g| g.dimensions.x) + .unwrap_or(Size::zero()); + // Move to the next line if necessary. - if self.line.x + boxed.dimensions.x > self.usable.x { + if self.line_metrics.x + boxed.dimensions.x + last_glue_x > self.usable.x { // If it still does not fit, we stand no chance. if boxed.dimensions.x > self.usable.x { return Err(LayoutError::NotEnoughSpace); } self.newline(); + } else if let Some(glue) = self.glue.take() { + self.append(glue); } self.append(boxed); @@ -145,37 +157,51 @@ impl FlexFinisher { /// Layout the glue. fn glue(&mut self, glue: BoxLayout) { - // Only add the glue if it fits on the line, otherwise move to the next line. - if self.line.x + glue.dimensions.x > self.usable.x { - self.newline(); - } else { - self.append(glue); - } + self.glue = Some(glue); } /// Append a box to the layout without checking anything. fn append(&mut self, layout: BoxLayout) { - // Move all actions into this layout and translate absolute positions. - self.actions.reset_origin(); - self.actions.add(LayoutAction::MoveAbsolute(self.cursor)); - self.actions.set_origin(self.cursor); - self.actions.extend(layout.actions); + let dim = layout.dimensions; + self.line_content.push((self.cursor, layout)); - // Adjust the sizes. - self.line.x += layout.dimensions.x; - self.line.y = crate::size::max(self.line.y, layout.dimensions.y); - self.cursor.x += layout.dimensions.x; + self.line_metrics.x += dim.x; + self.line_metrics.y = crate::size::max(self.line_metrics.y, dim.y); + self.cursor.x += dim.x; } /// Move to the next line. fn newline(&mut self) { - self.dimensions.x = crate::size::max(self.dimensions.x, self.line.x); + // Move all actions into this layout and translate absolute positions. + let remaining_space = Size2D::with_x(self.ctx.space.usable().x - self.line_metrics.x); + for (cursor, layout) in self.line_content.drain(..) { + let position = match self.ctx.space.alignment { + Alignment::Left => cursor, + Alignment::Right => { + // Right align everything by shifting it right by the + // amount of space left to the right of the line. + cursor + remaining_space + }, + }; + + self.actions.add_box_absolute(position, layout); + } + + // Stretch the dimensions to at least the line width. + self.dimensions.x = crate::size::max(self.dimensions.x, self.line_metrics.x); + + // If we wrote a line previously add the inter-line spacing. if self.dimensions.y > Size::zero() { self.dimensions.y += self.ctx.flex_spacing; } - self.dimensions.y += self.line.y; + + self.dimensions.y += self.line_metrics.y; + + // Reset the cursor the left and move down by the line and the inter-line spacing. self.cursor.x = self.ctx.space.padding.left; - self.cursor.y += self.line.y + self.ctx.flex_spacing; - self.line = Size2D::zero(); + self.cursor.y += self.line_metrics.y + self.ctx.flex_spacing; + + // Reset the current line metrics. + self.line_metrics = Size2D::zero(); } } diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 6b0400896..e5fdc42d7 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -52,11 +52,20 @@ pub struct LayoutSpace { pub dimensions: Size2D, /// Padding that should be respected on each side. pub padding: SizeBox, + /// 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. pub shrink_to_fit: bool, } +/// Where to align content. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum Alignment { + Left, + Right, +} + impl LayoutSpace { /// The actually usable area. pub fn usable(&self) -> Size2D { @@ -157,6 +166,7 @@ impl<'a, 'p> Layouter<'a, 'p> { space: LayoutSpace { dimensions: self.box_layouter.remaining(), padding: SizeBox::zero(), + alignment: self.box_layouter.ctx.space.alignment, shrink_to_fit: true, }, flex_spacing: (self.style.line_spacing - 1.0) * Size::pt(self.style.font_size), @@ -173,6 +183,7 @@ impl<'a, 'p> Layouter<'a, 'p> { space: LayoutSpace { dimensions: self.box_layouter.remaining(), padding: SizeBox::zero(), + alignment: self.box_layouter.ctx.space.alignment, shrink_to_fit: true, }, })?; @@ -196,8 +207,8 @@ impl<'a, 'p> Layouter<'a, 'p> { /// Manipulates and optimizes a list of actions. #[derive(Debug, Clone)] pub struct ActionList { + pub origin: Size2D, actions: Vec, - origin: Size2D, active_font: (usize, f32), } @@ -232,15 +243,12 @@ impl ActionList { } } - /// Move the origin for the upcomming actions. Absolute moves will be - /// changed by that origin. - pub fn set_origin(&mut self, origin: Size2D) { - self.origin = origin; - } - - /// Reset the origin to zero. - pub fn reset_origin(&mut self) { - self.origin = Size2D::zero(); + /// Add all actions from a box layout at a position. A move to the position + /// is generated and all moves inside the box layout are translated as necessary. + pub fn add_box_absolute(&mut self, position: Size2D, layout: BoxLayout) { + self.actions.push(LayoutAction::MoveAbsolute(position)); + self.origin = position; + self.extend(layout.actions); } /// Whether there are any actions in this list. diff --git a/src/lib.rs b/src/lib.rs index 1a97c5604..cb4be8b40 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,7 +20,7 @@ use toddle::query::{FontLoader, SharedFontLoader, FontProvider}; use crate::doc::Document; use crate::func::Scope; use crate::parsing::{parse, ParseContext, ParseResult, ParseError}; -use crate::layout::{layout, LayoutContext, LayoutSpace, LayoutError, LayoutResult}; +use crate::layout::{layout, LayoutContext, Alignment, LayoutSpace, LayoutError, LayoutResult}; use crate::layout::boxed::BoxLayout; use crate::style::{PageStyle, TextStyle}; use crate::syntax::SyntaxTree; @@ -35,6 +35,7 @@ pub mod parsing; pub mod size; pub mod style; pub mod syntax; +pub mod library; /// Transforms source code into typesetted documents. @@ -92,6 +93,7 @@ impl<'p> Typesetter<'p> { space: LayoutSpace { dimensions: self.page_style.dimensions, padding: self.page_style.margins, + alignment: Alignment::Left, shrink_to_fit: false, }, })?; @@ -184,5 +186,6 @@ mod test { #[test] fn shakespeare() { test("shakespeare", include_str!("../test/shakespeare.tps")); + test("shakespeare-right", &format!("[align:right][{}]", include_str!("../test/shakespeare.tps"))); } } diff --git a/src/library/align.rs b/src/library/align.rs new file mode 100644 index 000000000..4b2ee8c3e --- /dev/null +++ b/src/library/align.rs @@ -0,0 +1,51 @@ +//! Alignment function. + +use super::prelude::*; +use crate::layout::Alignment; + + +/// Allows to align content in different ways. +#[derive(Debug, PartialEq)] +pub struct AlignFunc { + alignment: Alignment, + body: Option, +} + +impl Function for AlignFunc { + fn parse(header: &FuncHeader, body: Option<&str>, ctx: ParseContext) + -> ParseResult where Self: Sized { + + if header.args.len() != 1 || !header.kwargs.is_empty() { + return err("expected exactly one positional argument specifying the alignment"); + } + + let alignment = if let Expression::Ident(ident) = &header.args[0] { + match ident.as_str() { + "left" => Alignment::Left, + "right" => Alignment::Right, + s => return err(format!("invalid alignment specifier: '{}'", s)), + } + } else { + return err(format!("expected alignment specifier, found: '{}'", header.args[0])); + }; + + let body = if let Some(body) = body { + Some(parse(body, ctx)?) + } else { + None + }; + + Ok(AlignFunc { alignment, body }) + } + + fn layout(&self, mut ctx: LayoutContext) -> LayoutResult> { + if let Some(body) = &self.body { + // Override the previous alignment and do the layouting. + ctx.space.alignment = self.alignment; + layout(body, ctx) + .map(|l| Some(Layout::Boxed(l))) + } else { + unimplemented!("context-modifying align func") + } + } +} diff --git a/src/library/mod.rs b/src/library/mod.rs new file mode 100644 index 000000000..9323c4c0e --- /dev/null +++ b/src/library/mod.rs @@ -0,0 +1,34 @@ +//! The standard library for the _Typst_ language. + +use crate::func::Scope; + +mod align; +mod styles; + +/// Useful imports for creating your own functions. +pub mod prelude { + pub use crate::syntax::{SyntaxTree, FuncHeader, Expression}; + pub use crate::parsing::{parse, ParseContext, ParseResult, ParseError}; + pub use crate::layout::{layout, Layout, LayoutContext, LayoutResult, LayoutError}; + pub use crate::layout::flex::FlexLayout; + pub use crate::layout::boxed::BoxLayout; + pub use crate::func::Function; + + pub fn err, T>(message: S) -> ParseResult { + Err(ParseError::new(message)) + } +} + +pub use align::AlignFunc; +pub use styles::{ItalicFunc, BoldFunc, MonospaceFunc}; + + +/// Create a scope with all standard functions. +pub fn std() -> Scope { + let mut std = Scope::new(); + std.add::("bold"); + std.add::("italic"); + std.add::("mono"); + std.add::("align"); + std +} diff --git a/src/library/styles.rs b/src/library/styles.rs new file mode 100644 index 000000000..c84b9fed6 --- /dev/null +++ b/src/library/styles.rs @@ -0,0 +1,64 @@ +//! Basic style functions: bold, italic, monospace. + +use super::prelude::*; +use toddle::query::FontClass; + + + +macro_rules! style_func { + ($(#[$outer:meta])* pub struct $struct:ident { $name:expr }, + $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) + -> 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 $style = ctx.style.clone(); + $style_change + + // Create a box and put it into a flex layout. + let boxed = layout(&self.body, LayoutContext { + style: &$style, + .. ctx + })?; + let flex = FlexLayout::from_box(boxed); + + Ok(Some(Layout::Flex(flex))) + } + } + }; +} + +style_func! { + /// Typesets text in bold. + pub struct BoldFunc { "bold" }, + style => { style.toggle_class(FontClass::Bold) } +} + +style_func! { + /// Typesets text in italics. + pub struct ItalicFunc { "italic" }, + style => { style.toggle_class(FontClass::Italic) } +} + +style_func! { + /// Typesets text in monospace. + pub struct MonospaceFunc { "mono" }, + style => { style.toggle_class(FontClass::Monospace) } +}