diff --git a/Cargo.toml b/Cargo.toml index dab6a6a4a..5aa6ffa54 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,9 +7,9 @@ edition = "2018" [dependencies] pdf = { path = "../pdf" } opentype = { path = "../opentype" } -unicode-xid = "0.1.0" byteorder = "1" -smallvec = "0.6.9" +smallvec = "0.6.10" +unicode-xid = "0.1.0" [[bin]] name = "typst" diff --git a/src/layout/boxed.rs b/src/layout/boxed.rs index 2ee2dbdc7..89c641bcf 100644 --- a/src/layout/boxed.rs +++ b/src/layout/boxed.rs @@ -3,7 +3,7 @@ use crate::doc::{Document, Page, TextAction}; use crate::font::Font; use crate::size::{Size, Size2D}; -use super::LayoutSpace; +use super::{ActionList, LayoutSpace, LayoutResult, LayoutError}; /// A box layout has a fixed width and height and composes of actions. @@ -40,7 +40,7 @@ pub struct BoxContext { #[derive(Debug)] pub struct BoxLayouter { ctx: BoxContext, - actions: Vec, + actions: ActionList, dimensions: Size2D, usable: Size2D, cursor: Size2D, @@ -52,7 +52,7 @@ impl BoxLayouter { let space = ctx.space; BoxLayouter { ctx, - actions: vec![], + actions: ActionList::new(), dimensions: Size2D::zero(), usable: space.usable(), cursor: Size2D::new(space.padding.left, space.padding.right), @@ -60,7 +60,7 @@ impl BoxLayouter { } /// Add a sublayout. - pub fn add_box(&mut self, layout: BoxLayout) { + pub fn add_box(&mut self, layout: BoxLayout) -> LayoutResult<()> { // In the flow direction (vertical) add the layout and in the second // direction just consider the maximal size of any child layout. let new = Size2D { @@ -68,34 +68,44 @@ impl BoxLayouter { y: self.dimensions.y + layout.dimensions.y, }; + // Check whether this box fits. if self.overflows(new) { - panic!("box layouter: would overflow in add_box"); + return Err(LayoutError::NotEnoughSpace); } - // Apply the dimensions because they fit. + // Apply the dimensions as they fit. + let height = layout.dimensions.y; self.dimensions = new; - // Move all actions into this layout and translate absolute positions. - self.actions.push(TextAction::MoveAbsolute(self.cursor)); - self.actions.extend(super::translate_actions(self.cursor, layout.actions)); + // Add the box. + self.add_box_absolute(self.cursor, layout); // Adjust the cursor. - self.cursor.y += layout.dimensions.y; + self.cursor.y += height; + + Ok(()) } /// Add some space in between two boxes. - pub fn add_space(&mut self, space: Size) { + pub fn add_space(&mut self, space: Size) -> LayoutResult<()> { + // Check whether this space fits. if self.overflows(self.dimensions + Size2D::with_y(space)) { - panic!("box layouter: would overflow in add_space"); + return Err(LayoutError::NotEnoughSpace); } + // Adjust the sizes. self.cursor.y += space; self.dimensions.y += space; + + Ok(()) } /// Add a sublayout at an absolute position. pub fn add_box_absolute(&mut self, position: Size2D, layout: BoxLayout) { - self.actions.push(TextAction::MoveAbsolute(position)); + // Move all actions into this layout and translate absolute positions. + self.actions.reset_origin(); + self.actions.add(TextAction::MoveAbsolute(position)); + self.actions.set_origin(position); self.actions.extend(layout.actions); } @@ -120,7 +130,7 @@ impl BoxLayouter { } else { self.ctx.space.dimensions }, - actions: self.actions, + actions: self.actions.into_vec(), } } diff --git a/src/layout/flex.rs b/src/layout/flex.rs index 924ebec50..5fd7b1579 100644 --- a/src/layout/flex.rs +++ b/src/layout/flex.rs @@ -2,7 +2,7 @@ use crate::doc::TextAction; use crate::size::Size2D; -use super::{LayoutSpace, BoxLayout}; +use super::{BoxLayout, ActionList, LayoutSpace, LayoutResult, LayoutError}; /// A flex layout consists of a yet unarranged list of boxes. @@ -54,7 +54,7 @@ impl FlexLayout { } /// Compute the justified layout. - pub fn into_box(self) -> BoxLayout { + pub fn into_box(self) -> LayoutResult { FlexFinisher::new(self).finish() } } @@ -73,7 +73,7 @@ pub struct FlexContext { struct FlexFinisher { units: Vec, ctx: FlexContext, - actions: Vec, + actions: ActionList, dimensions: Size2D, usable: Size2D, cursor: Size2D, @@ -87,7 +87,7 @@ impl FlexFinisher { FlexFinisher { units: layout.units, ctx: layout.ctx, - actions: vec![], + actions: ActionList::new(), dimensions: Size2D::zero(), usable: space.usable(), cursor: Size2D::new(space.padding.left, space.padding.top), @@ -96,7 +96,7 @@ impl FlexFinisher { } /// Finish the flex layout into the justified box layout. - fn finish(mut self) -> BoxLayout { + fn finish(mut self) -> LayoutResult { // Move the units out of the layout. let units = self.units; self.units = vec![]; @@ -104,7 +104,7 @@ impl FlexFinisher { // Arrange the units. for unit in units { match unit { - FlexUnit::Boxed(boxed) => self.boxed(boxed), + FlexUnit::Boxed(boxed) => self.boxed(boxed)?, FlexUnit::Glue(glue) => self.glue(glue), } } @@ -112,29 +112,31 @@ impl FlexFinisher { // Flush everything to get the correct dimensions. self.newline(); - BoxLayout { + Ok(BoxLayout { dimensions: if self.ctx.space.shrink_to_fit { self.dimensions.padded(self.ctx.space.padding) } else { self.ctx.space.dimensions }, - actions: self.actions, - } + actions: self.actions.into_vec(), + }) } /// Layout the box. - fn boxed(&mut self, boxed: BoxLayout) { + fn boxed(&mut self, boxed: BoxLayout) -> LayoutResult<()> { // Move to the next line if necessary. if self.line.x + boxed.dimensions.x > self.usable.x { // If it still does not fit, we stand no chance. if boxed.dimensions.x > self.usable.x { - panic!("flex layouter: box is to wide"); + return Err(LayoutError::NotEnoughSpace); } self.newline(); } self.append(boxed); + + Ok(()) } /// Layout the glue. @@ -150,8 +152,10 @@ impl FlexFinisher { /// 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.push(TextAction::MoveAbsolute(self.cursor)); - self.actions.extend(super::translate_actions(self.cursor, layout.actions)); + self.actions.reset_origin(); + self.actions.add(TextAction::MoveAbsolute(self.cursor)); + self.actions.set_origin(self.cursor); + self.actions.extend(layout.actions); // Adjust the sizes. self.line.x += layout.dimensions.x; diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 64eeb00a2..f8819d0be 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -124,9 +124,9 @@ impl<'a, 'p> Layouter<'a, 'p> { // Then start a new flex layouting process. Node::Newline => { // Finish the current paragraph into a box and add it. - self.add_paragraph_spacing(); - let boxed = self.flex_layout.into_box(); - self.box_layouter.add_box(boxed); + self.add_paragraph_spacing()?; + let boxed = self.flex_layout.into_box()?; + self.box_layouter.add_box(boxed)?; // Create a fresh flex layout for the next paragraph. self.flex_ctx.space.dimensions = self.box_layouter.remaining(); @@ -144,48 +144,87 @@ impl<'a, 'p> Layouter<'a, 'p> { // If there are remainings, add them to the layout. if !self.flex_layout.is_empty() { - self.add_paragraph_spacing(); - let boxed = self.flex_layout.into_box(); - self.box_layouter.add_box(boxed); + self.add_paragraph_spacing()?; + let boxed = self.flex_layout.into_box()?; + self.box_layouter.add_box(boxed)?; } Ok(self.box_layouter.finish()) } /// Add the spacing between two paragraphs. - fn add_paragraph_spacing(&mut self) { + fn add_paragraph_spacing(&mut self) -> LayoutResult<()> { let size = Size::points(self.text_ctx.style.font_size) * (self.text_ctx.style.line_spacing * self.text_ctx.style.paragraph_spacing - 1.0); - self.box_layouter.add_space(size); + self.box_layouter.add_space(size) } } -/// Translate a stream of text actions by an offset. -pub fn translate_actions(offset: Size2D, actions: I) -> TranslatedActions - where I: IntoIterator { - TranslatedActions { offset, iter: actions.into_iter() } +/// Manipulates and optimizes a list of actions. +#[derive(Debug, Clone)] +pub struct ActionList { + actions: Vec, + origin: Size2D, + active_font: (usize, f32), } -/// An iterator over the translated text actions, created by [`translate_actions`]. -pub struct TranslatedActions where I: Iterator { - offset: Size2D, - iter: I, -} +impl ActionList { + /// Create a new action list. + pub fn new() -> ActionList { + ActionList { + actions: vec![], + origin: Size2D::zero(), + active_font: (std::usize::MAX, 0.0), + } + } -impl Iterator for TranslatedActions where I: Iterator { - type Item = TextAction; - - fn next(&mut self) -> Option { + /// Add an action to the list if it is not useless + /// (like changing to a font that is already active). + pub fn add(&mut self, action: TextAction) { use TextAction::*; - self.iter.next().map(|action| match action { - MoveAbsolute(pos) => MoveAbsolute(pos + self.offset), - a => a, - }) + match action { + MoveAbsolute(pos) => self.actions.push(MoveAbsolute(self.origin + pos)), + SetFont(index, size) => if (index, size) != self.active_font { + self.active_font = (index, size); + self.actions.push(action); + }, + _ => self.actions.push(action), + } + } + + /// Add a series of actions. + pub fn extend(&mut self, actions: I) where I: IntoIterator { + for action in actions.into_iter() { + self.add(action); + } + } + + /// 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(); + } + + /// Whether there are any actions in this list. + pub fn is_empty(&self) -> bool { + self.actions.is_empty() + } + + /// Return the list of actions as a vector. + pub fn into_vec(self) -> Vec { + self.actions } } /// The error type for layouting. pub enum LayoutError { + /// There is not enough space to add an item. + NotEnoughSpace, /// There was no suitable font for the given character. NoSuitableFont(char), /// An error occured while gathering font data. @@ -198,6 +237,7 @@ pub type LayoutResult = Result; error_type! { err: LayoutError, show: f => match err { + LayoutError::NotEnoughSpace => write!(f, "not enough space"), LayoutError::NoSuitableFont(c) => write!(f, "no suitable font for '{}'", c), LayoutError::Font(err) => write!(f, "font error: {}", err), },