diff --git a/src/export/pdf.rs b/src/export/pdf.rs index 716651497..84023c71b 100644 --- a/src/export/pdf.rs +++ b/src/export/pdf.rs @@ -15,7 +15,7 @@ use fontdock::FaceId; use ttf_parser::{name_id, GlyphId}; use crate::SharedFontLoader; -use crate::layout::{MultiLayout, Layout}; +use crate::layout::{MultiLayout, BoxLayout}; use crate::layout::elements::LayoutElement; use crate::length::Length; @@ -117,8 +117,8 @@ impl<'a, W: Write> PdfExporter<'a, W> { let rect = Rect::new( 0.0, 0.0, - Length::raw(page.dimensions.x).as_pt() as f32, - Length::raw(page.dimensions.y).as_pt() as f32, + Length::raw(page.size.x).as_pt() as f32, + Length::raw(page.size.y).as_pt() as f32, ); self.writer.write_obj( @@ -141,7 +141,7 @@ impl<'a, W: Write> PdfExporter<'a, W> { } /// Write the content of a page. - fn write_page(&mut self, id: u32, page: &Layout) -> io::Result<()> { + fn write_page(&mut self, id: u32, page: &BoxLayout) -> io::Result<()> { // Moves and face switches are always cached and only flushed once // needed. let mut text = Text::new(); @@ -161,7 +161,7 @@ impl<'a, W: Write> PdfExporter<'a, W> { } let x = Length::raw(pos.x).as_pt(); - let y = Length::raw(page.dimensions.y - pos.y - size).as_pt(); + let y = Length::raw(page.size.y - pos.y - size).as_pt(); text.tm(1.0, 0.0, 0.0, 1.0, x as f32, y as f32); text.tj(shaped.encode_glyphs()); } diff --git a/src/func.rs b/src/func.rs index d3bc13cc7..afc796ae3 100644 --- a/src/func.rs +++ b/src/func.rs @@ -11,7 +11,7 @@ pub mod prelude { pub use crate::layout::Command::{self, *}; pub use crate::style::{LayoutStyle, PageStyle, TextStyle}; pub use crate::syntax::expr::*; - pub use crate::syntax::model::SyntaxModel; + pub use crate::syntax::tree::SyntaxTree; pub use crate::syntax::span::{Span, Spanned}; pub use crate::syntax::value::*; pub use super::OptionExt; @@ -132,11 +132,11 @@ macro_rules! function { }; (@layout($name:ident) layout($this:ident, $ctx:ident, $feedback:ident) $code:block) => { - impl $crate::syntax::model::Model for $name { + impl $crate::layout::Layout for $name { fn layout<'a, 'b, 't>( #[allow(unused)] &'a $this, #[allow(unused)] mut $ctx: $crate::layout::LayoutContext<'b>, - ) -> $crate::layout::DynFuture<'t, $crate::Pass<$crate::layout::Commands<'a>>> + ) -> $crate::DynFuture<'t, $crate::Pass<$crate::layout::Commands<'a>>> where 'a: 't, 'b: 't, diff --git a/src/layout/elements.rs b/src/layout/elements.rs index e524e1fd2..92b53ae89 100644 --- a/src/layout/elements.rs +++ b/src/layout/elements.rs @@ -35,8 +35,7 @@ impl Default for LayoutElements { } } -/// A layouting action, which is the basic building block layouts are composed -/// of. +/// A layout element, which is the basic building block layouts are composed of. #[derive(Debug, Clone, PartialEq)] pub enum LayoutElement { /// Shaped text. diff --git a/src/layout/line.rs b/src/layout/line.rs index 6b2fd3c62..358d2ac91 100644 --- a/src/layout/line.rs +++ b/src/layout/line.rs @@ -45,7 +45,7 @@ pub struct LineContext { #[derive(Debug)] struct LineRun { /// The so-far accumulated layouts in the line. - layouts: Vec<(f64, Layout)>, + layouts: Vec<(f64, BoxLayout)>, /// The width and maximal height of the line. size: Size, /// The alignment of all layouts in the line. @@ -77,7 +77,7 @@ impl LineLayouter { } /// Add a layout to the run. - pub fn add(&mut self, layout: Layout) { + pub fn add(&mut self, layout: BoxLayout) { let axes = self.ctx.axes; if let Some(align) = self.run.align { @@ -116,7 +116,7 @@ impl LineLayouter { self.add_primary_spacing(spacing, SpacingKind::Hard); } - let size = layout.dimensions.generalized(axes); + let size = layout.size.generalized(axes); if !self.usable().fits(size) { if !self.line_is_empty() { @@ -125,7 +125,7 @@ impl LineLayouter { // TODO: Issue warning about overflow if there is overflow. if !self.usable().fits(size) { - self.stack.skip_to_fitting_space(layout.dimensions); + self.stack.skip_to_fitting_space(layout.size); } } @@ -222,7 +222,7 @@ impl LineLayouter { /// a function how much space it has to layout itself. pub fn remaining(&self) -> LayoutSpaces { let mut spaces = self.stack.remaining(); - *spaces[0].dimensions.secondary_mut(self.ctx.axes) + *spaces[0].size.secondary_mut(self.ctx.axes) -= self.run.size.y; spaces } @@ -256,15 +256,15 @@ impl LineLayouter { true => offset, false => self.run.size.x - offset - - layout.dimensions.primary(self.ctx.axes), + - layout.size.primary(self.ctx.axes), }; let pos = Size::with_x(x); elements.extend_offset(pos, layout.elements); } - self.stack.add(Layout { - dimensions: self.run.size.specialized(self.ctx.axes), + self.stack.add(BoxLayout { + size: self.run.size.specialized(self.ctx.axes), align: self.run.align .unwrap_or(LayoutAlign::new(Start, Start)), elements diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 41a314f01..143f1984d 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -1,57 +1,100 @@ //! Layouting types and engines. -use std::fmt::{self, Display, Formatter}; +use async_trait::async_trait; +use crate::Pass; +use crate::font::SharedFontLoader; use crate::geom::{Size, Margins}; +use crate::style::{LayoutStyle, TextStyle, PageStyle}; +use crate::syntax::tree::SyntaxTree; + use elements::LayoutElements; +use tree::TreeLayouter; use prelude::*; +pub mod elements; pub mod line; +pub mod primitive; pub mod stack; pub mod text; -pub mod elements; -pub_use_mod!(model); +pub mod tree; + +pub use primitive::*; /// Basic types used across the layouting engine. pub mod prelude { - pub use super::{ - layout, LayoutContext, LayoutSpace, Command, Commands, - LayoutAxes, LayoutAlign, LayoutExpansion, - }; - pub use super::Dir::{self, *}; - pub use super::GenAxis::{self, *}; - pub use super::SpecAxis::{self, *}; - pub use super::GenAlign::{self, *}; - pub use super::SpecAlign::{self, *}; + pub use super::layout; + pub use super::primitive::*; + pub use Dir::*; + pub use GenAxis::*; + pub use SpecAxis::*; + pub use GenAlign::*; + pub use SpecAlign::*; } /// A collection of layouts. -pub type MultiLayout = Vec; +pub type MultiLayout = Vec; /// A finished box with content at fixed positions. #[derive(Debug, Clone, PartialEq)] -pub struct Layout { +pub struct BoxLayout { /// The size of the box. - pub dimensions: Size, + pub size: Size, /// How to align this layout in a parent container. pub align: LayoutAlign, - /// The actions composing this layout. + /// The elements composing this layout. pub elements: LayoutElements, } -/// A vector of layout spaces, that is stack allocated as long as it only -/// contains at most 2 spaces. +/// Layouting of elements. +#[async_trait(?Send)] +pub trait Layout { + /// Layout self into a sequence of layouting commands. + async fn layout<'a>(&'a self, _: LayoutContext<'_>) -> Pass>; +} + +/// Layout a syntax tree into a list of boxes. +pub async fn layout(tree: &SyntaxTree, ctx: LayoutContext<'_>) -> Pass { + let mut layouter = TreeLayouter::new(ctx); + layouter.layout_tree(tree).await; + layouter.finish() +} + +/// The context for layouting. +#[derive(Debug, Clone)] +pub struct LayoutContext<'a> { + /// The font loader to retrieve fonts from when typesetting text + /// using [`layout_text`]. + pub loader: &'a SharedFontLoader, + /// The style for pages and text. + pub style: &'a LayoutStyle, + /// The base unpadded size of this container (for relative sizing). + pub base: Size, + /// The spaces to layout in. + pub spaces: LayoutSpaces, + /// Whether to have repeated spaces or to use only the first and only once. + pub repeat: bool, + /// The initial axes along which content is laid out. + pub axes: LayoutAxes, + /// The alignment of the finished layout. + pub align: LayoutAlign, + /// Whether the layout that is to be created will be nested in a parent + /// container. + pub nested: bool, +} + +/// A collection of layout spaces. pub type LayoutSpaces = Vec; /// The space into which content is laid out. #[derive(Debug, Copy, Clone, PartialEq)] pub struct LayoutSpace { /// The maximum size of the box to layout in. - pub dimensions: Size, + pub size: Size, /// Padding that should be respected on each side. pub padding: Margins, - /// Whether to expand the dimensions of the resulting layout to the full - /// dimensions of this space or to shrink them to fit the content. + /// Whether to expand the size of the resulting layout to the full size of + /// this space or to shrink them to fit the content. pub expansion: LayoutExpansion, } @@ -62,363 +105,63 @@ impl LayoutSpace { Size::new(self.padding.left, self.padding.top) } - /// The actually usable area (dimensions minus padding). + /// The actually usable area (size minus padding). pub fn usable(&self) -> Size { - self.dimensions.unpadded(self.padding) + self.size.unpadded(self.padding) } - /// A layout space without padding and dimensions reduced by the padding. + /// A layout space without padding and size reduced by the padding. pub fn usable_space(&self) -> LayoutSpace { LayoutSpace { - dimensions: self.usable(), + size: self.usable(), padding: Margins::ZERO, expansion: LayoutExpansion::new(false, false), } } } -/// Specifies along which axes content is laid out. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub struct LayoutAxes { - /// The primary layouting direction. - pub primary: Dir, - /// The secondary layouting direction. - pub secondary: Dir, -} +/// A sequence of layouting commands. +pub type Commands<'a> = Vec>; -impl LayoutAxes { - /// Create a new instance from the two values. +/// Commands issued to the layouting engine by trees. +#[derive(Debug, Clone)] +pub enum Command<'a> { + /// Layout the given tree in the current context (i.e. not nested). The + /// content of the tree is not laid out into a separate box and then added, + /// but simply laid out flat in the active layouting process. /// - /// # Panics - /// This function panics if the axes are aligned, that is, they are - /// on the same axis. - pub fn new(primary: Dir, secondary: Dir) -> LayoutAxes { - if primary.axis() == secondary.axis() { - panic!("invalid aligned axes {} and {}", primary, secondary); - } + /// This has the effect that the content fits nicely into the active line + /// layouting, enabling functions to e.g. change the style of some piece of + /// text while keeping it integrated in the current paragraph. + LayoutSyntaxTree(&'a SyntaxTree), - LayoutAxes { primary, secondary } - } + /// Add a already computed layout. + Add(BoxLayout), + /// Add multiple layouts, one after another. This is equivalent to multiple + /// [Add](Command::Add) commands. + AddMultiple(MultiLayout), - /// Return the direction of the specified generic axis. - pub fn get(self, axis: GenAxis) -> Dir { - match axis { - Primary => self.primary, - Secondary => self.secondary, - } - } + /// Add spacing of given [kind](super::SpacingKind) along the primary or + /// secondary axis. The spacing kind defines how the spacing interacts with + /// surrounding spacing. + AddSpacing(f64, SpacingKind, GenAxis), - /// Borrow the direction of the specified generic axis mutably. - pub fn get_mut(&mut self, axis: GenAxis) -> &mut Dir { - match axis { - Primary => &mut self.primary, - Secondary => &mut self.secondary, - } - } -} - -/// Directions along which content is laid out. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum Dir { - LTT, - RTL, - TTB, - BTT, -} - -impl Dir { - /// The specific axis this direction belongs to. - pub fn axis(self) -> SpecAxis { - match self { - LTT | RTL => Horizontal, - TTB | BTT => Vertical, - } - } - - /// Whether this axis points into the positive coordinate direction. - /// - /// The positive axes are left-to-right and top-to-bottom. - pub fn is_positive(self) -> bool { - match self { - LTT | TTB => true, - RTL | BTT => false, - } - } - - /// The factor for this direction. - /// - /// - `1` if the direction is positive. - /// - `-1` if the direction is negative. - pub fn factor(self) -> f64 { - if self.is_positive() { 1.0 } else { -1.0 } - } - - /// The inverse axis. - pub fn inv(self) -> Dir { - match self { - LTT => RTL, - RTL => LTT, - TTB => BTT, - BTT => TTB, - } - } -} - -impl Display for Dir { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.pad(match self { - LTT => "ltr", - RTL => "rtl", - TTB => "ttb", - BTT => "btt", - }) - } -} - -/// The two generic layouting axes. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum GenAxis { - /// The primary axis along which words are laid out. - Primary, - /// The secondary axis along which lines and paragraphs are laid out. - Secondary, -} - -impl GenAxis { - /// The specific version of this axis in the given system of axes. - pub fn to_specific(self, axes: LayoutAxes) -> SpecAxis { - axes.get(self).axis() - } -} - -impl Display for GenAxis { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.pad(match self { - Primary => "primary", - Secondary => "secondary", - }) - } -} - -/// The two specific layouting axes. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum SpecAxis { - /// The horizontal layouting axis. - Horizontal, - /// The vertical layouting axis. - Vertical, -} - -impl SpecAxis { - /// The generic version of this axis in the given system of axes. - pub fn to_generic(self, axes: LayoutAxes) -> GenAxis { - if self == axes.primary.axis() { Primary } else { Secondary } - } -} - -impl Display for SpecAxis { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.pad(match self { - Horizontal => "horizontal", - Vertical => "vertical", - }) - } -} - -/// Specifies where to align a layout in a parent container. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub struct LayoutAlign { - /// The alignment along the primary axis. - pub primary: GenAlign, - /// The alignment along the secondary axis. - pub secondary: GenAlign, -} - -impl LayoutAlign { - /// Create a new instance from the two values. - pub fn new(primary: GenAlign, secondary: GenAlign) -> LayoutAlign { - LayoutAlign { primary, secondary } - } - - /// Return the alignment of the specified generic axis. - pub fn get(self, axis: GenAxis) -> GenAlign { - match axis { - Primary => self.primary, - Secondary => self.secondary, - } - } - - /// Borrow the alignment of the specified generic axis mutably. - pub fn get_mut(&mut self, axis: GenAxis) -> &mut GenAlign { - match axis { - Primary => &mut self.primary, - Secondary => &mut self.secondary, - } - } -} - -/// Where to align content along a generic context. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub enum GenAlign { - Start, - Center, - End, -} - -impl GenAlign { - /// The inverse alignment. - pub fn inv(self) -> GenAlign { - match self { - Start => End, - Center => Center, - End => Start, - } - } -} - -impl Display for GenAlign { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.pad(match self { - Start => "start", - Center => "center", - End => "end", - }) - } -} - -/// Where to align content in a specific context. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub enum SpecAlign { - Left, - Right, - Top, - Bottom, - Center, -} - -impl SpecAlign { - /// The specific axis this alignment refers to. - /// - /// Returns `None` if this is center. - pub fn axis(self) -> Option { - match self { - Self::Left => Some(Horizontal), - Self::Right => Some(Horizontal), - Self::Top => Some(Vertical), - Self::Bottom => Some(Vertical), - Self::Center => None, - } - } - - /// Convert this to a generic alignment. - pub fn to_generic(self, axes: LayoutAxes) -> GenAlign { - let get = |spec: SpecAxis, align: GenAlign| { - let axis = spec.to_generic(axes); - if axes.get(axis).is_positive() { align } else { align.inv() } - }; - - match self { - Self::Left => get(Horizontal, Start), - Self::Right => get(Horizontal, End), - Self::Top => get(Vertical, Start), - Self::Bottom => get(Vertical, End), - Self::Center => GenAlign::Center, - } - } -} - -impl Display for SpecAlign { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.pad(match self { - Self::Left => "left", - Self::Right => "right", - Self::Top => "top", - Self::Bottom => "bottom", - Self::Center => "center", - }) - } -} - -/// Specifies whether to expand a layout to the full size of the space it is -/// laid out in or to shrink it to fit the content. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub struct LayoutExpansion { - /// Whether to expand on the horizontal axis. - pub horizontal: bool, - /// Whether to expand on the vertical axis. - pub vertical: bool, -} - -impl LayoutExpansion { - /// Create a new instance from the two values. - pub fn new(horizontal: bool, vertical: bool) -> LayoutExpansion { - LayoutExpansion { horizontal, vertical } - } - - /// Return the expansion value for the given specific axis. - pub fn get(self, axis: SpecAxis) -> bool { - match axis { - Horizontal => self.horizontal, - Vertical => self.vertical, - } - } - - /// Borrow the expansion value for the given specific axis mutably. - pub fn get_mut(&mut self, axis: SpecAxis) -> &mut bool { - match axis { - Horizontal => &mut self.horizontal, - Vertical => &mut self.vertical, - } - } -} - -/// Defines how a given spacing interacts with (possibly existing) surrounding -/// spacing. -/// -/// There are two options for interaction: Hard and soft spacing. Typically, -/// hard spacing is used when a fixed amount of space needs to be inserted no -/// matter what. In contrast, soft spacing can be used to insert a default -/// spacing between e.g. two words or paragraphs that can still be overridden by -/// a hard space. -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub enum SpacingKind { - /// Hard spaces are always laid out and consume surrounding soft space. - Hard, - /// Soft spaces are not laid out if they are touching a hard space and - /// consume neighbouring soft spaces with higher levels. - Soft(u32), -} - -impl SpacingKind { - /// The standard spacing kind used for paragraph spacing. - pub const PARAGRAPH: SpacingKind = SpacingKind::Soft(1); - - /// The standard spacing kind used for line spacing. - pub const LINE: SpacingKind = SpacingKind::Soft(2); - - /// The standard spacing kind used for word spacing. - pub const WORD: SpacingKind = SpacingKind::Soft(1); -} - -/// The spacing kind of the most recently inserted item in a layouting process. -/// This is not about the last _spacing item_, but the last _item_, which is why -/// this can be `None`. -#[derive(Debug, Copy, Clone, PartialEq)] -enum LastSpacing { - /// The last item was hard spacing. - Hard, - /// The last item was soft spacing with the given width and level. - Soft(f64, u32), - /// The last item was not spacing. - None, -} - -impl LastSpacing { - /// The width of the soft space if this is a soft space or zero otherwise. - fn soft_or_zero(self) -> f64 { - match self { - LastSpacing::Soft(space, _) => space, - _ => 0.0, - } - } + /// Start a new line. + BreakLine, + /// Start a new paragraph. + BreakParagraph, + /// Start a new page, which will exist in the finished layout even if it + /// stays empty (since the page break is a _hard_ space break). + BreakPage, + + /// Update the text style. + SetTextStyle(TextStyle), + /// Update the page style. + SetPageStyle(PageStyle), + + /// Update the alignment for future boxes added to this layouting process. + SetAlignment(LayoutAlign), + /// Update the layouting axes along which future boxes will be laid + /// out. This finishes the current line. + SetAxes(LayoutAxes), } diff --git a/src/layout/model.rs b/src/layout/model.rs deleted file mode 100644 index db0698702..000000000 --- a/src/layout/model.rs +++ /dev/null @@ -1,313 +0,0 @@ -//! The model layouter layouts models (i.e. -//! [syntax models](crate::syntax::SyntaxModel) and [functions](crate::func)) -//! by executing commands issued by the models. - -use std::future::Future; -use std::pin::Pin; - -use crate::{Pass, Feedback}; -use crate::SharedFontLoader; -use crate::style::{LayoutStyle, PageStyle, TextStyle}; -use crate::geom::Size; -use crate::syntax::decoration::Decoration; -use crate::syntax::model::{Model, SyntaxModel, Node}; -use crate::syntax::span::{Span, Spanned}; -use super::line::{LineLayouter, LineContext}; -use super::text::{layout_text, TextContext}; -use super::*; - -/// Performs the model layouting. -#[derive(Debug)] -pub struct ModelLayouter<'a> { - ctx: LayoutContext<'a>, - layouter: LineLayouter, - style: LayoutStyle, - feedback: Feedback, -} - -/// The context for layouting. -#[derive(Debug, Clone)] -pub struct LayoutContext<'a> { - /// The font loader to retrieve fonts from when typesetting text - /// using [`layout_text`]. - pub loader: &'a SharedFontLoader, - /// The style for pages and text. - pub style: &'a LayoutStyle, - /// The base unpadded dimensions of this container (for relative sizing). - pub base: Size, - /// The spaces to layout in. - pub spaces: LayoutSpaces, - /// Whether to have repeated spaces or to use only the first and only once. - pub repeat: bool, - /// The initial axes along which content is laid out. - pub axes: LayoutAxes, - /// The alignment of the finished layout. - pub align: LayoutAlign, - /// Whether the layout that is to be created will be nested in a parent - /// container. - pub nested: bool, -} - -/// A sequence of layouting commands. -pub type Commands<'a> = Vec>; - -/// Commands issued to the layouting engine by models. -#[derive(Debug, Clone)] -pub enum Command<'a> { - /// Layout the given model in the current context (i.e. not nested). The - /// content of the model is not laid out into a separate box and then added, - /// but simply laid out flat in the active layouting process. - /// - /// This has the effect that the content fits nicely into the active line - /// layouting, enabling functions to e.g. change the style of some piece of - /// text while keeping it integrated in the current paragraph. - LayoutSyntaxModel(&'a SyntaxModel), - - /// Add a already computed layout. - Add(Layout), - /// Add multiple layouts, one after another. This is equivalent to multiple - /// [Add](Command::Add) commands. - AddMultiple(MultiLayout), - - /// Add spacing of given [kind](super::SpacingKind) along the primary or - /// secondary axis. The spacing kind defines how the spacing interacts with - /// surrounding spacing. - AddSpacing(f64, SpacingKind, GenAxis), - - /// Start a new line. - BreakLine, - /// Start a new paragraph. - BreakParagraph, - /// Start a new page, which will exist in the finished layout even if it - /// stays empty (since the page break is a _hard_ space break). - BreakPage, - - /// Update the text style. - SetTextStyle(TextStyle), - /// Update the page style. - SetPageStyle(PageStyle), - - /// Update the alignment for future boxes added to this layouting process. - SetAlignment(LayoutAlign), - /// Update the layouting axes along which future boxes will be laid - /// out. This finishes the current line. - SetAxes(LayoutAxes), -} - -/// Layout a syntax model into a list of boxes. -pub async fn layout(model: &SyntaxModel, ctx: LayoutContext<'_>) -> Pass { - let mut layouter = ModelLayouter::new(ctx); - layouter.layout_syntax_model(model).await; - layouter.finish() -} - -/// A dynamic future type which allows recursive invocation of async functions -/// when used as the return type. This is also how the async trait functions -/// work internally. -pub type DynFuture<'a, T> = Pin + 'a>>; - -impl<'a> ModelLayouter<'a> { - /// Create a new model layouter. - pub fn new(ctx: LayoutContext<'a>) -> ModelLayouter<'a> { - ModelLayouter { - layouter: LineLayouter::new(LineContext { - spaces: ctx.spaces.clone(), - axes: ctx.axes, - align: ctx.align, - repeat: ctx.repeat, - line_spacing: ctx.style.text.line_spacing(), - }), - style: ctx.style.clone(), - ctx, - feedback: Feedback::new(), - } - } - - /// Flatly layout a model into this layouting process. - pub async fn layout<'r>( - &'r mut self, - model: Spanned<&'r dyn Model> - ) { - // Execute the model's layout function which generates the commands. - let layouted = model.v.layout(LayoutContext { - style: &self.style, - spaces: self.layouter.remaining(), - nested: true, - .. self.ctx - }).await; - - // Add the errors generated by the model to the error list. - self.feedback.extend_offset(layouted.feedback, model.span.start); - - for command in layouted.output { - self.execute_command(command, model.span).await; - } - } - - /// Layout a syntax model by directly processing the nodes instead of using - /// the command based architecture. - pub async fn layout_syntax_model<'r>( - &'r mut self, - model: &'r SyntaxModel - ) { - use Node::*; - - for Spanned { v: node, span } in &model.nodes { - let decorate = |this: &mut ModelLayouter, deco| { - this.feedback.decorations.push(Spanned::new(deco, *span)); - }; - - match node { - Space => self.layout_space(), - Parbreak => self.layout_paragraph(), - Linebreak => self.layouter.finish_line(), - - Text(text) => { - if self.style.text.italic { - decorate(self, Decoration::Italic); - } - - if self.style.text.bolder { - decorate(self, Decoration::Bold); - } - - self.layout_text(text).await; - } - - ToggleItalic => { - self.style.text.italic = !self.style.text.italic; - decorate(self, Decoration::Italic); - } - - ToggleBolder => { - self.style.text.bolder = !self.style.text.bolder; - decorate(self, Decoration::Bold); - } - - Raw(lines) => { - // TODO: Make this more efficient. - let fallback = self.style.text.fallback.clone(); - self.style.text.fallback.list_mut().insert(0, "monospace".to_string()); - self.style.text.fallback.flatten(); - - // Layout the first line. - let mut iter = lines.iter(); - if let Some(line) = iter.next() { - self.layout_text(line).await; - } - - // Put a newline before each following line. - for line in iter { - self.layouter.finish_line(); - self.layout_text(line).await; - } - - self.style.text.fallback = fallback; - } - - Model(model) => { - self.layout(Spanned::new(model.as_ref(), *span)).await; - } - } - } - } - - /// Compute the finished list of boxes. - pub fn finish(self) -> Pass { - Pass::new(self.layouter.finish(), self.feedback) - } - - /// Execute a command issued by a model. When the command is errorful, the - /// given span is stored with the error. - fn execute_command<'r>( - &'r mut self, - command: Command<'r>, - model_span: Span, - ) -> DynFuture<'r, ()> { Box::pin(async move { - use Command::*; - - match command { - LayoutSyntaxModel(model) => self.layout_syntax_model(model).await, - - Add(layout) => self.layouter.add(layout), - AddMultiple(layouts) => self.layouter.add_multiple(layouts), - AddSpacing(space, kind, axis) => match axis { - Primary => self.layouter.add_primary_spacing(space, kind), - Secondary => self.layouter.add_secondary_spacing(space, kind), - } - - BreakLine => self.layouter.finish_line(), - BreakParagraph => self.layout_paragraph(), - BreakPage => { - if self.ctx.nested { - error!( - @self.feedback, model_span, - "page break cannot be issued from nested context", - ); - } else { - self.layouter.finish_space(true) - } - } - - SetTextStyle(style) => { - self.layouter.set_line_spacing(style.line_spacing()); - self.style.text = style; - } - SetPageStyle(style) => { - if self.ctx.nested { - error!( - @self.feedback, model_span, - "page style cannot be changed from nested context", - ); - } else { - self.style.page = style; - - // The line layouter has no idea of page styles and thus we - // need to recompute the layouting space resulting of the - // new page style and update it within the layouter. - let margins = style.margins(); - self.ctx.base = style.dimensions.unpadded(margins); - self.layouter.set_spaces(vec![ - LayoutSpace { - dimensions: style.dimensions, - padding: margins, - expansion: LayoutExpansion::new(true, true), - } - ], true); - } - } - - SetAlignment(align) => self.ctx.align = align, - SetAxes(axes) => { - self.layouter.set_axes(axes); - self.ctx.axes = axes; - } - } - }) } - - /// Layout a continous piece of text and add it to the line layouter. - async fn layout_text(&mut self, text: &str) { - self.layouter.add(layout_text(text, TextContext { - loader: &self.ctx.loader, - style: &self.style.text, - axes: self.ctx.axes, - align: self.ctx.align, - }).await) - } - - /// Add the spacing for a syntactic space node. - fn layout_space(&mut self) { - self.layouter.add_primary_spacing( - self.style.text.word_spacing(), - SpacingKind::WORD, - ); - } - - /// Finish the paragraph and add paragraph spacing. - fn layout_paragraph(&mut self) { - self.layouter.add_secondary_spacing( - self.style.text.paragraph_spacing(), - SpacingKind::PARAGRAPH, - ); - } -} diff --git a/src/layout/primitive.rs b/src/layout/primitive.rs new file mode 100644 index 000000000..2eb5669b3 --- /dev/null +++ b/src/layout/primitive.rs @@ -0,0 +1,350 @@ +//! Layouting primitives. + +use std::fmt::{self, Display, Formatter}; +use super::prelude::*; + +/// Specifies along which axes content is laid out. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct LayoutAxes { + /// The primary layouting direction. + pub primary: Dir, + /// The secondary layouting direction. + pub secondary: Dir, +} + +impl LayoutAxes { + /// Create a new instance from the two values. + /// + /// # Panics + /// This function panics if the axes are aligned, that is, they are + /// on the same axis. + pub fn new(primary: Dir, secondary: Dir) -> LayoutAxes { + if primary.axis() == secondary.axis() { + panic!("invalid aligned axes {} and {}", primary, secondary); + } + + LayoutAxes { primary, secondary } + } + + /// Return the direction of the specified generic axis. + pub fn get(self, axis: GenAxis) -> Dir { + match axis { + Primary => self.primary, + Secondary => self.secondary, + } + } + + /// Borrow the direction of the specified generic axis mutably. + pub fn get_mut(&mut self, axis: GenAxis) -> &mut Dir { + match axis { + Primary => &mut self.primary, + Secondary => &mut self.secondary, + } + } +} + +/// Directions along which content is laid out. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum Dir { + LTT, + RTL, + TTB, + BTT, +} + +impl Dir { + /// The specific axis this direction belongs to. + pub fn axis(self) -> SpecAxis { + match self { + LTT | RTL => Horizontal, + TTB | BTT => Vertical, + } + } + + /// Whether this axis points into the positive coordinate direction. + /// + /// The positive axes are left-to-right and top-to-bottom. + pub fn is_positive(self) -> bool { + match self { + LTT | TTB => true, + RTL | BTT => false, + } + } + + /// The factor for this direction. + /// + /// - `1` if the direction is positive. + /// - `-1` if the direction is negative. + pub fn factor(self) -> f64 { + if self.is_positive() { 1.0 } else { -1.0 } + } + + /// The inverse axis. + pub fn inv(self) -> Dir { + match self { + LTT => RTL, + RTL => LTT, + TTB => BTT, + BTT => TTB, + } + } +} + +impl Display for Dir { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad(match self { + LTT => "ltr", + RTL => "rtl", + TTB => "ttb", + BTT => "btt", + }) + } +} + +/// The two generic layouting axes. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum GenAxis { + /// The primary axis along which words are laid out. + Primary, + /// The secondary axis along which lines and paragraphs are laid out. + Secondary, +} + +impl GenAxis { + /// The specific version of this axis in the given system of axes. + pub fn to_specific(self, axes: LayoutAxes) -> SpecAxis { + axes.get(self).axis() + } +} + +impl Display for GenAxis { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad(match self { + Primary => "primary", + Secondary => "secondary", + }) + } +} + +/// The two specific layouting axes. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum SpecAxis { + /// The horizontal layouting axis. + Horizontal, + /// The vertical layouting axis. + Vertical, +} + +impl SpecAxis { + /// The generic version of this axis in the given system of axes. + pub fn to_generic(self, axes: LayoutAxes) -> GenAxis { + if self == axes.primary.axis() { Primary } else { Secondary } + } +} + +impl Display for SpecAxis { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad(match self { + Horizontal => "horizontal", + Vertical => "vertical", + }) + } +} + +/// Specifies where to align a layout in a parent container. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct LayoutAlign { + /// The alignment along the primary axis. + pub primary: GenAlign, + /// The alignment along the secondary axis. + pub secondary: GenAlign, +} + +impl LayoutAlign { + /// Create a new instance from the two values. + pub fn new(primary: GenAlign, secondary: GenAlign) -> LayoutAlign { + LayoutAlign { primary, secondary } + } + + /// Return the alignment of the specified generic axis. + pub fn get(self, axis: GenAxis) -> GenAlign { + match axis { + Primary => self.primary, + Secondary => self.secondary, + } + } + + /// Borrow the alignment of the specified generic axis mutably. + pub fn get_mut(&mut self, axis: GenAxis) -> &mut GenAlign { + match axis { + Primary => &mut self.primary, + Secondary => &mut self.secondary, + } + } +} + +/// Where to align content along a generic context. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub enum GenAlign { + Start, + Center, + End, +} + +impl GenAlign { + /// The inverse alignment. + pub fn inv(self) -> GenAlign { + match self { + Start => End, + Center => Center, + End => Start, + } + } +} + +impl Display for GenAlign { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad(match self { + Start => "start", + Center => "center", + End => "end", + }) + } +} + +/// Where to align content in a specific context. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub enum SpecAlign { + Left, + Right, + Top, + Bottom, + Center, +} + +impl SpecAlign { + /// The specific axis this alignment refers to. + /// + /// Returns `None` if this is center. + pub fn axis(self) -> Option { + match self { + Self::Left => Some(Horizontal), + Self::Right => Some(Horizontal), + Self::Top => Some(Vertical), + Self::Bottom => Some(Vertical), + Self::Center => None, + } + } + + /// Convert this to a generic alignment. + pub fn to_generic(self, axes: LayoutAxes) -> GenAlign { + let get = |spec: SpecAxis, align: GenAlign| { + let axis = spec.to_generic(axes); + if axes.get(axis).is_positive() { align } else { align.inv() } + }; + + match self { + Self::Left => get(Horizontal, Start), + Self::Right => get(Horizontal, End), + Self::Top => get(Vertical, Start), + Self::Bottom => get(Vertical, End), + Self::Center => GenAlign::Center, + } + } +} + +impl Display for SpecAlign { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad(match self { + Self::Left => "left", + Self::Right => "right", + Self::Top => "top", + Self::Bottom => "bottom", + Self::Center => "center", + }) + } +} + +/// Specifies whether to expand a layout to the full size of the space it is +/// laid out in or to shrink it to fit the content. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct LayoutExpansion { + /// Whether to expand on the horizontal axis. + pub horizontal: bool, + /// Whether to expand on the vertical axis. + pub vertical: bool, +} + +impl LayoutExpansion { + /// Create a new instance from the two values. + pub fn new(horizontal: bool, vertical: bool) -> LayoutExpansion { + LayoutExpansion { horizontal, vertical } + } + + /// Return the expansion value for the given specific axis. + pub fn get(self, axis: SpecAxis) -> bool { + match axis { + Horizontal => self.horizontal, + Vertical => self.vertical, + } + } + + /// Borrow the expansion value for the given specific axis mutably. + pub fn get_mut(&mut self, axis: SpecAxis) -> &mut bool { + match axis { + Horizontal => &mut self.horizontal, + Vertical => &mut self.vertical, + } + } +} + +/// Defines how a given spacing interacts with (possibly existing) surrounding +/// spacing. +/// +/// There are two options for interaction: Hard and soft spacing. Typically, +/// hard spacing is used when a fixed amount of space needs to be inserted no +/// matter what. In contrast, soft spacing can be used to insert a default +/// spacing between e.g. two words or paragraphs that can still be overridden by +/// a hard space. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum SpacingKind { + /// Hard spaces are always laid out and consume surrounding soft space. + Hard, + /// Soft spaces are not laid out if they are touching a hard space and + /// consume neighbouring soft spaces with higher levels. + Soft(u32), +} + +impl SpacingKind { + /// The standard spacing kind used for paragraph spacing. + pub const PARAGRAPH: SpacingKind = SpacingKind::Soft(1); + + /// The standard spacing kind used for line spacing. + pub const LINE: SpacingKind = SpacingKind::Soft(2); + + /// The standard spacing kind used for word spacing. + pub const WORD: SpacingKind = SpacingKind::Soft(1); +} + +/// The spacing kind of the most recently inserted item in a layouting process. +/// This is not about the last _spacing item_, but the last _item_, which is why +/// this can be `None`. +#[derive(Debug, Copy, Clone, PartialEq)] +pub(crate) enum LastSpacing { + /// The last item was hard spacing. + Hard, + /// The last item was soft spacing with the given width and level. + Soft(f64, u32), + /// The last item was not spacing. + None, +} + +impl LastSpacing { + /// The width of the soft space if this is a soft space or zero otherwise. + pub(crate) fn soft_or_zero(self) -> f64 { + match self { + LastSpacing::Soft(space, _) => space, + _ => 0.0, + } + } +} diff --git a/src/layout/stack.rs b/src/layout/stack.rs index 4f4d3d8b1..28da74b7b 100644 --- a/src/layout/stack.rs +++ b/src/layout/stack.rs @@ -59,12 +59,12 @@ struct Space { /// Whether to add the layout for this space even if it would be empty. hard: bool, /// The so-far accumulated layouts. - layouts: Vec<(LayoutAxes, Layout)>, + layouts: Vec<(LayoutAxes, BoxLayout)>, /// The specialized size of this space. size: Size, /// The specialized remaining space. usable: Size, - /// The specialized extra-needed dimensions to affect the size at all. + /// The specialized extra-needed size to affect the size at all. extra: Size, /// The rulers of a space dictate which alignments for new boxes are still /// allowed and which require a new space to be started. @@ -85,7 +85,7 @@ impl StackLayouter { } /// Add a layout to the stack. - pub fn add(&mut self, layout: Layout) { + pub fn add(&mut self, layout: BoxLayout) { // If the alignment cannot be fitted in this space, finish it. // TODO: Issue warning for non-fitting alignment in // non-repeating context. @@ -101,12 +101,12 @@ impl StackLayouter { } // TODO: Issue warning about overflow if there is overflow. - if !self.space.usable.fits(layout.dimensions) && self.ctx.repeat { - self.skip_to_fitting_space(layout.dimensions); + if !self.space.usable.fits(layout.size) && self.ctx.repeat { + self.skip_to_fitting_space(layout.size); } // Change the usable space and size of the space. - self.update_metrics(layout.dimensions.generalized(self.ctx.axes)); + self.update_metrics(layout.size.generalized(self.ctx.axes)); // Add the box to the vector and remember that spacings are allowed // again. @@ -130,11 +130,11 @@ impl StackLayouter { SpacingKind::Hard => { // Reduce the spacing such that it definitely fits. spacing = spacing.min(self.space.usable.secondary(self.ctx.axes)); - let dimensions = Size::with_y(spacing); + let size = Size::with_y(spacing); - self.update_metrics(dimensions); - self.space.layouts.push((self.ctx.axes, Layout { - dimensions: dimensions.specialized(self.ctx.axes), + self.update_metrics(size); + self.space.layouts.push((self.ctx.axes, BoxLayout { + size: size.specialized(self.ctx.axes), align: LayoutAlign::new(Start, Start), elements: LayoutElements::new(), })); @@ -159,22 +159,22 @@ impl StackLayouter { } /// Update the size metrics to reflect that a layout or spacing with the - /// given generalized dimensions has been added. - fn update_metrics(&mut self, dimensions: Size) { + /// given generalized size has been added. + fn update_metrics(&mut self, added: Size) { let axes = self.ctx.axes; let mut size = self.space.size.generalized(axes); let mut extra = self.space.extra.generalized(axes); - size.x += (dimensions.x - extra.x).max(0.0); - size.y += (dimensions.y - extra.y).max(0.0); + size.x += (added.x - extra.x).max(0.0); + size.y += (added.y - extra.y).max(0.0); - extra.x = extra.x.max(dimensions.x); - extra.y = (extra.y - dimensions.y).max(0.0); + extra.x = extra.x.max(added.x); + extra.y = (extra.y - added.y).max(0.0); self.space.size = size.specialized(axes); self.space.extra = extra.specialized(axes); - *self.space.usable.secondary_mut(axes) -= dimensions.y; + *self.space.usable.secondary_mut(axes) -= added.y; } /// Update the rulers to account for the new layout. Returns true if a @@ -226,12 +226,12 @@ impl StackLayouter { } } - /// Move to the first space that can fit the given dimensions or do nothing + /// Move to the first space that can fit the given size or do nothing /// if no space is capable of that. - pub fn skip_to_fitting_space(&mut self, dimensions: Size) { + pub fn skip_to_fitting_space(&mut self, size: Size) { let start = self.next_space(); for (index, space) in self.ctx.spaces[start..].iter().enumerate() { - if space.usable().fits(dimensions) { + if space.usable().fits(size) { self.finish_space(true); self.start_space(start + index, true); return; @@ -242,10 +242,10 @@ impl StackLayouter { /// The remaining unpadded, unexpanding spaces. If a function is laid out /// into these spaces, it will fit into this stack. pub fn remaining(&self) -> LayoutSpaces { - let dimensions = self.usable(); + let size = self.usable(); let mut spaces = vec![LayoutSpace { - dimensions, + size, padding: Margins::ZERO, expansion: LayoutExpansion::new(false, false), }]; @@ -287,7 +287,7 @@ impl StackLayouter { let space = self.ctx.spaces[self.space.index]; // ------------------------------------------------------------------ // - // Step 1: Determine the full dimensions of the space. + // Step 1: Determine the full size of the space. // (Mostly done already while collecting the boxes, but here we // expand if necessary.) @@ -295,7 +295,7 @@ impl StackLayouter { if space.expansion.horizontal { self.space.size.x = usable.x; } if space.expansion.vertical { self.space.size.y = usable.y; } - let dimensions = self.space.size.padded(space.padding); + let size = self.space.size.padded(space.padding); // ------------------------------------------------------------------ // // Step 2: Forward pass. Create a bounding box for each layout in which @@ -323,7 +323,7 @@ impl StackLayouter { // the usable space for following layouts at it's origin by its // extent along the secondary axis. *bound.get_mut(axes.secondary, Start) - += axes.secondary.factor() * layout.dimensions.secondary(*axes); + += axes.secondary.factor() * layout.size.secondary(*axes); } // ------------------------------------------------------------------ // @@ -355,7 +355,7 @@ impl StackLayouter { -= axes.secondary.factor() * extent.y; // Then, we add this layout's secondary extent to the accumulator. - let size = layout.dimensions.generalized(*axes); + let size = layout.size.generalized(*axes); extent.x = extent.x.max(size.x); extent.y += size.y; } @@ -368,7 +368,7 @@ impl StackLayouter { let layouts = std::mem::take(&mut self.space.layouts); for ((axes, layout), bound) in layouts.into_iter().zip(bounds) { - let size = layout.dimensions.specialized(axes); + let size = layout.size.specialized(axes); let align = layout.align; // The space in which this layout is aligned is given by the @@ -383,8 +383,8 @@ impl StackLayouter { elements.extend_offset(pos, layout.elements); } - self.layouts.push(Layout { - dimensions, + self.layouts.push(BoxLayout { + size, align: self.ctx.align, elements, }); diff --git a/src/layout/text.rs b/src/layout/text.rs index 477099e2d..5c18cd328 100644 --- a/src/layout/text.rs +++ b/src/layout/text.rs @@ -40,7 +40,7 @@ pub struct TextContext<'a> { } /// Layouts text into a box. -pub async fn layout_text(text: &str, ctx: TextContext<'_>) -> Layout { +pub async fn layout_text(text: &str, ctx: TextContext<'_>) -> BoxLayout { TextLayouter::new(text, ctx).layout().await } @@ -58,7 +58,7 @@ impl<'a> TextLayouter<'a> { } /// Do the layouting. - async fn layout(mut self) -> Layout { + async fn layout(mut self) -> BoxLayout { // If the primary axis is negative, we layout the characters reversed. if self.ctx.axes.primary.is_positive() { for c in self.text.chars() { @@ -76,8 +76,8 @@ impl<'a> TextLayouter<'a> { self.elements.push(pos, LayoutElement::Text(self.shaped)); } - Layout { - dimensions: Size::new(self.width, self.ctx.style.font_size()), + BoxLayout { + size: Size::new(self.width, self.ctx.style.font_size()), align: self.ctx.align, elements: self.elements, } diff --git a/src/layout/tree.rs b/src/layout/tree.rs new file mode 100644 index 000000000..44c592115 --- /dev/null +++ b/src/layout/tree.rs @@ -0,0 +1,223 @@ +//! The tree layouter layouts trees (i.e. +//! [syntax trees](crate::syntax::SyntaxTree) and [functions](crate::func)) +//! by executing commands issued by the trees. + +use crate::{Pass, Feedback, DynFuture}; +use crate::style::LayoutStyle; +use crate::syntax::decoration::Decoration; +use crate::syntax::tree::{SyntaxTree, SyntaxNode, DynamicNode}; +use crate::syntax::span::{Span, Spanned}; +use super::line::{LineLayouter, LineContext}; +use super::text::{layout_text, TextContext}; +use super::*; + +/// Performs the tree layouting. +#[derive(Debug)] +pub struct TreeLayouter<'a> { + ctx: LayoutContext<'a>, + layouter: LineLayouter, + style: LayoutStyle, + feedback: Feedback, +} + +impl<'a> TreeLayouter<'a> { + /// Create a new tree layouter. + pub fn new(ctx: LayoutContext<'a>) -> TreeLayouter<'a> { + TreeLayouter { + layouter: LineLayouter::new(LineContext { + spaces: ctx.spaces.clone(), + axes: ctx.axes, + align: ctx.align, + repeat: ctx.repeat, + line_spacing: ctx.style.text.line_spacing(), + }), + style: ctx.style.clone(), + ctx, + feedback: Feedback::new(), + } + } + + /// Layout a syntax tree by directly processing the nodes instead of using + /// the command based architecture. + pub async fn layout_tree(&mut self, tree: &SyntaxTree) { + for node in tree { + self.layout_node(node).await; + } + } + + pub async fn layout_node(&mut self, node: &Spanned) { + let decorate = |this: &mut TreeLayouter, deco| { + this.feedback.decorations.push(Spanned::new(deco, node.span)); + }; + + match &node.v { + SyntaxNode::Space => self.layout_space(), + SyntaxNode::Parbreak => self.layout_paragraph(), + SyntaxNode::Linebreak => self.layouter.finish_line(), + + SyntaxNode::Text(text) => { + if self.style.text.italic { + decorate(self, Decoration::Italic); + } + + if self.style.text.bolder { + decorate(self, Decoration::Bold); + } + + self.layout_text(text).await; + } + + SyntaxNode::ToggleItalic => { + self.style.text.italic = !self.style.text.italic; + decorate(self, Decoration::Italic); + } + + SyntaxNode::ToggleBolder => { + self.style.text.bolder = !self.style.text.bolder; + decorate(self, Decoration::Bold); + } + + SyntaxNode::Raw(lines) => { + // TODO: Make this more efficient. + let fallback = self.style.text.fallback.clone(); + self.style.text.fallback.list_mut().insert(0, "monospace".to_string()); + self.style.text.fallback.flatten(); + + // Layout the first line. + let mut iter = lines.iter(); + if let Some(line) = iter.next() { + self.layout_text(line).await; + } + + // Put a newline before each following line. + for line in iter { + self.layouter.finish_line(); + self.layout_text(line).await; + } + + self.style.text.fallback = fallback; + } + + SyntaxNode::Dyn(dynamic) => { + self.layout_dyn(Spanned::new(dynamic.as_ref(), node.span)).await; + } + } + } + + /// Layout a node into this layouting process. + pub async fn layout_dyn(&mut self, dynamic: Spanned<&dyn DynamicNode>) { + // Execute the tree's layout function which generates the commands. + let layouted = dynamic.v.layout(LayoutContext { + style: &self.style, + spaces: self.layouter.remaining(), + nested: true, + .. self.ctx + }).await; + + // Add the errors generated by the tree to the error list. + self.feedback.extend_offset(layouted.feedback, dynamic.span.start); + + for command in layouted.output { + self.execute_command(command, dynamic.span).await; + } + } + + /// Compute the finished list of boxes. + pub fn finish(self) -> Pass { + Pass::new(self.layouter.finish(), self.feedback) + } + + /// Execute a command issued by a tree. When the command is errorful, the + /// given span is stored with the error. + fn execute_command<'r>( + &'r mut self, + command: Command<'r>, + tree_span: Span, + ) -> DynFuture<'r, ()> { Box::pin(async move { + use Command::*; + + match command { + LayoutSyntaxTree(tree) => self.layout_tree(tree).await, + + Add(layout) => self.layouter.add(layout), + AddMultiple(layouts) => self.layouter.add_multiple(layouts), + AddSpacing(space, kind, axis) => match axis { + Primary => self.layouter.add_primary_spacing(space, kind), + Secondary => self.layouter.add_secondary_spacing(space, kind), + } + + BreakLine => self.layouter.finish_line(), + BreakParagraph => self.layout_paragraph(), + BreakPage => { + if self.ctx.nested { + error!( + @self.feedback, tree_span, + "page break cannot be issued from nested context", + ); + } else { + self.layouter.finish_space(true) + } + } + + SetTextStyle(style) => { + self.layouter.set_line_spacing(style.line_spacing()); + self.style.text = style; + } + SetPageStyle(style) => { + if self.ctx.nested { + error!( + @self.feedback, tree_span, + "page style cannot be changed from nested context", + ); + } else { + self.style.page = style; + + // The line layouter has no idea of page styles and thus we + // need to recompute the layouting space resulting of the + // new page style and update it within the layouter. + let margins = style.margins(); + self.ctx.base = style.size.unpadded(margins); + self.layouter.set_spaces(vec![ + LayoutSpace { + size: style.size, + padding: margins, + expansion: LayoutExpansion::new(true, true), + } + ], true); + } + } + + SetAlignment(align) => self.ctx.align = align, + SetAxes(axes) => { + self.layouter.set_axes(axes); + self.ctx.axes = axes; + } + } + }) } + + /// Layout a continous piece of text and add it to the line layouter. + async fn layout_text(&mut self, text: &str) { + self.layouter.add(layout_text(text, TextContext { + loader: &self.ctx.loader, + style: &self.style.text, + axes: self.ctx.axes, + align: self.ctx.align, + }).await) + } + + /// Add the spacing for a syntactic space node. + fn layout_space(&mut self) { + self.layouter.add_primary_spacing( + self.style.text.word_spacing(), + SpacingKind::WORD, + ); + } + + /// Finish the paragraph and add paragraph spacing. + fn layout_paragraph(&mut self) { + self.layouter.add_secondary_spacing( + self.style.text.paragraph_spacing(), + SpacingKind::PARAGRAPH, + ); + } +} diff --git a/src/lib.rs b/src/lib.rs index 67b94adf3..5a0b8d0be 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,25 +16,19 @@ //! format is [_PDF_](crate::export::pdf). use std::fmt::Debug; +use std::future::Future; +use std::pin::Pin; use crate::diagnostic::Diagnostics; use crate::font::SharedFontLoader; use crate::layout::MultiLayout; use crate::style::{LayoutStyle, PageStyle, TextStyle}; use crate::syntax::decoration::Decorations; -use crate::syntax::model::SyntaxModel; +use crate::syntax::tree::SyntaxTree; use crate::syntax::parsing::{parse, ParseState}; use crate::syntax::scope::Scope; use crate::syntax::span::{Offset, Pos}; -/// Declare a module and reexport all its contents. -macro_rules! pub_use_mod { - ($name:ident) => { - mod $name; - pub use $name::*; - }; -} - #[macro_use] mod macros; #[macro_use] @@ -84,23 +78,24 @@ impl Typesetter { } /// Parse source code into a syntax tree. - pub fn parse(&self, src: &str) -> Pass { + pub fn parse(&self, src: &str) -> Pass { parse(src, Pos::ZERO, &self.parse_state) } /// Layout a syntax tree and return the produced layout. - pub async fn layout(&self, model: &SyntaxModel) -> Pass { + pub async fn layout(&self, tree: &SyntaxTree) -> Pass { use crate::layout::prelude::*; + use crate::layout::{LayoutContext, LayoutSpace}; let margins = self.style.page.margins(); - crate::layout::layout( - &model, + layout( + &tree, LayoutContext { loader: &self.loader, style: &self.style, - base: self.style.page.dimensions.unpadded(margins), + base: self.style.page.size.unpadded(margins), spaces: vec![LayoutSpace { - dimensions: self.style.page.dimensions, + size: self.style.page.size, padding: margins, expansion: LayoutExpansion::new(true, true), }], @@ -121,6 +116,11 @@ impl Typesetter { } } +/// A dynamic future type which allows recursive invocation of async functions +/// when used as the return type. This is also how the async trait functions +/// work internally. +pub type DynFuture<'a, T> = Pin + 'a>>; + /// The result of some pass: Some output `T` and feedback data. #[derive(Debug, Clone, Eq, PartialEq)] pub struct Pass { diff --git a/src/library/font.rs b/src/library/font.rs index a5ee7d9c6..efcbb86fa 100644 --- a/src/library/font.rs +++ b/src/library/font.rs @@ -1,3 +1,5 @@ +//! Font configuration. + use fontdock::{FontStyle, FontWeight, FontWidth}; use crate::length::ScaleLength; use super::*; @@ -6,7 +8,7 @@ function! { /// `font`: Configure the font. #[derive(Debug, Clone, PartialEq)] pub struct FontFunc { - body: Option, + body: Option, size: Option, style: Option, weight: Option, diff --git a/src/library/layout.rs b/src/library/layout.rs index 5dd754bb2..d46265a46 100644 --- a/src/library/layout.rs +++ b/src/library/layout.rs @@ -1,3 +1,5 @@ +//! Layout building blocks. + use crate::length::ScaleLength; use super::*; @@ -5,14 +7,14 @@ function! { /// `box`: Layouts content into a box. #[derive(Debug, Clone, PartialEq)] pub struct BoxFunc { - body: SyntaxModel, + body: SyntaxTree, width: Option, height: Option, } parse(header, body, ctx, f) { BoxFunc { - body: body!(opt: body, ctx, f).unwrap_or(SyntaxModel::new()), + body: body!(opt: body, ctx, f).unwrap_or(SyntaxTree::new()), width: header.args.key.get::("width", f), height: header.args.key.get::("height", f), } @@ -25,14 +27,14 @@ function! { self.width.with(|v| { let length = v.raw_scaled(ctx.base.x); ctx.base.x = length; - ctx.spaces[0].dimensions.x = length; + ctx.spaces[0].size.x = length; ctx.spaces[0].expansion.horizontal = true; }); self.height.with(|v| { let length = v.raw_scaled(ctx.base.y); ctx.base.y = length; - ctx.spaces[0].dimensions.y = length; + ctx.spaces[0].size.y = length; ctx.spaces[0].expansion.vertical = true; }); @@ -48,7 +50,7 @@ function! { /// `align`: Aligns content along the layouting axes. #[derive(Debug, Clone, PartialEq)] pub struct AlignFunc { - body: Option, + body: Option, aligns: Vec>, h: Option>, v: Option>, @@ -64,7 +66,7 @@ function! { } layout(self, ctx, f) { - ctx.base = ctx.spaces[0].dimensions; + ctx.base = ctx.spaces[0].size; let axes = ctx.axes; let all = self.aligns.iter() diff --git a/src/library/mod.rs b/src/library/mod.rs index 7b7034a07..7a664257a 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -1,12 +1,14 @@ //! The _Typst_ standard library. -use crate::syntax::scope::Scope; use crate::func::prelude::*; +use crate::layout::{LayoutContext, Commands}; +use crate::syntax::scope::Scope; -pub_use_mod!(font); -pub_use_mod!(layout); -pub_use_mod!(page); -pub_use_mod!(spacing); +macro_rules! lib { ($name:ident) => { mod $name; pub use $name::*; }} +lib!(font); +lib!(layout); +lib!(page); +lib!(spacing); /// Create a scope with all standard functions. pub fn std() -> Scope { @@ -17,10 +19,10 @@ pub fn std() -> Scope { std.add::("page"); std.add::("align"); std.add::("box"); - std.add_with_meta::("h", Horizontal); - std.add_with_meta::("v", Vertical); std.add::("parbreak"); std.add::("pagebreak"); + std.add_with_meta::("h", Horizontal); + std.add_with_meta::("v", Vertical); std } @@ -29,7 +31,7 @@ function! { /// `val`: Layouts the body with no special effect. #[derive(Debug, Clone, PartialEq)] pub struct ValFunc { - body: Option, + body: Option, } parse(header, body, state, f) { @@ -40,7 +42,7 @@ function! { layout(self, ctx, f) { match &self.body { - Some(model) => vec![LayoutSyntaxModel(model)], + Some(tree) => vec![LayoutSyntaxTree(tree)], None => vec![], } } @@ -48,7 +50,7 @@ function! { /// Layout an optional body with a change of the text style. fn styled<'a, T, F>( - body: &'a Option, + body: &'a Option, ctx: LayoutContext<'_>, data: Option, f: F, @@ -58,9 +60,9 @@ fn styled<'a, T, F>( f(&mut style, data); match body { - Some(model) => vec![ + Some(tree) => vec![ SetTextStyle(style), - LayoutSyntaxModel(model), + LayoutSyntaxTree(tree), SetTextStyle(ctx.style.text.clone()), ], None => vec![SetTextStyle(style)], diff --git a/src/library/page.rs b/src/library/page.rs index f1dcc9bca..d1964fd28 100644 --- a/src/library/page.rs +++ b/src/library/page.rs @@ -1,3 +1,5 @@ +//! Page setup. + use crate::length::{Length, ScaleLength}; use crate::paper::{Paper, PaperClass}; use super::*; @@ -37,13 +39,13 @@ function! { if let Some(paper) = self.paper { style.class = paper.class; - style.dimensions = paper.size(); + style.size = paper.size(); } else if self.width.is_some() || self.height.is_some() { style.class = PaperClass::Custom; } - self.width.with(|v| style.dimensions.x = v.as_raw()); - self.height.with(|v| style.dimensions.y = v.as_raw()); + self.width.with(|v| style.size.x = v.as_raw()); + self.height.with(|v| style.size.y = v.as_raw()); self.margins.with(|v| style.margins.set_all(Some(v))); self.left.with(|v| style.margins.left = Some(v)); self.right.with(|v| style.margins.right = Some(v)); @@ -51,7 +53,7 @@ function! { self.bottom.with(|v| style.margins.bottom = Some(v)); if self.flip { - style.dimensions.swap(); + style.size.swap(); } vec![SetPageStyle(style)] diff --git a/src/library/spacing.rs b/src/library/spacing.rs index 68ef27f23..22c4669e3 100644 --- a/src/library/spacing.rs +++ b/src/library/spacing.rs @@ -1,3 +1,5 @@ +//! Spacing. + use crate::length::ScaleLength; use crate::layout::SpacingKind; use super::*; diff --git a/src/style.rs b/src/style.rs index 0490ef07a..9dce381ec 100644 --- a/src/style.rs +++ b/src/style.rs @@ -97,7 +97,7 @@ pub struct PageStyle { /// The class of this page. pub class: PaperClass, /// The width and height of the page. - pub dimensions: Size, + pub size: Size, /// The amount of white space on each side. If a side is set to `None`, the /// default for the paper class is used. pub margins: Value4>, @@ -108,21 +108,20 @@ impl PageStyle { pub fn new(paper: Paper) -> PageStyle { PageStyle { class: paper.class, - dimensions: paper.size(), + size: paper.size(), margins: Value4::with_all(None), } } /// The absolute margins. pub fn margins(&self) -> Margins { - let dims = self.dimensions; + let size = self.size; let default = self.class.default_margins(); - Margins { - left: self.margins.left.unwrap_or(default.left).raw_scaled(dims.x), - top: self.margins.top.unwrap_or(default.top).raw_scaled(dims.y), - right: self.margins.right.unwrap_or(default.right).raw_scaled(dims.x), - bottom: self.margins.bottom.unwrap_or(default.bottom).raw_scaled(dims.y), + left: self.margins.left.unwrap_or(default.left).raw_scaled(size.x), + top: self.margins.top.unwrap_or(default.top).raw_scaled(size.y), + right: self.margins.right.unwrap_or(default.right).raw_scaled(size.x), + bottom: self.margins.bottom.unwrap_or(default.bottom).raw_scaled(size.y), } } } diff --git a/src/syntax/expr.rs b/src/syntax/expr.rs index a551c2b60..0a9ab1498 100644 --- a/src/syntax/expr.rs +++ b/src/syntax/expr.rs @@ -7,7 +7,7 @@ use std::u8; use crate::Feedback; use crate::length::Length; -use super::span::Spanned; +use super::span::{Spanned, SpanVec}; use super::tokens::is_identifier; use super::value::Value; @@ -237,7 +237,7 @@ impl fmt::Display for ParseColorError { /// (false, 12cm, "hi") /// ``` #[derive(Default, Clone, PartialEq)] -pub struct Tuple(pub Vec>); +pub struct Tuple(pub SpanVec); impl Tuple { /// Create an empty tuple. @@ -333,7 +333,7 @@ impl Deref for NamedTuple { /// { fit: false, width: 12cm, items: (1, 2, 3) } /// ``` #[derive(Default, Clone, PartialEq)] -pub struct Object(pub Vec>); +pub struct Object(pub SpanVec); /// A key-value pair in an object. #[derive(Debug, Clone, PartialEq)] diff --git a/src/syntax/mod.rs b/src/syntax/mod.rs index e844fdf1d..86c2fd248 100644 --- a/src/syntax/mod.rs +++ b/src/syntax/mod.rs @@ -1,4 +1,4 @@ -//! Syntax models, parsing and tokenization. +//! Syntax trees, parsing and tokenization. #[cfg(test)] #[macro_use] @@ -6,7 +6,7 @@ mod test; pub mod decoration; pub mod expr; -pub mod model; +pub mod tree; pub mod parsing; pub mod span; pub mod scope; diff --git a/src/syntax/model.rs b/src/syntax/model.rs deleted file mode 100644 index 4eb2abe02..000000000 --- a/src/syntax/model.rs +++ /dev/null @@ -1,134 +0,0 @@ -//! The syntax model. - -use std::any::Any; -use std::fmt::Debug; -use async_trait::async_trait; - -use crate::{Pass, Feedback}; -use crate::layout::{LayoutContext, Commands, Command}; -use super::span::{Spanned, SpanVec}; - -/// Represents a parsed piece of source that can be layouted and in the future -/// also be queried for information used for refactorings, autocomplete, etc. -#[async_trait(?Send)] -pub trait Model: Debug + ModelBounds { - /// Layout the model into a sequence of commands processed by a - /// [`ModelLayouter`](crate::layout::ModelLayouter). - async fn layout<'a>(&'a self, ctx: LayoutContext<'_>) -> Pass>; -} - -/// A tree representation of source code. -#[derive(Debug, Default, Clone, PartialEq)] -pub struct SyntaxModel { - /// The syntactical elements making up this model. - pub nodes: SpanVec, -} - -impl SyntaxModel { - /// Create an empty syntax model. - pub fn new() -> SyntaxModel { - SyntaxModel { nodes: vec![] } - } - - /// Add a node to the model. - pub fn add(&mut self, node: Spanned) { - self.nodes.push(node); - } -} - -#[async_trait(?Send)] -impl Model for SyntaxModel { - async fn layout<'a>(&'a self, _: LayoutContext<'_>) -> Pass> { - Pass::new(vec![Command::LayoutSyntaxModel(self)], Feedback::new()) - } -} - -/// A node in the [syntax model](SyntaxModel). -#[derive(Debug, Clone)] -pub enum Node { - /// Whitespace containing less than two newlines. - Space, - /// Whitespace with more than two newlines. - Parbreak, - /// A forced line break. - Linebreak, - /// Plain text. - Text(String), - /// Lines of raw text. - Raw(Vec), - /// Italics were enabled / disabled. - ToggleItalic, - /// Bolder was enabled / disabled. - ToggleBolder, - /// A submodel, typically a function invocation. - Model(Box), -} - -impl PartialEq for Node { - fn eq(&self, other: &Node) -> bool { - use Node::*; - match (self, other) { - (Space, Space) => true, - (Parbreak, Parbreak) => true, - (Linebreak, Linebreak) => true, - (Text(a), Text(b)) => a == b, - (Raw(a), Raw(b)) => a == b, - (ToggleItalic, ToggleItalic) => true, - (ToggleBolder, ToggleBolder) => true, - (Model(a), Model(b)) => a == b, - _ => false, - } - } -} - -impl dyn Model { - /// Downcast this model to a concrete type implementing [`Model`]. - pub fn downcast(&self) -> Option<&T> where T: Model + 'static { - self.as_any().downcast_ref::() - } -} - -impl PartialEq for dyn Model { - fn eq(&self, other: &dyn Model) -> bool { - self.bound_eq(other) - } -} - -impl Clone for Box { - fn clone(&self) -> Self { - self.bound_clone() - } -} - -/// This trait describes bounds necessary for types implementing [`Model`]. It is -/// automatically implemented for all types that are [`Model`], [`PartialEq`], -/// [`Clone`] and `'static`. -/// -/// It is necessary to make models comparable and clonable. -pub trait ModelBounds { - /// Convert into a `dyn Any`. - fn as_any(&self) -> &dyn Any; - - /// Check for equality with another model. - fn bound_eq(&self, other: &dyn Model) -> bool; - - /// Clone into a boxed model trait object. - fn bound_clone(&self) -> Box; -} - -impl ModelBounds for T where T: Model + PartialEq + Clone + 'static { - fn as_any(&self) -> &dyn Any { - self - } - - fn bound_eq(&self, other: &dyn Model) -> bool { - match other.as_any().downcast_ref::() { - Some(other) => self == other, - None => false, - } - } - - fn bound_clone(&self) -> Box { - Box::new(self.clone()) - } -} diff --git a/src/syntax/parsing.rs b/src/syntax/parsing.rs index 75e30177e..7594c14da 100644 --- a/src/syntax/parsing.rs +++ b/src/syntax/parsing.rs @@ -1,4 +1,4 @@ -//! Parsing of source code into syntax models. +//! Parsing of source code into syntax trees. use std::str::FromStr; @@ -8,10 +8,10 @@ use super::expr::*; use super::scope::Scope; use super::span::{Pos, Span, Spanned}; use super::tokens::{is_newline_char, Token, Tokens, TokenMode}; -use super::model::{SyntaxModel, Node, Model}; +use super::tree::{SyntaxTree, SyntaxNode, DynamicNode}; -/// A function which parses a function call into a model. -pub type CallParser = dyn Fn(FuncCall, &ParseState) -> Pass>; +/// A function which parses a function call into a tree. +pub type CallParser = dyn Fn(FuncCall, &ParseState) -> Pass>; /// An invocation of a function. #[derive(Debug, Clone, PartialEq)] @@ -73,12 +73,12 @@ pub struct ParseState { /// Parse a string of source code. /// -/// All spans in the resulting model and feedback are offset by the given +/// All spans in the resulting tree and feedback are offset by the given /// `offset` position. This is used to make spans of a function body relative to /// the start of the function as a whole as opposed to the start of the /// function's body. -pub fn parse(src: &str, offset: Pos, state: &ParseState) -> Pass { - let mut model = SyntaxModel::new(); +pub fn parse(src: &str, offset: Pos, state: &ParseState) -> Pass { + let mut tree = SyntaxTree::new(); let mut feedback = Feedback::new(); for token in Tokens::new(src, offset, TokenMode::Body) { @@ -87,9 +87,9 @@ pub fn parse(src: &str, offset: Pos, state: &ParseState) -> Pass { // Starting from two newlines counts as a paragraph break, a single // newline does not. Token::Space(newlines) => if newlines >= 2 { - Node::Parbreak + SyntaxNode::Parbreak } else { - Node::Space + SyntaxNode::Space } Token::Function { header, body, terminated } => { @@ -103,19 +103,19 @@ pub fn parse(src: &str, offset: Pos, state: &ParseState) -> Pass { parsed.output } - Token::Star => Node::ToggleBolder, - Token::Underscore => Node::ToggleItalic, - Token::Backslash => Node::Linebreak, + Token::Star => SyntaxNode::ToggleBolder, + Token::Underscore => SyntaxNode::ToggleItalic, + Token::Backslash => SyntaxNode::Linebreak, Token::Raw { raw, terminated } => { if !terminated { error!(@feedback, Span::at(span.end), "expected backtick"); } - Node::Raw(unescape_raw(raw)) + SyntaxNode::Raw(unescape_raw(raw)) } - Token::Text(text) => Node::Text(text.to_string()), + Token::Text(text) => SyntaxNode::Text(text.to_string()), Token::LineComment(_) | Token::BlockComment(_) => continue, unexpected => { @@ -124,10 +124,10 @@ pub fn parse(src: &str, offset: Pos, state: &ParseState) -> Pass { } }; - model.add(Spanned::new(node, span)); + tree.push(Spanned::new(node, span)); } - Pass::new(model, feedback) + Pass::new(tree, feedback) } struct FuncParser<'s> { @@ -164,7 +164,7 @@ impl<'s> FuncParser<'s> { } } - fn parse(mut self) -> Pass { + fn parse(mut self) -> Pass { let (parser, header) = if let Some(header) = self.parse_func_header() { let name = header.name.v.as_str(); let (parser, deco) = match self.state.scope.get_parser(name) { @@ -197,9 +197,8 @@ impl<'s> FuncParser<'s> { let call = FuncCall { header, body: self.body }; let parsed = parser(call, self.state); - self.feedback.extend(parsed.feedback); - Pass::new(Node::Model(parsed.output), self.feedback) + Pass::new(SyntaxNode::Dyn(parsed.output), self.feedback) } fn parse_func_header(&mut self) -> Option { @@ -662,26 +661,27 @@ fn unescape_raw(raw: &str) -> Vec { #[allow(non_snake_case)] mod tests { use crate::length::Length; - use super::super::test::{check, DebugFn}; + use crate::syntax::span::SpanVec; + use crate::syntax::test::{check, DebugFn}; use super::*; use Decoration::*; use Expr::{Number as Num, Length as Len, Bool}; - use Node::{ + use SyntaxNode::{ Space as S, ToggleItalic as Italic, ToggleBolder as Bold, Parbreak, Linebreak, }; /// Test whether the given string parses into - /// - the given node list (required). + /// - the given SyntaxNode list (required). /// - the given error list (optional, if omitted checks against empty list). /// - the given decoration list (optional, if omitted it is not tested). macro_rules! p { - ($source:expr => [$($model:tt)*]) => { - p!($source => [$($model)*], []); + ($source:expr => [$($tree:tt)*]) => { + p!($source => [$($tree)*], []); }; - ($source:expr => [$($model:tt)*], [$($diagnostics:tt)*] $(, [$($decos:tt)*])? $(,)?) => { + ($source:expr => [$($tree:tt)*], [$($diagnostics:tt)*] $(, [$($decos:tt)*])? $(,)?) => { let mut scope = Scope::new::(); scope.add::("f"); scope.add::("n"); @@ -691,9 +691,9 @@ mod tests { let state = ParseState { scope }; let pass = parse($source, Pos::ZERO, &state); - // Test model. - let (exp, cmp) = span_vec![$($model)*]; - check($source, exp, pass.output.nodes, cmp); + // Test tree. + let (exp, cmp) = span_vec![$($tree)*]; + check($source, exp, pass.output, cmp); // Test diagnostics. let (exp, cmp) = span_vec![$($diagnostics)*]; @@ -728,7 +728,7 @@ mod tests { fn Sub(e1: Expr, e2: Expr) -> Expr { Expr::Sub(Box::new(Z(e1)), Box::new(Z(e2))) } fn Mul(e1: Expr, e2: Expr) -> Expr { Expr::Mul(Box::new(Z(e1)), Box::new(Z(e2))) } fn Div(e1: Expr, e2: Expr) -> Expr { Expr::Div(Box::new(Z(e1)), Box::new(Z(e2))) } - fn T(text: &str) -> Node { Node::Text(text.to_string()) } + fn T(text: &str) -> SyntaxNode { SyntaxNode::Text(text.to_string()) } fn Z(v: T) -> Spanned { Spanned::zero(v) } macro_rules! tuple { @@ -757,7 +757,7 @@ mod tests { macro_rules! raw { ($($line:expr),* $(,)?) => { - Node::Raw(vec![$($line.to_string()),*]) + SyntaxNode::Raw(vec![$($line.to_string()),*]) }; } @@ -769,7 +769,7 @@ mod tests { #[allow(unused_mut)] let mut args = FuncArgs::new(); $( - let items: Vec> = span_vec![$($pos)*].0; + let items: SpanVec = span_vec![$($pos)*].0; for item in items { args.push(item.map(|v| FuncArg::Pos(v))); } @@ -778,7 +778,7 @@ mod tests { value: Z($value), })));)*)? )? - Node::Model(Box::new(DebugFn { + SyntaxNode::Dyn(Box::new(DebugFn { header: FuncHeader { name: span_item!($name).map(|s| Ident(s.to_string())), args, @@ -786,7 +786,7 @@ mod tests { body: func!(@body $($($body)*)?), })) }}; - (@body [$($body:tt)*]) => { Some(SyntaxModel { nodes: span_vec![$($body)*].0 }) }; + (@body [$($body:tt)*]) => { Some(span_vec![$($body)*].0) }; (@body) => { None }; } @@ -818,8 +818,8 @@ mod tests { #[test] fn unescape_raws() { - fn test(raw: &str, expected: Node) { - let vec = if let Node::Raw(v) = expected { v } else { panic!() }; + fn test(raw: &str, expected: SyntaxNode) { + let vec = if let SyntaxNode::Raw(v) = expected { v } else { panic!() }; assert_eq!(unescape_raw(raw), vec); } @@ -834,8 +834,8 @@ mod tests { } #[test] - fn parse_basic_nodes() { - // Basic nodes. + fn parse_basic_SyntaxNodes() { + // Basic SyntaxNodes. p!("" => []); p!("hi" => [T("hi")]); p!("*hi" => [Bold, T("hi")]); @@ -855,7 +855,7 @@ mod tests { p!("`hi\nyou" => [raw!["hi", "you"]], [(1:3, 1:3, "expected backtick")]); p!("`hi\\`du`" => [raw!["hi`du"]]); - // Spanned nodes. + // Spanned SyntaxNodes. p!("Hi" => [(0:0, 0:2, T("Hi"))]); p!("*Hi*" => [(0:0, 0:1, Bold), (0:1, 0:3, T("Hi")), (0:3, 0:4, Bold)]); p!("🌎\n*/[n]" => diff --git a/src/syntax/scope.rs b/src/syntax/scope.rs index 83b9fdee5..d30929442 100644 --- a/src/syntax/scope.rs +++ b/src/syntax/scope.rs @@ -5,7 +5,7 @@ use std::fmt::{self, Debug, Formatter}; use crate::func::ParseFunc; use super::parsing::CallParser; -use super::model::Model; +use super::tree::DynamicNode; /// A map from identifiers to function parsers. pub struct Scope { @@ -17,7 +17,7 @@ impl Scope { /// Create a new empty scope with a fallback parser that is invoked when no /// match is found. pub fn new() -> Scope - where F: ParseFunc + Model + 'static { + where F: ParseFunc + DynamicNode + 'static { Scope { parsers: HashMap::new(), fallback: make_parser::(()), @@ -31,14 +31,14 @@ impl Scope { /// Associate the given name with a type that is parseable into a function. pub fn add(&mut self, name: &str) - where F: ParseFunc + Model + 'static { + where F: ParseFunc + DynamicNode + 'static { self.add_with_meta::(name, ()); } /// Add a parseable type with additional metadata that is given to the /// parser (other than the default of `()`). pub fn add_with_meta(&mut self, name: &str, metadata: ::Meta) - where F: ParseFunc + Model + 'static { + where F: ParseFunc + DynamicNode + 'static { self.parsers.insert( name.to_string(), make_parser::(metadata), @@ -65,9 +65,9 @@ impl Debug for Scope { } fn make_parser(metadata: ::Meta) -> Box -where F: ParseFunc + Model + 'static { +where F: ParseFunc + DynamicNode + 'static { Box::new(move |f, s| { F::parse(f, s, metadata.clone()) - .map(|model| Box::new(model) as Box) + .map(|tree| Box::new(tree) as Box) }) } diff --git a/src/syntax/test.rs b/src/syntax/test.rs index 38a124aab..b701e5777 100644 --- a/src/syntax/test.rs +++ b/src/syntax/test.rs @@ -5,7 +5,7 @@ use super::expr::{Expr, Ident, Tuple, NamedTuple, Object, Pair}; use super::parsing::{FuncHeader, FuncArgs, FuncArg}; use super::span::Spanned; use super::tokens::Token; -use super::model::{SyntaxModel, Model, Node}; +use super::tree::{SyntaxTree, SyntaxNode, DynamicNode}; /// Check whether the expected and found results are the same. pub fn check(src: &str, exp: T, found: T, cmp_spans: bool) @@ -62,7 +62,7 @@ function! { #[derive(Debug, Clone, PartialEq)] pub struct DebugFn { pub header: FuncHeader, - pub body: Option, + pub body: Option, } parse(header, body, state, f) { @@ -83,20 +83,14 @@ pub trait SpanlessEq { fn spanless_eq(&self, other: &Rhs) -> bool; } -impl SpanlessEq for SyntaxModel { - fn spanless_eq(&self, other: &SyntaxModel) -> bool { - self.nodes.spanless_eq(&other.nodes) - } -} - -impl SpanlessEq for Node { - fn spanless_eq(&self, other: &Node) -> bool { - fn downcast<'a>(func: &'a (dyn Model + 'static)) -> &'a DebugFn { +impl SpanlessEq for SyntaxNode { + fn spanless_eq(&self, other: &SyntaxNode) -> bool { + fn downcast<'a>(func: &'a (dyn DynamicNode + 'static)) -> &'a DebugFn { func.downcast::().expect("not a debug fn") } match (self, other) { - (Node::Model(a), Node::Model(b)) => { + (SyntaxNode::Dyn(a), SyntaxNode::Dyn(b)) => { downcast(a.as_ref()).spanless_eq(downcast(b.as_ref())) } (a, b) => a == b, diff --git a/src/syntax/tokens.rs b/src/syntax/tokens.rs index a0af5a02e..1ea114494 100644 --- a/src/syntax/tokens.rs +++ b/src/syntax/tokens.rs @@ -83,8 +83,8 @@ pub enum Token<'s> { ExprHex(&'s str), /// A plus in a function header, signifying the addition of expressions. Plus, - /// A hyphen in a function header, - /// signifying the subtraction of expressions. + /// A hyphen in a function header, signifying the subtraction of + /// expressions. Hyphen, /// A slash in a function header, signifying the division of expressions. Slash, diff --git a/src/syntax/tree.rs b/src/syntax/tree.rs new file mode 100644 index 000000000..41a03faeb --- /dev/null +++ b/src/syntax/tree.rs @@ -0,0 +1,100 @@ +//! The syntax tree. + +use std::any::Any; +use std::fmt::Debug; + +use crate::layout::Layout; +use super::span::SpanVec; + +/// A list of nodes which forms a tree together with the nodes' children. +pub type SyntaxTree = SpanVec; + +/// A syntax node, which encompasses a single logical entity of parsed source +/// code. +#[derive(Debug, Clone)] +pub enum SyntaxNode { + /// Whitespace containing less than two newlines. + Space, + /// Whitespace with more than two newlines. + Parbreak, + /// A forced line break. + Linebreak, + /// Plain text. + Text(String), + /// Lines of raw text. + Raw(Vec), + /// Italics were enabled / disabled. + ToggleItalic, + /// Bolder was enabled / disabled. + ToggleBolder, + /// A subtree, typically a function invocation. + Dyn(Box), +} + +impl PartialEq for SyntaxNode { + fn eq(&self, other: &SyntaxNode) -> bool { + use SyntaxNode::*; + match (self, other) { + (Space, Space) => true, + (Parbreak, Parbreak) => true, + (Linebreak, Linebreak) => true, + (Text(a), Text(b)) => a == b, + (Raw(a), Raw(b)) => a == b, + (ToggleItalic, ToggleItalic) => true, + (ToggleBolder, ToggleBolder) => true, + (Dyn(a), Dyn(b)) => a == b, + _ => false, + } + } +} + +/// Dynamic syntax nodes. +/// +/// *Note*: This is automatically implemented for all types which are +/// `Debug + Clone + PartialEq`, `Layout` and `'static`. +pub trait DynamicNode: Debug + Layout { + /// Convert into a `dyn Any`. + fn as_any(&self) -> &dyn Any; + + /// Check for equality with another dynamic node. + fn dyn_eq(&self, other: &dyn DynamicNode) -> bool; + + /// Clone into a boxed node trait object. + fn box_clone(&self) -> Box; +} + +impl dyn DynamicNode { + /// Downcast this dynamic node to a concrete node. + pub fn downcast(&self) -> Option<&N> where N: DynamicNode + 'static { + self.as_any().downcast_ref::() + } +} + +impl PartialEq for dyn DynamicNode { + fn eq(&self, other: &dyn DynamicNode) -> bool { + self.dyn_eq(other) + } +} + +impl Clone for Box { + fn clone(&self) -> Self { + self.box_clone() + } +} + +impl DynamicNode for T where T: Debug + PartialEq + Clone + Layout + 'static { + fn as_any(&self) -> &dyn Any { + self + } + + fn dyn_eq(&self, other: &dyn DynamicNode) -> bool { + match other.as_any().downcast_ref::() { + Some(other) => self == other, + None => false, + } + } + + fn box_clone(&self) -> Box { + Box::new(self.clone()) + } +} diff --git a/tests/test_typeset.rs b/tests/test_typeset.rs index 216e4a1a3..6ac34c85c 100644 --- a/tests/test_typeset.rs +++ b/tests/test_typeset.rs @@ -73,7 +73,7 @@ fn main() { typesetter.set_page_style(PageStyle { class: PaperClass::Custom, - dimensions: Size::with_all(Length::pt(250.0).as_raw()), + size: Size::with_all(Length::pt(250.0).as_raw()), margins: Value4::with_all(None), }); @@ -148,13 +148,13 @@ fn render( ) -> DrawTarget { let pad = scale * 10.0; let width = 2.0 * pad + layouts.iter() - .map(|l| scale * l.dimensions.x) + .map(|l| scale * l.size.x) .max_by(|a, b| a.partial_cmp(&b).unwrap()) .unwrap() .round(); let height = pad + layouts.iter() - .map(|l| scale * l.dimensions.y + pad) + .map(|l| scale * l.size.y + pad) .sum::() .round(); @@ -166,8 +166,8 @@ fn render( surface.fill_rect( offset.x as f32, offset.y as f32, - (scale * layout.dimensions.x) as f32, - (scale * layout.dimensions.y) as f32, + (scale * layout.size.x) as f32, + (scale * layout.size.y) as f32, &Source::Solid(WHITE), &Default::default(), ); @@ -186,7 +186,7 @@ fn render( } } - offset.y += scale * layout.dimensions.y + pad; + offset.y += scale * layout.size.y + pad; } surface