diff --git a/src/func/mod.rs b/src/func/mod.rs index 27dceee31..360c19f81 100644 --- a/src/func/mod.rs +++ b/src/func/mod.rs @@ -103,7 +103,9 @@ pub enum Command<'a> { BreakParagraph, - SetStyle(TextStyle), + SetTextStyle(TextStyle), + SetPageStyle(PageStyle), + SetAxes(LayoutAxes), } diff --git a/src/layout/flex.rs b/src/layout/flex.rs index 0ad31521c..142530a60 100644 --- a/src/layout/flex.rs +++ b/src/layout/flex.rs @@ -19,7 +19,9 @@ use super::*; /// However, it can be any layout. #[derive(Debug, Clone)] pub struct FlexLayouter { - ctx: FlexContext, + axes: LayoutAxes, + flex_spacing: Size, + stack: StackLayouter, units: Vec, @@ -69,14 +71,16 @@ impl FlexLayouter { /// Create a new flex layouter. pub fn new(ctx: FlexContext) -> FlexLayouter { let stack = StackLayouter::new(StackContext { - spaces: ctx.spaces.clone(), + spaces: ctx.spaces, axes: ctx.axes, shrink_to_fit: ctx.shrink_to_fit, }); let usable = stack.usable().x; FlexLayouter { - ctx, + axes: ctx.axes, + flex_spacing: ctx.flex_spacing, + units: vec![], stack, @@ -126,6 +130,18 @@ impl FlexLayouter { self.units.push(FlexUnit::SetAxes(axes)); } + /// Update the followup space to be used by this flex layouter. + pub fn set_spaces(&mut self, spaces: LayoutSpaces, replace_empty: bool) { + if replace_empty && self.box_is_empty() && self.stack.space_is_empty() { + self.stack.set_spaces(spaces, true); + self.total_usable = self.stack.usable().x; + self.usable = self.total_usable; + self.space = None; + } else { + self.stack.set_spaces(spaces, false); + } + } + /// Compute the justified layout. /// /// The layouter is not consumed by this to prevent ownership problems @@ -176,7 +192,7 @@ impl FlexLayouter { /// Layout a content box into the current flex run or start a new run if /// it does not fit. fn layout_box(&mut self, boxed: Layout) -> LayoutResult<()> { - let size = self.ctx.axes.generalize(boxed.dimensions); + let size = self.axes.generalize(boxed.dimensions); if size.x > self.size_left() { self.space = None; @@ -213,7 +229,7 @@ impl FlexLayouter { } fn layout_set_axes(&mut self, axes: LayoutAxes) { - if axes.primary != self.ctx.axes.primary { + if axes.primary != self.axes.primary { self.finish_aligned_run(); self.usable = match axes.primary.alignment { @@ -231,11 +247,11 @@ impl FlexLayouter { }; } - if axes.secondary != self.ctx.axes.secondary { + if axes.secondary != self.axes.secondary { self.stack.set_axes(axes); } - self.ctx.axes = axes; + self.axes = axes; } /// Finish the current flex run. @@ -248,7 +264,7 @@ impl FlexLayouter { let actions = std::mem::replace(&mut self.merged_actions, LayoutActionList::new()); self.stack.add(Layout { - dimensions: self.ctx.axes.specialize(self.merged_dimensions), + dimensions: self.axes.specialize(self.merged_dimensions), actions: actions.into_vec(), debug_render: false, })?; @@ -265,38 +281,33 @@ impl FlexLayouter { return; } - let factor = if self.ctx.axes.primary.axis.is_positive() { 1 } else { -1 }; - let anchor = self.ctx.axes.primary.anchor(self.total_usable) - - self.ctx.axes.primary.anchor(self.run.size.x); + let factor = if self.axes.primary.axis.is_positive() { 1 } else { -1 }; + let anchor = self.axes.primary.anchor(self.total_usable) + - self.axes.primary.anchor(self.run.size.x); self.max_extent = crate::size::max(self.max_extent, anchor + factor * self.run.size.x); for (offset, layout) in self.run.content.drain(..) { let general_position = Size2D::with_x(anchor + factor * offset); - let position = self.ctx.axes.specialize(general_position); + let position = self.axes.specialize(general_position); self.merged_actions.add_layout(position, layout); } - self.merged_dimensions.x = match self.ctx.axes.primary.alignment { + self.merged_dimensions.x = match self.axes.primary.alignment { Alignment::Origin => self.run.size.x, Alignment::Center | Alignment::End => self.total_usable, }; self.merged_dimensions.y = crate::size::max( self.merged_dimensions.y, - self.run.size.y + self.ctx.flex_spacing, + self.run.size.y + self.flex_spacing, ); self.last_run_remaining = Size2D::new(self.size_left(), self.merged_dimensions.y); self.run.size = Size2D::zero(); } - /// This layouter's context. - pub fn ctx(&self) -> &FlexContext { - &self.ctx - } - pub fn remaining(&self) -> LayoutResult<(LayoutSpaces, LayoutSpaces)> { let mut future = self.clone(); future.finish_box()?; @@ -310,7 +321,6 @@ impl FlexLayouter { Ok((flex_spaces, stack_spaces)) } - /// Whether this layouter contains any items. pub fn box_is_empty(&self) -> bool { !self.units.iter().any(|unit| matches!(unit, FlexUnit::Boxed(_))) } diff --git a/src/layout/mod.rs b/src/layout/mod.rs index a45147048..426a51ecc 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -9,7 +9,7 @@ use toddle::Error as FontError; use crate::func::Command; use crate::size::{Size, Size2D, SizeBox}; -use crate::style::TextStyle; +use crate::style::{PageStyle, TextStyle}; use crate::syntax::{FuncCall, Node, SyntaxTree}; mod actions; @@ -131,9 +131,15 @@ pub struct LayoutContext<'a, 'p> { /// using [`layout_text`]. pub loader: &'a SharedFontLoader<'p>, + /// Whether this layouting process handles the top-level pages. + pub top_level: bool, + /// The style to set text with. This includes sizes and font classes /// which determine which font from the loaders selection is used. - pub style: &'a TextStyle, + pub text_style: &'a TextStyle, + + /// The current size and margins of the top-level pages. + pub page_style: PageStyle, /// The spaces to layout in. pub spaces: LayoutSpaces, @@ -281,6 +287,8 @@ pub enum Alignment { /// The error type for layouting. pub enum LayoutError { + /// An action is unallowed in the active context. + Unallowed(&'static str), /// There is not enough space to add an item. NotEnoughSpace(&'static str), /// There was no suitable font for the given character. @@ -295,6 +303,7 @@ pub type LayoutResult = Result; error_type! { err: LayoutError, show: f => match err { + LayoutError::Unallowed(desc) => write!(f, "unallowed: {}", desc), LayoutError::NotEnoughSpace(desc) => write!(f, "not enough space: {}", desc), LayoutError::NoSuitableFont(c) => write!(f, "no suitable font for '{}'", c), LayoutError::Font(err) => write!(f, "font error: {}", err), diff --git a/src/layout/stacked.rs b/src/layout/stacked.rs index a75850460..a5db5638b 100644 --- a/src/layout/stacked.rs +++ b/src/layout/stacked.rs @@ -96,9 +96,21 @@ impl StackLayouter { pub fn set_axes(&mut self, axes: LayoutAxes) { if axes != self.ctx.axes { self.finish_boxes(); + self.ctx.axes = axes; self.usable = self.remains(); self.dimensions = Size2D::zero(); - self.ctx.axes = axes; + } + } + + /// Update the followup space to be used by this flex layouter. + pub fn set_spaces(&mut self, spaces: LayoutSpaces, replace_empty: bool) { + if replace_empty && self.space_is_empty() { + self.usable = self.ctx.axes.generalize(spaces[0].usable()); + self.active_space = 0; + self.ctx.spaces = spaces; + } else { + self.ctx.spaces.truncate(self.active_space + 1); + self.ctx.spaces.extend(spaces); } } @@ -143,6 +155,10 @@ impl StackLayouter { /// Compose all cached boxes into a layout. fn finish_boxes(&mut self) { + if self.boxes.is_empty() { + return; + } + let space = self.ctx.spaces[self.active_space]; let start = space.start() + Size2D::with_y(self.merged_dimensions.y); @@ -170,11 +186,6 @@ impl StackLayouter { self.merged_dimensions = merge_sizes(self.merged_dimensions, dimensions); } - /// This layouter's context. - pub fn ctx(&self) -> &StackContext { - &self.ctx - } - /// The (generalized) usable area of the current space. pub fn usable(&self) -> Size2D { self.usable @@ -198,6 +209,10 @@ impl StackLayouter { Size2D::new(self.usable.x, self.usable.y - self.dimensions.y) } + pub fn space_is_empty(&self) -> bool { + self.boxes.is_empty() && self.merged_dimensions == Size2D::zero() + } + /// Whether this layouter is in its last space. pub fn in_last_space(&self) -> bool { self.active_space == self.ctx.spaces.len() - 1 diff --git a/src/layout/tree.rs b/src/layout/tree.rs index 177a6308a..ae6a15c88 100644 --- a/src/layout/tree.rs +++ b/src/layout/tree.rs @@ -1,4 +1,5 @@ use super::*; +use smallvec::smallvec; /// Layouts syntax trees into boxes. pub fn layout_tree(tree: &SyntaxTree, ctx: LayoutContext) -> LayoutResult { @@ -19,12 +20,12 @@ impl<'a, 'p> TreeLayouter<'a, 'p> { fn new(ctx: LayoutContext<'a, 'p>) -> TreeLayouter<'a, 'p> { TreeLayouter { flex: FlexLayouter::new(FlexContext { - flex_spacing: flex_spacing(&ctx.style), + flex_spacing: flex_spacing(&ctx.text_style), spaces: ctx.spaces.clone(), axes: ctx.axes, shrink_to_fit: ctx.shrink_to_fit, }), - style: ctx.style.clone(), + style: ctx.text_style.clone(), ctx, } } @@ -68,7 +69,8 @@ impl<'a, 'p> TreeLayouter<'a, 'p> { let (flex_spaces, stack_spaces) = self.flex.remaining()?; let ctx = |spaces| LayoutContext { - style: &self.style, + top_level: false, + text_style: &self.style, spaces: spaces, shrink_to_fit: true, .. self.ctx @@ -107,7 +109,21 @@ impl<'a, 'p> TreeLayouter<'a, 'p> { Command::BreakParagraph => self.break_paragraph()?, - Command::SetStyle(style) => self.style = style, + Command::SetTextStyle(style) => self.style = style, + Command::SetPageStyle(style) => { + if !self.ctx.top_level { + Err(LayoutError::Unallowed("can only set page style from top level"))?; + } + + self.ctx.page_style = style; + self.flex.set_spaces(smallvec![ + LayoutSpace { + dimensions: style.dimensions, + padding: style.margins, + } + ], true); + }, + Command::SetAxes(axes) => { self.flex.set_axes(axes); self.ctx.axes = axes; diff --git a/src/lib.rs b/src/lib.rs index d24941665..c6a21c511 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,7 +21,7 @@ use smallvec::smallvec; use toddle::query::{FontLoader, FontProvider, SharedFontLoader}; use crate::func::Scope; -use crate::layout::{layout_tree, LayoutContext, MultiLayout}; +use crate::layout::{layout_tree, MultiLayout, LayoutContext}; use crate::layout::{LayoutAxes, AlignedAxis, Axis, Alignment}; use crate::layout::{LayoutError, LayoutResult, LayoutSpace}; use crate::syntax::{SyntaxTree, parse, ParseContext, ParseError, ParseResult}; @@ -98,7 +98,9 @@ impl<'p> Typesetter<'p> { &tree, LayoutContext { loader: &self.loader, - style: &self.text_style, + top_level: true, + text_style: &self.text_style, + page_style: self.page_style, spaces: smallvec![LayoutSpace { dimensions: self.page_style.dimensions, padding: self.page_style.margins, diff --git a/src/library/mod.rs b/src/library/mod.rs index d795d4883..5fa326c89 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -6,6 +6,7 @@ pub_use_mod!(boxed); pub_use_mod!(axes); pub_use_mod!(spacing); pub_use_mod!(style); +pub_use_mod!(page); /// Create a scope with all standard functions. pub fn std() -> Scope { @@ -19,7 +20,6 @@ pub fn std() -> Scope { std.add::("line.break"); std.add::("paragraph.break"); std.add::("page.break"); - std.add::("h"); std.add::("v"); @@ -27,5 +27,8 @@ pub fn std() -> Scope { std.add::("italic"); std.add::("mono"); + std.add::("page.size"); + std.add::("page.margins"); + std } diff --git a/src/library/page.rs b/src/library/page.rs new file mode 100644 index 000000000..4efcbea0a --- /dev/null +++ b/src/library/page.rs @@ -0,0 +1,79 @@ +use crate::func::prelude::*; + +/// `page.break`: Ends the current page. +#[derive(Debug, PartialEq)] +pub struct PageBreak; + +function! { + data: PageBreak, + parse: plain, + layout(_, _) { Ok(commands![FinishLayout]) } +} + +/// `page.size`: Set the size of pages. +#[derive(Debug, PartialEq)] +pub struct PageSize { + width: Option, + height: Option, +} + +function! { + data: PageSize, + + parse(args, body, _ctx) { + parse!(forbidden: body); + Ok(PageSize { + width: args.get_key_opt::("width")?.map(|a| a.val), + height: args.get_key_opt::("height")?.map(|a| a.val), + }) + } + + layout(this, ctx) { + let mut style = ctx.page_style; + + if let Some(width) = this.width { style.dimensions.x = width; } + if let Some(height) = this.height { style.dimensions.y = height; } + + Ok(commands![SetPageStyle(style)]) + } +} + +/// `page.margins`: Set the margins of pages. +#[derive(Debug, PartialEq)] +pub struct PageMargins { + left: Option, + top: Option, + right: Option, + bottom: Option, +} + +function! { + data: PageMargins, + + parse(args, body, _ctx) { + parse!(forbidden: body); + let default = args.get_pos_opt::()?; + let mut get = |which| { + args.get_key_opt::(which) + .map(|size| size.or(default).map(|a| a.val)) + }; + + Ok(PageMargins { + left: get("left")?, + top: get("top")?, + right: get("right")?, + bottom: get("bottom")?, + }) + } + + layout(this, ctx) { + let mut style = ctx.page_style; + + if let Some(left) = this.left { style.margins.left = left; } + if let Some(top) = this.top { style.margins.top = top; } + if let Some(right) = this.right { style.margins.right = right; } + if let Some(bottom) = this.bottom { style.margins.bottom = bottom; } + + Ok(commands![SetPageStyle(style)]) + } +} diff --git a/src/library/spacing.rs b/src/library/spacing.rs index afd26d805..f83f333dc 100644 --- a/src/library/spacing.rs +++ b/src/library/spacing.rs @@ -22,16 +22,6 @@ function! { layout(_, _) { Ok(commands![FinishBox]) } } -/// `page.break`: Ends the current page. -#[derive(Debug, PartialEq)] -pub struct PageBreak; - -function! { - data: PageBreak, - parse: plain, - layout(_, _) { Ok(commands![FinishLayout]) } -} - macro_rules! space_func { ($ident:ident, $doc:expr, $var:ident => $command:expr) => ( #[doc = $doc] @@ -57,7 +47,7 @@ macro_rules! space_func { layout(this, ctx) { let $var = match this.0 { Spacing::Absolute(s) => s, - Spacing::Relative(f) => f * ctx.style.font_size, + Spacing::Relative(f) => f * ctx.text_style.font_size, }; Ok(commands![$command]) diff --git a/src/library/style.rs b/src/library/style.rs index 0615c0e7b..a63166cff 100644 --- a/src/library/style.rs +++ b/src/library/style.rs @@ -18,16 +18,16 @@ macro_rules! stylefunc { } layout(this, ctx) { - let mut style = ctx.style.clone(); + let mut style = ctx.text_style.clone(); style.toggle_class(FontClass::$ident); Ok(match &this.body { Some(body) => commands![ - SetStyle(style), + SetTextStyle(style), LayoutTree(body), - SetStyle(ctx.style.clone()), + SetTextStyle(ctx.text_style.clone()), ], - None => commands![SetStyle(style)] + None => commands![SetTextStyle(style)] }) } } diff --git a/src/style.rs b/src/style.rs index e2ab09377..da190b46a 100644 --- a/src/style.rs +++ b/src/style.rs @@ -74,7 +74,7 @@ impl Default for TextStyle { } /// Defines the size and margins of a page. -#[derive(Debug, Clone)] +#[derive(Debug, Copy, Clone)] pub struct PageStyle { /// The width and height of the page. pub dimensions: Size2D, diff --git a/tests/layouting.rs b/tests/layouting.rs index 27999d43b..933335cc4 100644 --- a/tests/layouting.rs +++ b/tests/layouting.rs @@ -9,8 +9,6 @@ use regex::{Regex, Captures}; use typst::export::pdf::PdfExporter; use typst::layout::LayoutAction; use typst::toddle::query::FileSystemFontProvider; -use typst::size::{Size, Size2D, SizeBox}; -use typst::style::PageStyle; use typst::Typesetter; const CACHE_DIR: &str = "tests/cache"; @@ -62,19 +60,12 @@ fn main() { fn test(name: &str, src: &str) { println!("Testing: {}.", name); - let (src, size) = preprocess(src); + let src = preprocess(src); let mut typesetter = Typesetter::new(); let provider = FileSystemFontProvider::from_listing("fonts/fonts.toml").unwrap(); typesetter.add_font_provider(provider.clone()); - if let Some(dimensions) = size { - typesetter.set_page_style(PageStyle { - dimensions, - margins: SizeBox::zero() - }); - } - // Make run warm. #[cfg(not(debug_assertions))] let warmup_start = Instant::now(); #[cfg(not(debug_assertions))] typesetter.typeset(&src).unwrap(); @@ -138,24 +129,11 @@ fn test(name: &str, src: &str) { exporter.export(&layouts, typesetter.loader(), file).unwrap(); } -fn preprocess<'a>(src: &'a str) -> (String, Option) { +fn preprocess<'a>(src: &'a str) -> String { let include_regex = Regex::new(r"\{include:((.|\.|\-)*)\}").unwrap(); let lorem_regex = Regex::new(r"\{lorem:(\d*)\}").unwrap(); - let size_regex = Regex::new(r"\{(size:(([\d\w]*)\sx\s([\d\w]*)))\}").unwrap(); - let mut size = None; - - let mut preprocessed = size_regex.replace_all(&src, |cap: &Captures| { - let width_str = cap.get(3).unwrap().as_str(); - let height_str = cap.get(4).unwrap().as_str(); - - let width = width_str.parse::().unwrap(); - let height = height_str.parse::().unwrap(); - - size = Some(Size2D::new(width, height)); - - "".to_string() - }).to_string(); + let mut preprocessed = src.to_string(); let mut changed = true; while changed { @@ -179,7 +157,7 @@ fn preprocess<'a>(src: &'a str) -> (String, Option) { generate_lorem(num_words) }).to_string(); - (preprocessed, size) + preprocessed } fn generate_lorem(num_words: usize) -> String { diff --git a/tests/layouts/align.typ b/tests/layouts/align.typ index e993a43b3..34c2f16fe 100644 --- a/tests/layouts/align.typ +++ b/tests/layouts/align.typ @@ -1,4 +1,5 @@ -{size:150pt x 215pt} +[page.size: width=150pt, height=215pt] +[page.margins: 0pt] // ---------------------------------- // // Without newline in between. diff --git a/tests/layouts/coma.typ b/tests/layouts/coma.typ index e9fb09947..237b24f45 100644 --- a/tests/layouts/coma.typ +++ b/tests/layouts/coma.typ @@ -1,4 +1,5 @@ -{size:420pt x 300pt} +[page.size: width=450pt, height=300pt] +[page.margins: 1cm] [box][ *Technical University Berlin* [n] diff --git a/tests/layouts/pagebreaks.typ b/tests/layouts/pagebreaks.typ index 6252ecb93..4d3c1843e 100644 --- a/tests/layouts/pagebreaks.typ +++ b/tests/layouts/pagebreaks.typ @@ -1,4 +1,6 @@ -{size:150pt x 200pt} +[page.size: width=150pt, height=200pt] +[page.margins: 0pt] + {lorem:100} [page.break] diff --git a/tests/layouts/styles.typ b/tests/layouts/styles.typ index 25057d8a8..637450268 100644 --- a/tests/layouts/styles.typ +++ b/tests/layouts/styles.typ @@ -1,4 +1,5 @@ -{size:250pt x 500pt} +[page.size: width=250pt, height=300pt] +[page.margins: 10pt] _Emoji:_ Hello World! 🌍