diff --git a/src/func/mod.rs b/src/func/mod.rs index 126fd8243..33a6c7565 100644 --- a/src/func/mod.rs +++ b/src/func/mod.rs @@ -12,8 +12,9 @@ pub mod helpers; /// Useful imports for creating your own functions. pub mod prelude { pub use crate::func::{Command, CommandList, Function}; - pub use crate::layout::{layout_tree, Layout, MultiLayout, LayoutContext, LayoutSpace}; - pub use crate::layout::{LayoutAxes, Axis, Alignment}; + pub use crate::layout::{layout_tree, Layout, MultiLayout, LayoutContext}; + pub use crate::layout::{LayoutSpace, LayoutSpaces, SpacingKind}; + pub use crate::layout::{LayoutAxes, Axis, AxisKind, LayoutAlignment, Alignment}; pub use crate::layout::{LayoutError, LayoutResult}; pub use crate::syntax::{SyntaxTree, FuncHeader, FuncArgs, Expression, Spanned, Span}; pub use crate::syntax::{parse, ParseContext, ParseError, ParseResult}; @@ -96,19 +97,16 @@ pub enum Command<'a> { Add(Layout), AddMultiple(MultiLayout), - - AddPrimarySpace(Size), - AddSecondarySpace(Size), + AddSpacing(Size, SpacingKind, AxisKind), FinishLine, FinishRun, FinishSpace, - BreakParagraph, SetTextStyle(TextStyle), SetPageStyle(PageStyle), - + SetAlignment(LayoutAlignment), SetAxes(LayoutAxes), } diff --git a/src/layout/flex.rs b/src/layout/flex.rs index 96b2aa859..afca23ec8 100644 --- a/src/layout/flex.rs +++ b/src/layout/flex.rs @@ -62,7 +62,7 @@ impl PartialLine { pub struct FlexContext { pub spaces: LayoutSpaces, pub axes: LayoutAxes, - pub expand: bool, + pub alignment: LayoutAlignment, pub flex_spacing: Size, } @@ -110,7 +110,7 @@ impl FlexLayouter { if !self.run_is_empty() { self.finish_run()?; } - Ok(self.stack.add_space(space, kind)) + Ok(self.stack.add_spacing(space, kind)) } pub fn set_axes(&mut self, axes: LayoutAxes) { @@ -179,7 +179,7 @@ impl FlexLayouter { debug_render: false, })?; - self.stack.add_space(self.flex_spacing, SpaceKind::Independent); + self.stack.add_spacing(self.flex_spacing, SpaceKind::Independent); let remaining = self.axes.specialize(Size2D { x: self.part.usable diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 31064d40d..f45ed10af 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -59,7 +59,7 @@ pub struct LayoutContext<'a, 'p> { pub spaces: LayoutSpaces, /// The initial axes along which content is laid out. pub axes: LayoutAxes, - /// The alignment for the two axes. + /// The alignment of the finished layout. pub alignment: LayoutAlignment, } @@ -71,12 +71,12 @@ pub type LayoutSpaces = SmallVec<[LayoutSpace; 2]>; pub struct LayoutSpace { /// The maximum size of the box to layout in. pub dimensions: Size2D, + /// Padding that should be respected on each side. + pub padding: SizeBox, /// Whether to expand the dimensions of the resulting layout to the full /// dimensions of this space or to shrink them to fit the content for the /// vertical and horizontal axis. pub expand: (bool, bool), - /// Padding that should be respected on each side. - pub padding: SizeBox, } impl LayoutSpace { @@ -95,8 +95,8 @@ impl LayoutSpace { pub fn usable_space(&self) -> LayoutSpace { LayoutSpace { dimensions: self.usable(), - expand: (false, false), padding: SizeBox::zero(), + expand: (false, false), } } } @@ -110,6 +110,10 @@ pub struct LayoutAxes { impl LayoutAxes { pub fn new(primary: Axis, secondary: Axis) -> LayoutAxes { + if primary.is_horizontal() == secondary.is_horizontal() { + panic!("LayoutAxes::new: invalid parallel axes {:?} and {:?}", primary, secondary); + } + LayoutAxes { primary, secondary } } @@ -134,6 +138,13 @@ impl LayoutAxes { } } +/// The two kinds of axes. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum AxisKind { + Primary, + Secondary, +} + /// Directions along which content is laid out. #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum Axis { diff --git a/src/layout/stack.rs b/src/layout/stack.rs index fd88ce982..793c2044d 100644 --- a/src/layout/stack.rs +++ b/src/layout/stack.rs @@ -11,69 +11,13 @@ pub struct StackLayouter { ctx: StackContext, /// The output layouts. layouts: MultiLayout, - /// The full layout space. + /// The currently active layout space. space: Space, - /// The currently active subspace. + /// The remaining subspace of the active space. Whenever the layouting axes + /// change a new subspace is started. sub: Subspace, } -#[derive(Debug, Clone)] -struct Space { - /// The index of this space in the list of spaces. - index: usize, - /// Whether to add the layout for this space even if it would be empty. - hard: bool, - /// The layouting actions accumulated from the subspaces. - actions: LayoutActionList, - /// The used size of this space from the top-left corner to - /// the bottomright-most point of used space (specialized). - combined_dimensions: Size2D, -} - -#[derive(Debug, Clone)] -struct Subspace { - /// The axes along which contents in this subspace are laid out. - axes: LayoutAxes, - /// The beginning of this subspace in the parent space (specialized). - origin: Size2D, - /// The total usable space of this subspace (generalized). - usable: Size2D, - /// The used size of this subspace (generalized), with - /// - `x` being the maximum of the primary size of all boxes. - /// - `y` being the total extent of all boxes and space in the secondary - /// direction. - size: Size2D, - /// The so-far accumulated (offset, anchor, box) triples. - boxes: Vec<(Size, Size, Layout)>, - /// The last added spacing if the last was spacing. - last_spacing: LastSpacing, -} - -impl Space { - fn new(index: usize, hard: bool) -> Space { - Space { - index, - hard, - actions: LayoutActionList::new(), - combined_dimensions: Size2D::zero(), - } - } -} - -impl Subspace { - fn new(origin: Size2D, usable: Size2D, axes: LayoutAxes) -> Subspace { - Subspace { - origin, - anchor: axes.anchor(usable), - factor: axes.secondary.axis.factor(), - boxes: vec![], - usable: axes.generalize(usable), - dimensions: Size2D::zero(), - space: LastSpacing::Forbidden, - } - } -} - /// The context for stack layouting. /// /// See [`LayoutContext`] for details about the fields. @@ -81,7 +25,50 @@ impl Subspace { pub struct StackContext { pub spaces: LayoutSpaces, pub axes: LayoutAxes, - pub expand: bool, + pub alignment: LayoutAlignment, +} + +/// A layout space composed of subspaces which can have different axes and +/// alignments. +#[derive(Debug, Clone)] +struct Space { + /// The index of this space in the list of spaces. + index: usize, + /// Whether to add the layout for this space even if it would be empty. + hard: bool, + /// The so-far accumulated subspaces. + spaces: Vec, +} + +/// A part of a space with fixed axes and secondary alignment. +#[derive(Debug, Clone)] +struct Subspace { + /// The axes along which contents in this subspace are laid out. + axes: LayoutAxes, + /// The secondary alignment of this subspace. + alignment: Alignment, + /// The beginning of this subspace in the parent space (specialized). + origin: Size2D, + /// The total usable space of this subspace (generalized). + usable: Size2D, + /// The used size of this subspace (generalized), with + /// - `x` being the maximum of the primary size of all boxes. + /// - `y` being the total extent of all boxes and space in the secondary + /// direction. + size: Size2D, + /// The so-far accumulated layouts. + layouts: Vec, + /// The last added spacing if the last added thing was spacing. + last_spacing: LastSpacing, +} + +/// A single layout in a subspace. +#[derive(Debug, Clone)] +struct LayoutEntry { + /// The offset of this box on the secondary axis. + offset: Size, + /// The layout itself. + layout: Layout, } impl StackLayouter { @@ -94,41 +81,61 @@ impl StackLayouter { ctx, layouts: MultiLayout::new(), space: Space::new(0, true), - sub: Subspace::new(space.start(), space.usable(), axes), + sub: Subspace::new(axes, Alignment::Origin, space.start(), space.usable()), } } + /// Add a layout to the stack. pub fn add(&mut self, layout: Layout) -> LayoutResult<()> { - if let LastSpacing::Soft(space) = self.sub.space { - self.add_space(space, SpaceKind::Hard); + if layout.alignment.secondary != self.sub.alignment { + // self.finish_subspace(); + // finish sub and start new with layout's alignment } - let size = self.ctx.axes.generalize(layout.dimensions); + // Add a cached soft space if there is one. + if let LastSpacing::Soft(space, _) = self.sub.last_spacing { + self.add_spacing(space, SpacingKind::Hard); + } - let mut new_dimensions = Size2D { - x: crate::size::max(self.sub.dimensions.x, size.x), - y: self.sub.dimensions.y + size.y + // The new primary size is the maximum of the current one and the + // layout's one while the secondary size grows by the layout's size. + let size = self.ctx.axes.generalize(layout.dimensions); + let mut new_size = Size2D { + x: crate::size::max(self.sub.size.x, size.x), + y: self.sub.size.y + size.y }; - while !self.sub.usable.fits(new_dimensions) { + // Find the first (sub-)space that fits the layout. + while !self.sub.usable.fits(new_size) { if self.space_is_last() && self.space_is_empty() { lerr!("box does not fit into stack"); } self.finish_space(true); - new_dimensions = size; + new_size = size; } - let offset = self.sub.dimensions.y; - let anchor = self.ctx.axes.primary.anchor(size.x); + // The secondary offset from the start of layouts is given by the + // current primary size of the subspace. + let offset = self.sub.size.y; + self.sub.layouts.push(LayoutEntry { + offset, + layout, + }); - self.sub.boxes.push((offset, anchor, layout)); - self.sub.dimensions = new_dimensions; - self.sub.space = LastSpacing::Allowed; + // The new size of the subspace is the previously calculated + // combination. + self.sub.size = new_size; + + // Since the last item was a box, last spacing is reset to `None`. + self.sub.last_spacing = LastSpacing::None; Ok(()) } + /// Add multiple layouts to the stack. + /// + /// This function simply calls `add` for each layout. pub fn add_multiple(&mut self, layouts: MultiLayout) -> LayoutResult<()> { for layout in layouts { self.add(layout)?; @@ -136,33 +143,55 @@ impl StackLayouter { Ok(()) } - pub fn add_space(&mut self, space: Size, kind: SpaceKind) { - if kind == SpaceKind::Soft { - if self.sub.space != LastSpacing::Forbidden { - self.sub.space = LastSpacing::Soft(space); - } - } else { - if self.sub.dimensions.y + space > self.sub.usable.y { - self.sub.dimensions.y = self.sub.usable.y; - } else { - self.sub.dimensions.y += space; + /// Add secondary spacing to the stack. + pub fn add_spacing(&mut self, space: Size, kind: SpacingKind) { + match kind { + // A hard space is directly added to the sub's size. + SpacingKind::Hard => { + if self.sub.size.y + space > self.sub.usable.y { + self.sub.size.y = self.sub.usable.y; + } else { + self.sub.size.y += space; + } + + self.sub.last_spacing = LastSpacing::Hard; } - if kind == SpaceKind::Hard { - self.sub.space = LastSpacing::Forbidden; + // A hard space is cached if it is not consumed by a hard space or + // previous soft space with higher level. + SpacingKind::Soft(level) => { + let consumes = match self.sub.last_spacing { + LastSpacing::None => true, + LastSpacing::Soft(_, prev) if level < prev => true, + _ => false, + }; + + if consumes { + self.sub.last_spacing = LastSpacing::Soft(space, level); + } } } } + /// Change the layouting axis used by this layouter. + /// + /// This starts a new subspace (if the axes are actually different from the + /// current ones). pub fn set_axes(&mut self, axes: LayoutAxes) { if axes != self.ctx.axes { self.finish_subspace(); + let (origin, usable) = self.remaining_subspace(); + self.sub = Subspace::new(axes, Alignment::Origin, origin, usable); self.ctx.axes = axes; - self.sub = Subspace::new(origin, usable, axes); } } + /// Change the layouting spaces to use. + /// + /// If `replace_empty` is true, the current space is replaced if there are + /// no boxes laid into it yet. Otherwise, only the followup spaces are + /// replaced. pub fn set_spaces(&mut self, spaces: LayoutSpaces, replace_empty: bool) { if replace_empty && self.space_is_empty() { self.ctx.spaces = spaces; @@ -173,10 +202,13 @@ impl StackLayouter { } } + /// The remaining unpadded, unexpanding spaces. If a multi-layout is laid + /// out into these spaces, it will fit into this stack. pub fn remaining(&self) -> LayoutSpaces { let mut spaces = smallvec![LayoutSpace { dimensions: self.remaining_subspace().1, padding: SizeBox::zero(), + expand: (false, false), }]; for space in &self.ctx.spaces[self.next_space()..] { @@ -186,20 +218,24 @@ impl StackLayouter { spaces } + /// The usable size along the primary axis. pub fn primary_usable(&self) -> Size { self.sub.usable.x } + /// Whether the current layout space (not subspace) is empty. pub fn space_is_empty(&self) -> bool { - self.space.combined_dimensions == Size2D::zero() - && self.space.actions.is_empty() - && self.sub.dimensions == Size2D::zero() + self.sub.layouts.is_empty() + && self.sub.size == Size2D::zero() + && self.space.spaces.is_empty() } + /// Whether the current layout space is the last is the followup list. pub fn space_is_last(&self) -> bool { self.space.index == self.ctx.spaces.len() - 1 } + /// Compute the finished multi-layout. pub fn finish(mut self) -> MultiLayout { if self.space.hard || !self.space_is_empty() { self.finish_space(false); @@ -212,29 +248,19 @@ impl StackLayouter { let space = self.ctx.spaces[self.space.index]; - self.layouts.add(Layout { + self.layouts.push(Layout { dimensions: match self.ctx.expand { true => space.dimensions, false => self.space.combined_dimensions.padded(space.padding), }, - actions: self.space.actions.to_vec(), - debug_render: true, + baseline: None, + alignment: self.ctx.alignment, + actions: actions.to_vec(), }); self.start_space(self.next_space(), hard); } - fn start_space(&mut self, space: usize, hard: bool) { - self.space = Space::new(space, hard); - - let space = self.ctx.spaces[space]; - self.sub = Subspace::new(space.start(), space.usable(), self.ctx.axes); - } - - fn next_space(&self) -> usize { - (self.space.index + 1).min(self.ctx.spaces.len() - 1) - } - fn finish_subspace(&mut self) { let factor = self.ctx.axes.secondary.axis.factor(); let anchor = @@ -264,6 +290,18 @@ impl StackLayouter { self.space.combined_dimensions.max_eq(origin - space.start() + dimensions); } + /// Start a new space with the given index. + fn start_space(&mut self, space: usize, hard: bool) { + // Start the space. + self.space = Space::new(space, hard); + + // Start the subspace. + let space = self.ctx.spaces[space]; + let axes = self.ctx.axes; + self.sub = Subspace::new(axes, Alignment::Origin, space.start(), space.usable()); + } + + /// The remaining sub fn remaining_subspace(&self) -> (Size2D, Size2D) { let new_origin = self.sub.origin + match self.ctx.axes.secondary.axis.is_positive() { true => self.ctx.axes.specialize(Size2D::with_y(self.sub.dimensions.y)), @@ -277,4 +315,32 @@ impl StackLayouter { (new_origin, new_usable) } + + fn next_space(&self) -> usize { + (self.space.index + 1).min(self.ctx.spaces.len() - 1) + } +} + +impl Space { + fn new(index: usize, hard: bool) -> Space { + Space { + index, + hard, + spaces: vec![], + } + } +} + +impl Subspace { + fn new(axes: LayoutAxes, alignment: Alignment, origin: Size2D, usable: Size2D) -> Subspace { + Subspace { + axes, + alignment, + origin, + usable: axes.generalize(usable), + size: Size2D::zero(), + layouts: vec![], + last_spacing: LastSpacing::Hard, + } + } } diff --git a/src/layout/text.rs b/src/layout/text.rs index 3ca826ca1..343127e37 100644 --- a/src/layout/text.rs +++ b/src/layout/text.rs @@ -11,6 +11,7 @@ use crate::size::{Size, Size2D}; pub struct TextContext<'a, 'p> { pub loader: &'a SharedFontLoader<'p>, pub style: &'a TextStyle, + pub alignment: LayoutAlignment, } /// Layouts text into a box. @@ -72,6 +73,8 @@ impl<'a, 'p> TextLayouter<'a, 'p> { Ok(Layout { dimensions: Size2D::new(self.width, self.ctx.style.font_size), + baseline: None, + alignment: self.ctx.alignment, actions: self.actions.to_vec(), }) } diff --git a/src/layout/tree.rs b/src/layout/tree.rs index 9a8189632..efa0c7b74 100644 --- a/src/layout/tree.rs +++ b/src/layout/tree.rs @@ -19,10 +19,10 @@ 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.text), spaces: ctx.spaces.clone(), axes: ctx.axes, - expand: ctx.expand, + alignment: ctx.alignment, + flex_spacing: flex_spacing(&ctx.style.text), }), style: ctx.style.clone(), ctx, @@ -52,6 +52,7 @@ impl<'a, 'p> TreeLayouter<'a, 'p> { let layout = layout_text(text, TextContext { loader: &self.ctx.loader, style: &self.style.text, + alignment: self.ctx.alignment, })?; Ok(self.flex.add(layout)) @@ -90,23 +91,25 @@ impl<'a, 'p> TreeLayouter<'a, 'p> { } fn execute(&mut self, command: Command) -> LayoutResult<()> { + use Command::*; + match command { - Command::LayoutTree(tree) => self.layout(tree)?, + LayoutTree(tree) => self.layout(tree)?, - Command::Add(layout) => self.flex.add(layout), - Command::AddMultiple(layouts) => self.flex.add_multiple(layouts), + Add(layout) => self.flex.add(layout), + AddMultiple(layouts) => self.flex.add_multiple(layouts), + AddSpacing(space, kind, axis) => match axis { + AxisKind::Primary => self.flex.add_primary_space(space, kind), + AxisKind::Secondary => self.flex.add_secondary_space(space, kind)?, + } - Command::AddPrimarySpace(space) => self.flex.add_primary_space(space, SpacingKind::Hard), - Command::AddSecondarySpace(space) => self.flex.add_secondary_space(space, SpacingKind::Hard)?, + FinishLine => self.flex.add_break(), + FinishRun => { self.flex.finish_run()?; }, + FinishSpace => self.flex.finish_space(true)?, + BreakParagraph => self.layout_paragraph()?, - Command::FinishLine => self.flex.add_break(), - Command::FinishRun => { self.flex.finish_run()?; }, - Command::FinishSpace => self.flex.finish_space(true)?, - - Command::BreakParagraph => self.layout_paragraph()?, - - Command::SetTextStyle(style) => self.style.text = style, - Command::SetPageStyle(style) => { + SetTextStyle(style) => self.style.text = style, + SetPageStyle(style) => { if !self.ctx.top_level { lerr!("page style cannot only be altered in the top-level context"); } @@ -115,13 +118,13 @@ impl<'a, 'p> TreeLayouter<'a, 'p> { self.flex.set_spaces(smallvec![ LayoutSpace { dimensions: style.dimensions, - expand: (true, true), padding: style.margins, + expand: (true, true), } ], true); - }, - - Command::SetAxes(axes) => { + } + SetAlignment(alignment) => self.ctx.alignment = alignment, + SetAxes(axes) => { self.flex.set_axes(axes); self.ctx.axes = axes; } diff --git a/src/library/spacing.rs b/src/library/spacing.rs index 869a62273..47fe9fff6 100644 --- a/src/library/spacing.rs +++ b/src/library/spacing.rs @@ -65,7 +65,7 @@ enum Spacing { // FIXME: h != primary and v != secondary. space_func!(HorizontalSpace, "📖 `h`: Adds horizontal whitespace.", - space => AddPrimarySpace(space)); + space => AddSpacing(space, SpacingKind::Hard, AxisKind::Primary)); space_func!(VerticalSpace, "📑 `v`: Adds vertical whitespace.", - space => AddSecondarySpace(space)); + space => AddSpacing(space, SpacingKind::Hard, AxisKind::Secondary)); diff --git a/tests/layouts/align.typ b/tests/layouts/align.typ index c020ef4ba..c3c607337 100644 --- a/tests/layouts/align.typ +++ b/tests/layouts/align.typ @@ -8,3 +8,16 @@ [align: bottom] A longer sentence with a few more words. ] + +[page.break] + +[box][ + [align: vertical=top] + Top + + [align: vertical=center] + Center + + [align: vertical=bottom] + Bottom [n] Bottom [n] Bottom +]