From a791ef162868c65284903ab479731e0dc9e7a223 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Wed, 11 Dec 2019 22:06:54 +0100 Subject: [PATCH] =?UTF-8?q?Pretty=20good=20stack=20layouter=20=E2=9C=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/layout/flex.rs | 3 +- src/layout/mod.rs | 6 +- src/layout/stack.rs | 177 +++++++++++++++++++++++++--------------- src/layout/tree.rs | 4 +- src/lib.rs | 5 +- src/library/boxed.rs | 8 +- src/size.rs | 23 ++++-- tests/layouts/stack.typ | 81 ++++++++++++++++++ tests/layouts/test.typ | 52 ------------ tests/render.py | 5 +- 10 files changed, 230 insertions(+), 134 deletions(-) create mode 100644 tests/layouts/stack.typ delete mode 100644 tests/layouts/test.typ diff --git a/src/layout/flex.rs b/src/layout/flex.rs index fc1a09c08..488538630 100644 --- a/src/layout/flex.rs +++ b/src/layout/flex.rs @@ -66,6 +66,7 @@ pub struct FlexContext { pub axes: LayoutAxes, pub alignment: LayoutAlignment, pub flex_spacing: Size, + pub debug: bool, } impl FlexLayouter { @@ -75,6 +76,7 @@ impl FlexLayouter { spaces: ctx.spaces, axes: ctx.axes, alignment: ctx.alignment, + debug: ctx.debug, }); let usable = stack.primary_usable(); @@ -176,7 +178,6 @@ impl FlexLayouter { unimplemented!() } - #[allow(dead_code)] fn finish_partial_line(&mut self) { unimplemented!() } diff --git a/src/layout/mod.rs b/src/layout/mod.rs index fe4d3e08f..fa3a8866c 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -65,14 +65,16 @@ pub struct LayoutContext<'a, 'p> { pub loader: &'a SharedFontLoader<'p>, /// The style for pages and text. pub style: &'a LayoutStyle, - /// Whether this layouting process handles the top-level pages. - pub top_level: bool, /// The spaces to layout in. pub spaces: LayoutSpaces, /// The initial axes along which content is laid out. pub axes: LayoutAxes, /// The alignment of the finished layout. pub alignment: LayoutAlignment, + /// Whether this layouting process handles the top-level pages. + pub top_level: bool, + /// Whether to debug render a box around the layout. + pub debug: bool, } /// A possibly stack-allocated vector of layout spaces. diff --git a/src/layout/stack.rs b/src/layout/stack.rs index 369b1c816..27ca433bd 100644 --- a/src/layout/stack.rs +++ b/src/layout/stack.rs @@ -1,14 +1,15 @@ use smallvec::smallvec; -use crate::size::{min, max}; +use crate::size::max; use super::*; -/// The stack layouter arranges boxes stacked onto each other. +/// The stack layouter stack boxes onto each other along the secondary layouting +/// axis. /// -/// The boxes are laid out in the direction of the secondary layouting axis and -/// are aligned along both axes. +/// The boxes are aligned along both axes according to their requested +/// alignment. #[derive(Debug, Clone)] pub struct StackLayouter { - /// The context for layouter. + /// The context for layouting. ctx: StackContext, /// The output layouts. layouts: MultiLayout, @@ -17,13 +18,19 @@ pub struct StackLayouter { } /// The context for stack layouting. -/// -/// See [`LayoutContext`] for details about the fields. #[derive(Debug, Clone)] pub struct StackContext { + /// The spaces to layout in. pub spaces: LayoutSpaces, + /// The initial layouting axes, which can be updated by the + /// [`StackLayouter::set_axes`] method. pub axes: LayoutAxes, + /// Which alignment to set on the resulting layout. This affects how it will + /// be positioned in a parent box. pub alignment: LayoutAlignment, + /// Whether to output a command which renders a debugging box showing the + /// extent of the layout. + pub debug: bool, } /// A layout space composed of subspaces which can have different axes and @@ -42,19 +49,26 @@ struct Space { usable: Size2D, /// The specialized extra-needed dimensions to affect the size at all. extra: Size2D, - /// The maximal secondary alignment for both specialized axes (horizontal, - /// vertical). - alignment: (Alignment, Alignment), + /// Dictates the valid alignments for new boxes in this space. + rulers: Rulers, /// The last added spacing if the last added thing was spacing. last_spacing: LastSpacing, } +/// The rulers of a space dictate which alignments for new boxes are still +/// allowed and which require a new space to be started. +#[derive(Debug, Clone)] +struct Rulers { + top: Alignment, + bottom: Alignment, + left: Alignment, + right: Alignment, +} + impl StackLayouter { /// Create a new stack layouter. pub fn new(ctx: StackContext) -> StackLayouter { - let axes = ctx.axes; let space = ctx.spaces[0]; - StackLayouter { ctx, layouts: MultiLayout::new(), @@ -64,23 +78,15 @@ impl StackLayouter { /// Add a layout to the stack. pub fn add(&mut self, layout: Layout) -> LayoutResult<()> { - // If the layout's secondary alignment is less than what we have already - // seen, it needs to go into the next space. - if layout.alignment.secondary < *self.secondary_alignment() { + if !self.update_rulers(layout.alignment) { self.finish_space(true); } - if layout.alignment.secondary == *self.secondary_alignment() { - // Add a cached soft space if there is one and the alignment stayed - // the same. Soft spaces are discarded if the alignment changes. - if let LastSpacing::Soft(spacing, _) = self.space.last_spacing { - self.add_spacing(spacing, SpacingKind::Hard); - } - } else { - // We want the new maximal alignment and since the layout's - // secondary alignment is at least the previous maximum, we just - // take it. - *self.secondary_alignment() = layout.alignment.secondary; + // Now, we add a possibly cached soft space. If the secondary alignment + // changed before, a possibly cached space would have already been + // discarded. + if let LastSpacing::Soft(spacing, _) = self.space.last_spacing { + self.add_spacing(spacing, SpacingKind::Hard); } // Find the first space that fits the layout. @@ -93,8 +99,11 @@ impl StackLayouter { self.finish_space(true); } + // Change the usable space and size of the space. self.update_metrics(layout.dimensions.generalized(self.ctx.axes)); + // Add the box to the vector and remember that spacings are allowed + // again. self.space.layouts.push((self.ctx.axes, layout)); self.space.last_spacing = LastSpacing::None; @@ -103,7 +112,7 @@ impl StackLayouter { /// Add multiple layouts to the stack. /// - /// This function simply calls `add` for each layout. + /// This function simply calls `add` repeatedly for each layout. pub fn add_multiple(&mut self, layouts: MultiLayout) -> LayoutResult<()> { for layout in layouts { self.add(layout)?; @@ -121,7 +130,6 @@ impl StackLayouter { let dimensions = Size2D::with_y(spacing); self.update_metrics(dimensions); - self.space.layouts.push((self.ctx.axes, Layout { dimensions: dimensions.specialized(self.ctx.axes), alignment: LayoutAlignment::default(), @@ -147,6 +155,26 @@ impl StackLayouter { } } + /// Update the rulers to account for the new layout. Returns true if a + /// space break is necessary. + fn update_rulers(&mut self, alignment: LayoutAlignment) -> bool { + let axes = self.ctx.axes; + let allowed = self.alignment_allowed(axes.primary, alignment.primary) + && self.alignment_allowed(axes.secondary, alignment.secondary); + + if allowed { + *self.space.rulers.get(axes.secondary) = alignment.secondary; + } + + allowed + } + + /// Whether the given alignment is still allowed according to the rulers. + fn alignment_allowed(&mut self, axis: Axis, alignment: Alignment) -> bool { + alignment >= *self.space.rulers.get(axis) + && alignment <= self.space.rulers.get(axis.inv()).inv() + } + /// 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: Size2D) { @@ -196,8 +224,12 @@ 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 dimensions = self.space.usable + - Size2D::with_y(self.space.last_spacing.soft_or_zero()) + .specialized(self.ctx.axes); + let mut spaces = smallvec![LayoutSpace { - dimensions: self.space.usable, + dimensions, padding: SizeBox::ZERO, expand: LayoutExpansion::new(false, false), }]; @@ -280,18 +312,34 @@ impl StackLayouter { // Step 3: Backward pass. Reduce the bounding boxes from the previous // layouts by what is taken by the following ones. - let mut extent = Size::ZERO; + // The `x` field stores the maximal primary extent in one axis-aligned + // run, while the `y` fields stores the accumulated secondary extent. + let mut extent = Size2D::ZERO; + let mut rotated = false; for (bound, entry) in bounds.iter_mut().zip(&self.space.layouts).rev() { let (axes, layout) = entry; + // When the axes get rotated, the the maximal primary size + // (`extent.x`) dictates how much secondary extent the whole run + // had. This value is thus stored in `extent.y`. The primary extent + // is reset for this new axis-aligned run. + let is_horizontal = axes.secondary.is_horizontal(); + if is_horizontal != rotated { + extent.y = extent.x; + extent.x = Size::ZERO; + rotated = is_horizontal; + } + // We reduce the bounding box of this layout at it's end by the // accumulated secondary extent of all layouts we have seen so far, // which are the layouts after this one since we iterate reversed. - *bound.secondary_end_mut(*axes) -= axes.secondary.factor() * extent; + *bound.secondary_end_mut(*axes) -= axes.secondary.factor() * extent.y; // Then, we add this layout's secondary extent to the accumulator. - extent += layout.dimensions.secondary(*axes); + let size = layout.dimensions.generalized(*axes); + extent.x.max_eq(size.x); + extent.y += size.y; } // ------------------------------------------------------------------ // @@ -299,38 +347,26 @@ impl StackLayouter { // into a single finished layout. let mut actions = LayoutActions::new(); - actions.add(LayoutAction::DebugBox(dimensions)); + + if self.ctx.debug { + actions.add(LayoutAction::DebugBox(dimensions)); + } let layouts = std::mem::replace(&mut self.space.layouts, vec![]); - for ((axes, layout), bound) in layouts.into_iter().zip(bounds) { - let LayoutAxes { primary, secondary } = axes; - let size = layout.dimensions.specialized(axes); let alignment = layout.alignment; - // The space in which this layout is aligned is given by it's - // corresponding bound box. - let usable = Size2D::new( - bound.right - bound.left, - bound.bottom - bound.top - ).generalized(axes); + // The space in which this layout is aligned is given by the + // distances between the borders of it's bounding box. + let usable = + Size2D::new(bound.right - bound.left, bound.bottom - bound.top) + .generalized(axes); - let offsets = Size2D { - x: usable.x.anchor(alignment.primary, primary.is_positive()) - - size.x.anchor(alignment.primary, primary.is_positive()), - y: usable.y.anchor(alignment.secondary, secondary.is_positive()) - - size.y.anchor(alignment.secondary, secondary.is_positive()), - }; + let local = usable.anchor(alignment, axes) - size.anchor(alignment, axes); + let pos = Size2D::new(bound.left, bound.top) + local.specialized(axes); - let position = Size2D::new(bound.left, bound.top) - + offsets.specialized(axes); - - println!("pos: {}", position); - println!("usable: {}", usable); - println!("size: {}", size); - - actions.add_layout(position, layout); + actions.add_layout(pos, layout); } self.layouts.push(Layout { @@ -339,6 +375,9 @@ impl StackLayouter { actions: actions.to_vec(), }); + // ------------------------------------------------------------------ // + // Step 5: Start the next space. + self.start_space(self.next_space(), hard); } @@ -352,14 +391,6 @@ impl StackLayouter { fn next_space(&self) -> usize { (self.space.index + 1).min(self.ctx.spaces.len() - 1) } - - // Access the secondary alignment in the current system of axes. - fn secondary_alignment(&mut self) -> &mut Alignment { - match self.ctx.axes.primary.is_horizontal() { - true => &mut self.space.alignment.1, - false => &mut self.space.alignment.0, - } - } } impl Space { @@ -371,8 +402,24 @@ impl Space { size: Size2D::ZERO, usable, extra: Size2D::ZERO, - alignment: (Alignment::Origin, Alignment::Origin), + rulers: Rulers { + top: Alignment::Origin, + bottom: Alignment::Origin, + left: Alignment::Origin, + right: Alignment::Origin, + }, last_spacing: LastSpacing::Hard, } } } + +impl Rulers { + fn get(&mut self, axis: Axis) -> &mut Alignment { + match axis { + Axis::TopToBottom => &mut self.top, + Axis::BottomToTop => &mut self.bottom, + Axis::LeftToRight => &mut self.left, + Axis::RightToLeft => &mut self.right, + } + } +} diff --git a/src/layout/tree.rs b/src/layout/tree.rs index d620739d0..195b6075e 100644 --- a/src/layout/tree.rs +++ b/src/layout/tree.rs @@ -23,6 +23,7 @@ impl<'a, 'p> TreeLayouter<'a, 'p> { spaces: ctx.spaces.clone(), axes: ctx.axes, alignment: ctx.alignment, + debug: ctx.debug, }), style: ctx.style.clone(), ctx, @@ -75,8 +76,9 @@ impl<'a, 'p> TreeLayouter<'a, 'p> { let commands = func.0.layout(LayoutContext { loader: self.ctx.loader, style: &self.style, - top_level: false, spaces, + top_level: false, + debug: true, .. self.ctx })?; diff --git a/src/lib.rs b/src/lib.rs index 368d0cda3..5b5e3b0ce 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,7 +26,7 @@ use toddle::Error as FontError; use crate::func::Scope; use crate::layout::{layout_tree, MultiLayout, LayoutContext}; -use crate::layout::{LayoutAxes, LayoutAlignment, Axis, Alignment}; +use crate::layout::{LayoutAxes, LayoutAlignment}; use crate::layout::{LayoutResult, LayoutSpace, LayoutExpansion}; use crate::syntax::{parse, SyntaxTree, ParseContext, Span, ParseResult}; use crate::style::{LayoutStyle, PageStyle, TextStyle}; @@ -94,7 +94,6 @@ impl<'p> Typesetter<'p> { &tree, LayoutContext { loader: &self.loader, - top_level: true, style: &self.style, spaces: smallvec![LayoutSpace { dimensions: self.style.page.dimensions, @@ -103,6 +102,8 @@ impl<'p> Typesetter<'p> { }], axes: LayoutAxes::default(), alignment: LayoutAlignment::default(), + top_level: true, + debug: false, }, )?) } diff --git a/src/library/boxed.rs b/src/library/boxed.rs index 7c0ea0c62..0428e746e 100644 --- a/src/library/boxed.rs +++ b/src/library/boxed.rs @@ -7,19 +7,25 @@ function! { pub struct Boxed { body: SyntaxTree, map: ExtentMap, + debug: bool, } parse(args, body, ctx) { Boxed { body: parse!(optional: body, ctx).unwrap_or(SyntaxTree::new()), map: ExtentMap::new(&mut args, false)?, + debug: args.get_key_opt::("debug")? + .map(Spanned::value) + .unwrap_or(true), } } layout(self, mut ctx) { use SpecificAxisKind::*; + ctx.debug = self.debug; let space = &mut ctx.spaces[0]; + self.map.apply_with(ctx.axes, |axis, p| { let entity = match axis { Horizontal => { space.expand.horizontal = true; &mut space.dimensions.x }, @@ -27,7 +33,7 @@ function! { }; *entity = p.concretize(*entity) - }); + })?; vec![AddMultiple(layout_tree(&self.body, ctx)?)] } diff --git a/src/size.rs b/src/size.rs index 44e1e6421..b22f14bbf 100644 --- a/src/size.rs +++ b/src/size.rs @@ -6,7 +6,7 @@ use std::iter::Sum; use std::ops::*; use std::str::FromStr; -use crate::layout::{LayoutAxes, Axis, Alignment}; +use crate::layout::{LayoutAxes, LayoutAlignment, Axis, Alignment}; /// A general space type. #[derive(Copy, Clone, PartialEq)] @@ -91,11 +91,11 @@ impl Size { *self = min(*self, other); } - /// The specialized anchor position for an item with the given alignment in a - /// container with a given size along the given axis. - pub fn anchor(&self, alignment: Alignment, positive: bool) -> Size { + /// The anchor position along the given axis for an item with the given + /// alignment in a container with this size. + pub fn anchor(&self, alignment: Alignment, axis: Axis) -> Size { use Alignment::*; - match (positive, alignment) { + match (axis.is_positive(), alignment) { (true, Origin) | (false, End) => Size::ZERO, (_, Center) => *self / 2, (true, End) | (false, Origin) => *self, @@ -219,9 +219,16 @@ impl Size2D { self.y.min_eq(other.y); } - /// Swap the two dimensions. - pub fn swap(&mut self) { - std::mem::swap(&mut self.x, &mut self.y); + /// The anchor position along the given axis for an item with the given + /// alignment in a container with this size. + /// + /// This assumes the size to be generalized such that `x` corresponds to the + /// primary axis. + pub fn anchor(&self, alignment: LayoutAlignment, axes: LayoutAxes) -> Size2D { + Size2D { + x: self.x.anchor(alignment.primary, axes.primary), + y: self.y.anchor(alignment.secondary, axes.secondary), + } } } diff --git a/tests/layouts/stack.typ b/tests/layouts/stack.typ new file mode 100644 index 000000000..934480ca9 --- /dev/null +++ b/tests/layouts/stack.typ @@ -0,0 +1,81 @@ +[page.size: w=5cm, h=5cm] +[page.margins: 0cm] + +// Test 1 +[box: w=1, h=1, debug=false][ + [box][ + [align: center] + [box: ps=3cm, ss=1cm] + [direction: ttb, ltr] + [box: ps=3cm, ss=1cm] + [box: ps=1cm, ss=1cm] + [box: ps=2cm, ss=1cm] + [box: ps=1cm, ss=1cm] + ] +] +[page.break] + +// Test 2 +[box: w=1, h=1, debug=false][ + [align: secondary=top] Top + [align: secondary=center] Center + [align: secondary=bottom] Bottom + [direction: ttb, ltr] + [align: secondary=origin, primary=bottom] + [box: w=1cm, h=1cm] +] +[page.break] + +// Test 3 +[box: w=1, h=1, debug=false][ + [align: center][ + Somelongspacelessword! + [align: left] Some + [align: right] word! + ] +] +[page.break] + +// Test 4 +[box: w=1, h=1, debug=false][ + [direction: ltr, ttb] + [align: center] + [align: secondary=origin] + [box: ps=1cm, ss=1cm] + [align: secondary=center] + [box: ps=3cm, ss=1cm] + [box: ps=4cm, ss=0.5cm] + [align: secondary=end] + [box: ps=2cm, ss=1cm] +] +[page.break] + +// Test 5 +[box: w=1, h=1, debug=false][ + [direction: primary=btt, secondary=ltr] + [align: primary=center, secondary=left] + [box: h=2cm, w=1cm] + + [direction: rtl, btt] + [align: center] + [align: vertical=origin] ORIGIN + [align: vertical=center] CENTER + [align: vertical=end] END +] +[page.break] + +// Test 6 +[box: w=1, h=1, debug=false][ + [box: w=4cm, h=1cm] + + [align: primary=right, secondary=center] CENTER + + [direction: btt, rtl] + [align: primary=center, secondary=origin] + [box: w=0.5cm, h=0.5cm] + [box: w=0.5cm, h=1cm] + [box: w=0.5cm, h=0.5cm] + + [align: primary=origin, secondary=end] + END +] diff --git a/tests/layouts/test.typ b/tests/layouts/test.typ deleted file mode 100644 index 4b55e5565..000000000 --- a/tests/layouts/test.typ +++ /dev/null @@ -1,52 +0,0 @@ -[page.size: w=5cm, h=5cm] -[page.margins: 0cm] - -// Test 1 -// [box][ -// [align: center] -// [box: ps=3cm, ss=1cm] -// [direction: ttb, ltr] -// [box: ps=3cm, ss=1cm] -// [box: ps=1cm, ss=1cm] -// [box: ps=2cm, ss=1cm] -// [box: ps=1cm, ss=1cm] -// ] - -// Test 2 -// [align: secondary=top] Top -// [align: secondary=center] Center -// [align: secondary=bottom] Bottom -// [direction: ttb, ltr] -// [align: primary=bottom] -// [box: w=1cm, h=1cm] - -// Test 3 -// [align: center][ -// Somelongspacelessword! -// [align: left] Some -// [align: right] word! -// ] - -// Test 4: In all combinations, please! -// [direction: ltr, ttb] -// [align: center] -// [align: secondary=origin] -// [box: ps=1cm, ss=1cm] -// [align: secondary=center] -// [box: ps=3cm, ss=1cm] -// [box: ps=4cm, ss=0.5cm] -// [align: secondary=end] -// [box: ps=2cm, ss=1cm] - -[align: primary=left, secondary=center] -[box: w=4cm, h=2cm] - -[direction: primary=btt, secondary=ltr] -[align: primary=center, secondary=left] -[box: h=2cm, w=1cm] - -// [direction: rtl, btt] -// [align: center] -// [align: vertical=origin] ORIGIN -// [align: vertical=center] CENTER -// [align: vertical=end] END diff --git a/tests/render.py b/tests/render.py index 93d59ea88..fe7a1de41 100644 --- a/tests/render.py +++ b/tests/render.py @@ -56,7 +56,6 @@ class MultiboxRenderer: renderer = BoxRenderer(self.fonts, width, height) for i in range(action_count): - if i == 0: continue command = self.content[start + i] renderer.execute(command) @@ -134,7 +133,7 @@ class BoxRenderer: if cmd == 'm': x, y = (pix(float(s)) for s in parts) - self.cursor = (x, y) + self.cursor = [x, y] elif cmd == 'f': index = int(parts[0]) @@ -143,7 +142,9 @@ class BoxRenderer: elif cmd == 'w': text = command[2:] + width = self.draw.textsize(text, font=self.font)[0] self.draw.text(self.cursor, text, (0, 0, 0, 255), font=self.font) + self.cursor[0] += width elif cmd == 'b': x, y = self.cursor