Add standard align
function and support right-alignment ➡️
This commit is contained in:
parent
61470fba68
commit
8f788f9a4f
72
src/func.rs
72
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::<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) }
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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
51
src/library/align.rs
Normal 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
34
src/library/mod.rs
Normal 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
64
src/library/styles.rs
Normal 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) }
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user