diff --git a/src/export/pdf.rs b/src/export/pdf.rs index bb6881183..76fc59b89 100644 --- a/src/export/pdf.rs +++ b/src/export/pdf.rs @@ -249,8 +249,8 @@ impl<'d, W: Write> ExportProcess<'d, W> { }, LayoutAction::SetFont(id, size) => { - active_font = (self.font_remap[id], *size); - text.tf(active_font.0 as u32 + 1, *size); + active_font = (self.font_remap[id], size.to_pt()); + text.tf(active_font.0 as u32 + 1, size.to_pt()); } LayoutAction::WriteText(string) => { diff --git a/src/func/mod.rs b/src/func/mod.rs index bd61204ec..fa6407d85 100644 --- a/src/func/mod.rs +++ b/src/func/mod.rs @@ -12,8 +12,9 @@ pub mod helpers; /// Useful imports for creating your own functions. pub mod prelude { pub use crate::func::{Command, CommandList, Function}; - pub use crate::layout::{layout_tree, Layout, LayoutContext, MultiLayout}; - pub use crate::layout::{Flow, Alignment, LayoutError, LayoutResult}; + pub use crate::layout::{layout_tree, Layout, MultiLayout, LayoutContext, LayoutSpace}; + pub use crate::layout::{LayoutAxes, AlignedAxis, Axis, Alignment}; + pub use crate::layout::{LayoutError, LayoutResult}; pub use crate::syntax::{SyntaxTree, FuncHeader, FuncArgs, Expression, Spanned, Span}; pub use crate::syntax::{parse, ParseContext, ParseError, ParseResult}; pub use crate::size::{Size, Size2D, SizeBox}; @@ -88,13 +89,16 @@ where T: Debug + PartialEq + 'static #[derive(Debug)] pub enum Command<'a> { LayoutTree(&'a SyntaxTree), + Add(Layout), - AddMany(MultiLayout), - AddFlex(Layout), - SetAlignment(Alignment), - SetStyle(TextStyle), - FinishLayout, + AddMultiple(MultiLayout), + FinishFlexRun, + FinishFlexLayout, + FinishLayout, + + SetStyle(TextStyle), + SetAxes(LayoutAxes), } /// A sequence of commands requested for execution by a function. diff --git a/src/layout/actions.rs b/src/layout/actions.rs index 707d6113e..ed3bc1826 100644 --- a/src/layout/actions.rs +++ b/src/layout/actions.rs @@ -4,7 +4,7 @@ use std::fmt::{self, Display, Formatter}; use std::io::{self, Write}; use super::Layout; -use crate::size::Size2D; +use crate::size::{Size, Size2D}; use LayoutAction::*; /// A layouting action. @@ -13,7 +13,7 @@ pub enum LayoutAction { /// Move to an absolute position. MoveAbsolute(Size2D), /// Set the font by index and font size. - SetFont(usize, f32), + SetFont(usize, Size), /// Write text starting at the current position. WriteText(String), /// Visualize a box for debugging purposes. @@ -26,7 +26,7 @@ impl LayoutAction { pub fn serialize(&self, f: &mut W) -> io::Result<()> { match self { MoveAbsolute(s) => write!(f, "m {:.4} {:.4}", s.x.to_pt(), s.y.to_pt()), - SetFont(i, s) => write!(f, "f {} {}", i, s), + SetFont(i, s) => write!(f, "f {} {}", i, s.to_pt()), WriteText(s) => write!(f, "w {}", s), DebugBox(p, s) => write!( f, @@ -69,9 +69,9 @@ debug_display!(LayoutAction); pub struct LayoutActionList { pub origin: Size2D, actions: Vec, - active_font: (usize, f32), + active_font: (usize, Size), next_pos: Option, - next_font: Option<(usize, f32)>, + next_font: Option<(usize, Size)>, } impl LayoutActionList { @@ -80,7 +80,7 @@ impl LayoutActionList { LayoutActionList { actions: vec![], origin: Size2D::zero(), - active_font: (std::usize::MAX, 0.0), + active_font: (std::usize::MAX, Size::zero()), next_pos: None, next_font: None, } diff --git a/src/layout/flex.rs b/src/layout/flex.rs index a364b6083..c136ad0d1 100644 --- a/src/layout/flex.rs +++ b/src/layout/flex.rs @@ -75,16 +75,18 @@ impl FlexLayouter { } } - /// This layouter's context. - pub fn ctx(&self) -> FlexContext { - self.ctx - } - /// Add a sublayout. pub fn add(&mut self, layout: Layout) { self.units.push(FlexUnit::Boxed(layout)); } + /// Add multiple sublayouts from a multi-layout. + pub fn add_multiple(&mut self, layouts: MultiLayout) { + for layout in layouts { + self.add(layout); + } + } + /// Add a space box which can be replaced by a run break. pub fn add_space(&mut self, space: Size) { self.units.push(FlexUnit::Space(space)); @@ -181,6 +183,11 @@ impl FlexLayouter { Ok(()) } + /// This layouter's context. + pub fn ctx(&self) -> FlexContext { + self.ctx + } + /// Whether this layouter contains any items. pub fn is_empty(&self) -> bool { self.units.is_empty() diff --git a/src/layout/mod.rs b/src/layout/mod.rs index f0160f31f..2bafd792e 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -192,7 +192,7 @@ impl LayoutSpace { } /// The axes along which the content is laid out. -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, Eq, PartialEq)] pub struct LayoutAxes { pub primary: AlignedAxis, pub secondary: AlignedAxis, diff --git a/src/layout/stacked.rs b/src/layout/stacked.rs index 419ec6b7b..cc6caa5e3 100644 --- a/src/layout/stacked.rs +++ b/src/layout/stacked.rs @@ -1,3 +1,4 @@ +use smallvec::smallvec; use super::*; /// Layouts boxes stack-like. @@ -66,7 +67,7 @@ impl StackLayouter { } /// Add multiple sublayouts from a multi-layout. - pub fn add_many(&mut self, layouts: MultiLayout) -> LayoutResult<()> { + pub fn add_multiple(&mut self, layouts: MultiLayout) -> LayoutResult<()> { for layout in layouts { self.add(layout)?; } @@ -135,7 +136,7 @@ impl StackLayouter { /// finishing this stack. Otherwise, the new layout only appears if new /// content is added to it. fn start_new_space(&mut self, include_empty: bool) { - self.active_space = (self.active_space + 1).min(self.ctx.spaces.len() - 1); + self.active_space = self.next_space(); self.usable = self.ctx.spaces[self.active_space].usable().generalized(self.ctx.axes); self.dimensions = start_dimensions(self.usable, self.ctx.axes); self.include_empty = include_empty; @@ -151,10 +152,20 @@ impl StackLayouter { self.usable } - /// The (specialized) remaining area for new layouts in the current space. - pub fn remaining(&self) -> Size2D { - Size2D::new(self.usable.x, self.usable.y - self.dimensions.y) - .specialized(self.ctx.axes) + /// The remaining spaces for new layouts in the current space. + pub fn remaining(&self, shrink_to_fit: bool) -> LayoutSpaces { + let mut spaces = smallvec![LayoutSpace { + dimensions: Size2D::new(self.usable.x, self.usable.y - self.dimensions.y) + .specialized(self.ctx.axes), + padding: SizeBox::zero(), + shrink_to_fit, + }]; + + for space in &self.ctx.spaces[self.next_space()..] { + spaces.push(space.usable_space(shrink_to_fit)); + } + + spaces } /// Whether this layouter is in its last space. @@ -162,6 +173,10 @@ impl StackLayouter { self.active_space == self.ctx.spaces.len() - 1 } + fn next_space(&self) -> usize { + (self.active_space + 1).min(self.ctx.spaces.len() - 1) + } + /// The combined size of the so-far included boxes with the other size. fn size_with(&self, other: Size2D) -> Size2D { Size2D { diff --git a/src/layout/text.rs b/src/layout/text.rs index 79ace0405..88d83ab71 100644 --- a/src/layout/text.rs +++ b/src/layout/text.rs @@ -13,16 +13,6 @@ pub struct TextContext<'a, 'p> { pub style: &'a TextStyle, } -impl<'a, 'p> TextContext<'a, 'p> { - /// Create a text context from a generic layout context. - pub fn from_layout_ctx(ctx: LayoutContext<'a, 'p>) -> TextContext<'a, 'p> { - TextContext { - loader: ctx.loader, - style: ctx.style, - } - } -} - /// Layouts text into a box. /// /// There is no complex layout involved. The text is simply laid out left- @@ -81,7 +71,7 @@ impl<'a, 'p> TextLayouter<'a, 'p> { } Ok(Layout { - dimensions: Size2D::new(self.width, Size::pt(self.ctx.style.font_size)), + dimensions: Size2D::new(self.width, self.ctx.style.font_size), actions: self.actions.into_vec(), debug_render: false, }) @@ -107,15 +97,16 @@ impl<'a, 'p> TextLayouter<'a, 'p> { let glyph = font .read_table::()? .get(c) - .expect("layout text: font should have char"); + .expect("select_font: font should have char"); let glyph_width = font .read_table::()? .get(glyph) - .expect("layout text: font should have glyph") + .expect("select_font: font should have glyph") .advance_width as f32; - let char_width = font_unit_to_size(glyph_width) * self.ctx.style.font_size; + let char_width = font_unit_to_size(glyph_width) + * self.ctx.style.font_size.to_pt(); return Ok((index, char_width)); } diff --git a/src/layout/tree.rs b/src/layout/tree.rs index b64dd6ebd..ca2051f4a 100644 --- a/src/layout/tree.rs +++ b/src/layout/tree.rs @@ -13,8 +13,6 @@ struct TreeLayouter<'a, 'p> { stack: StackLayouter, flex: FlexLayouter, style: Cow<'a, TextStyle>, - alignment: Alignment, - set_newline: bool, } impl<'a, 'p> TreeLayouter<'a, 'p> { @@ -22,51 +20,41 @@ impl<'a, 'p> TreeLayouter<'a, 'p> { fn new(ctx: LayoutContext<'a, 'p>) -> TreeLayouter<'a, 'p> { TreeLayouter { ctx, - stack: StackLayouter::new(StackContext::from_layout_ctx(ctx)), + stack: StackLayouter::new(StackContext { + spaces: ctx.spaces, + axes: ctx.axes, + }), flex: FlexLayouter::new(FlexContext { - space: ctx.space.usable_space(), - followup_spaces: ctx.followup_spaces.map(|s| s.usable_space()), - shrink_to_fit: true, - .. FlexContext::from_layout_ctx(ctx, flex_spacing(&ctx.style)) + flex_spacing: flex_spacing(&ctx.style), + spaces: ctx.spaces.iter().map(|space| space.usable_space(true)).collect(), + axes: ctx.axes, }), style: Cow::Borrowed(ctx.style), - alignment: ctx.alignment, - set_newline: false, } } - /// Layout the tree into a box. + /// Layout a syntax tree. fn layout(&mut self, tree: &SyntaxTree) -> LayoutResult<()> { for node in &tree.nodes { match &node.val { Node::Text(text) => { - let layout = self.layout_text(text)?; - self.flex.add(layout); - self.set_newline = true; + self.flex.add(layout_text(text, TextContext { + loader: &self.ctx.loader, + style: &self.style, + })?); } Node::Space => { - // Only add a space if there was any content before. if !self.flex.is_empty() { - let layout = self.layout_text(" ")?; - self.flex.add_glue(layout.dimensions); + self.flex.add_space(self.style.word_spacing * self.style.font_size); } } - - // Finish the current flex layouting process. Node::Newline => { - self.finish_flex()?; - - if self.set_newline { - let space = paragraph_spacing(&self.style); - self.stack.add_space(space); - self.set_newline = false; + if !self.flex.is_empty() { + self.finish_paragraph()?; } - - self.start_new_flex(); } - // 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), @@ -78,115 +66,98 @@ impl<'a, 'p> TreeLayouter<'a, 'p> { Ok(()) } + /// Layout a function. + fn layout_func(&mut self, func: &FuncCall) -> LayoutResult<()> { + // Finish the current flex layout on a copy to find out how + // much space would be remaining if we finished. + let mut lookahead = self.stack.clone(); + lookahead.add_multiple(self.flex.clone().finish()?)?; + let spaces = lookahead.remaining(true); + + let commands = func.body.val.layout(LayoutContext { + style: &self.style, + spaces, + .. self.ctx + })?; + + for command in commands { + self.execute(command)?; + } + + Ok(()) + } + + fn execute(&mut self, command: Command) -> LayoutResult<()> { + match command { + Command::LayoutTree(tree) => self.layout(tree)?, + + Command::Add(layout) => self.flex.add(layout), + Command::AddMultiple(layouts) => self.flex.add_multiple(layouts), + + Command::FinishFlexRun => self.flex.add_break(), + Command::FinishFlexLayout => self.finish_paragraph()?, + Command::FinishLayout => self.finish_layout(true)?, + + Command::SetStyle(style) => *self.style.to_mut() = style, + Command::SetAxes(axes) => { + if axes.secondary != self.ctx.axes.secondary { + self.stack.set_axis(axes.secondary); + } else if axes.primary != self.ctx.axes.primary { + self.flex.set_axis(axes.primary); + } + + self.ctx.axes = axes; + } + } + + Ok(()) + } + /// Finish the layout. fn finish(mut self) -> LayoutResult { self.finish_flex()?; Ok(self.stack.finish()) } - /// Layout a function. - fn layout_func(&mut self, func: &FuncCall) -> LayoutResult<()> { - // Finish the current flex layout on a copy to find out how - // much space would be remaining if we finished. - - let mut lookahead_stack = self.stack.clone(); - let layouts = self.flex.clone().finish()?; - lookahead_stack.add_many(layouts)?; - let remaining = lookahead_stack.remaining(); - - let mut ctx = self.ctx; - ctx.style = &self.style; - ctx.flow = Flow::Vertical; - ctx.shrink_to_fit = true; - ctx.space.dimensions = remaining; - ctx.space.padding = SizeBox::zero(); - if let Some(space) = ctx.followup_spaces.as_mut() { - *space = space.usable_space(); - } - - let commands = func.body.val.layout(ctx)?; - - for command in commands { - match command { - Command::LayoutTree(tree) => self.layout(tree)?, - - Command::Add(layout) => { - self.finish_flex()?; - self.stack.add(layout)?; - self.set_newline = true; - self.start_new_flex(); - } - - Command::AddMany(layouts) => { - self.finish_flex()?; - self.stack.add_many(layouts)?; - self.set_newline = true; - self.start_new_flex(); - } - - Command::AddFlex(layout) => self.flex.add(layout), - - Command::SetAlignment(alignment) => { - self.finish_flex()?; - self.alignment = alignment; - self.start_new_flex(); - } - - Command::SetStyle(style) => *self.style.to_mut() = style, - - Command::FinishLayout => { - self.finish_flex()?; - self.stack.finish_layout(true); - self.start_new_flex(); - } - - Command::FinishFlexRun => self.flex.add_break(), - } - } - + /// Finish the current stack layout. + fn finish_layout(&mut self, include_empty: bool) -> LayoutResult<()> { + self.finish_flex()?; + self.stack.finish_layout(include_empty); + self.start_new_flex(); Ok(()) } - /// 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) -> LayoutResult { - let ctx = TextContext { - loader: &self.ctx.loader, - style: &self.style, - }; - - layout_text(text, ctx) + /// Finish the current flex layout and add space after it. + fn finish_paragraph(&mut self) -> LayoutResult<()> { + self.finish_flex()?; + self.stack.add_space(paragraph_spacing(&self.style)); + self.start_new_flex(); + Ok(()) } /// Finish the current flex layout and add it the stack. fn finish_flex(&mut self) -> LayoutResult<()> { - if self.flex.is_empty() { - return Ok(()); + if !self.flex.is_empty() { + let layouts = self.flex.finish()?; + self.stack.add_multiple(layouts)?; } - - let layouts = self.flex.finish()?; - self.stack.add_many(layouts)?; - Ok(()) } /// Start a new flex layout. fn start_new_flex(&mut self) { - let mut ctx = self.flex.ctx(); - ctx.space.dimensions = self.stack.remaining(); - ctx.alignment = self.alignment; - ctx.flex_spacing = flex_spacing(&self.style); - - self.flex = FlexLayouter::new(ctx); + self.flex = FlexLayouter::new(FlexContext { + flex_spacing: flex_spacing(&self.style), + spaces: self.stack.remaining(true), + axes: self.ctx.axes, + }); } } fn flex_spacing(style: &TextStyle) -> Size { - (style.line_spacing - 1.0) * Size::pt(style.font_size) + (style.line_spacing - 1.0) * 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 + (style.paragraph_spacing - 1.0) * style.font_size } diff --git a/src/library/structure.rs b/src/library/structure.rs index e6d242a00..2bcd27444 100644 --- a/src/library/structure.rs +++ b/src/library/structure.rs @@ -1,6 +1,28 @@ use crate::func::prelude::*; use Command::*; +/// ↩ `line.break`, `n`: Ends the current line. +#[derive(Debug, PartialEq)] +pub struct Linebreak; + +function! { + data: Linebreak, + parse: plain, + layout(_, _) { Ok(commands![FinishFlexRun]) } +} + +/// ↕ `paragraph.break`: Ends the current paragraph. +/// +/// This has the same effect as two subsequent newlines. +#[derive(Debug, PartialEq)] +pub struct Parbreak; + +function! { + data: Parbreak, + parse: plain, + layout(_, _) { Ok(commands![FinishFlexLayout]) } +} + /// 📜 `page.break`: Ends the current page. #[derive(Debug, PartialEq)] pub struct Pagebreak; @@ -8,23 +30,7 @@ pub struct Pagebreak; function! { data: Pagebreak, parse: plain, - - layout(_, _) { - Ok(commands![FinishLayout]) - } -} - -/// 🔙 `line.break`, `n`: Ends the current line. -#[derive(Debug, PartialEq)] -pub struct Linebreak; - -function! { - data: Linebreak, - parse: plain, - - layout(_, _) { - Ok(commands![FinishFlexRun]) - } + layout(_, _) { Ok(commands![FinishLayout]) } } /// 📐 `align`: Aligns content in different ways. diff --git a/src/style.rs b/src/style.rs index 68bd9a270..e2ab09377 100644 --- a/src/style.rs +++ b/src/style.rs @@ -13,7 +13,9 @@ pub struct TextStyle { /// leftmost possible one. pub fallback: Vec, /// The font size. - pub font_size: f32, + pub font_size: Size, + /// The word spacing (as a multiple of the font size). + pub word_spacing: f32, /// The line spacing (as a multiple of the font size). pub line_spacing: f32, /// The paragraphs spacing (as a multiple of the font size). @@ -63,7 +65,8 @@ impl Default for TextStyle { TextStyle { classes: vec![Regular], fallback: vec![Serif], - font_size: 11.0, + font_size: Size::pt(11.0), + word_spacing: 0.25, line_spacing: 1.2, paragraph_spacing: 1.5, }