Add standard align function and support right-alignment ➡️

This commit is contained in:
Laurenz 2019-10-10 23:36:17 +02:00
parent 61470fba68
commit 8f788f9a4f
8 changed files with 246 additions and 116 deletions

View File

@ -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::<BoldFunc>("bold");
std.add::<ItalicFunc>("italic");
std.add::<MonospaceFunc>("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<Self> 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<Option<Layout>> {
// 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) }
}

View File

@ -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.

View File

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

View File

@ -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<LayoutAction>,
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.

View File

@ -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")));
}
}

51
src/library/align.rs Normal file
View File

@ -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<SyntaxTree>,
}
impl Function for AlignFunc {
fn parse(header: &FuncHeader, body: Option<&str>, ctx: ParseContext)
-> ParseResult<Self> 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<Option<Layout>> {
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")
}
}
}

34
src/library/mod.rs Normal file
View File

@ -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<S: Into<String>, T>(message: S) -> ParseResult<T> {
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::<BoldFunc>("bold");
std.add::<ItalicFunc>("italic");
std.add::<MonospaceFunc>("mono");
std.add::<AlignFunc>("align");
std
}

64
src/library/styles.rs Normal file
View File

@ -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<Self> 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<Option<Layout>> {
// 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) }
}