summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2019-12-11 22:06:54 +0100
committerLaurenz <laurmaedje@gmail.com>2019-12-11 22:06:54 +0100
commita791ef162868c65284903ab479731e0dc9e7a223 (patch)
tree28384c5647086db87c822b186860492dfce23af3
parentd34707a6ae058560140c83af21365884451e9274 (diff)
Pretty good stack layouter ✈
-rw-r--r--src/layout/flex.rs3
-rw-r--r--src/layout/mod.rs6
-rw-r--r--src/layout/stack.rs177
-rw-r--r--src/layout/tree.rs4
-rw-r--r--src/lib.rs5
-rw-r--r--src/library/boxed.rs8
-rw-r--r--src/size.rs23
-rw-r--r--tests/layouts/stack.typ81
-rw-r--r--tests/layouts/test.typ52
-rw-r--r--tests/render.py5
10 files changed, 230 insertions, 134 deletions
diff --git a/src/layout/flex.rs b/src/layout/flex.rs
index fc1a09c0..48853863 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 fe4d3e08..fa3a8866 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 369b1c81..27ca433b 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));
- let layouts = std::mem::replace(&mut self.space.layouts, vec![]);
+ 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);
-
- 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()),
- };
+ // 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 position = Size2D::new(bound.left, bound.top)
- + offsets.specialized(axes);
+ let local = usable.anchor(alignment, axes) - size.anchor(alignment, axes);
+ let pos = Size2D::new(bound.left, bound.top) + local.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 d620739d..195b6075 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 368d0cda..5b5e3b0c 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 7c0ea0c6..0428e746 100644
--- a/src/library/boxed.rs
+++ b/src/library/boxed.rs
@@ -7,19 +7,25 @@ function! {
pub struct Boxed {
body: SyntaxTree,
map: ExtentMap<PSize>,
+ 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::<bool>("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 44e1e642..b22f14bb 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 00000000..934480ca
--- /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 4b55e556..00000000
--- 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 93d59ea8..fe7a1de4 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