Generalize flex layouter 🎯

This commit is contained in:
Laurenz 2019-11-15 19:55:47 +01:00
parent 9473ae61e9
commit 0917d89bb8
5 changed files with 102 additions and 149 deletions

View File

@ -21,58 +21,31 @@ use super::*;
pub struct FlexLayouter { pub struct FlexLayouter {
ctx: FlexContext, ctx: FlexContext,
units: Vec<FlexUnit>, units: Vec<FlexUnit>,
stack: StackLayouter, stack: StackLayouter,
usable_width: Size,
usable: Size,
run: FlexRun, run: FlexRun,
cached_glue: Option<Size2D>, space: Option<Size>,
} }
/// The context for flex layouting. /// The context for flex layouting.
/// ///
/// See [`LayoutContext`] for details about the fields. /// See [`LayoutContext`] for details about the fields.
#[derive(Debug, Copy, Clone)] #[derive(Debug, Clone)]
pub struct FlexContext { pub struct FlexContext {
pub spaces: LayoutSpaces,
pub axes: LayoutAxes,
/// The spacing between two lines of boxes. /// The spacing between two lines of boxes.
pub flex_spacing: Size, pub flex_spacing: Size,
pub alignment: Alignment,
pub space: LayoutSpace,
pub followup_spaces: Option<LayoutSpace>,
pub shrink_to_fit: bool,
}
macro_rules! reuse {
($ctx:expr, $flex_spacing:expr) => (
FlexContext {
flex_spacing: $flex_spacing,
alignment: $ctx.alignment,
space: $ctx.space,
followup_spaces: $ctx.followup_spaces,
shrink_to_fit: $ctx.shrink_to_fit,
}
);
}
impl FlexContext {
/// Create a flex context from a generic layout context.
pub fn from_layout_ctx(ctx: LayoutContext, flex_spacing: Size) -> FlexContext {
reuse!(ctx, flex_spacing)
}
/// Create a flex context from a stack context.
pub fn from_stack_ctx(ctx: StackContext, flex_spacing: Size) -> FlexContext {
reuse!(ctx, flex_spacing)
}
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
enum FlexUnit { enum FlexUnit {
/// A content unit to be arranged flexibly. /// A content unit to be arranged flexibly.
Boxed(Layout), Boxed(Layout),
/// A unit which acts as glue between two [`FlexUnit::Boxed`] units and /// Space between two box units which is only present if there
/// is only present if there was no flow break in between the two /// was no flow break in between the two surrounding units.
/// surrounding boxes. Space(Size),
Glue(Size2D),
/// A forced break of the current flex run. /// A forced break of the current flex run.
Break, Break,
} }
@ -86,18 +59,19 @@ struct FlexRun {
impl FlexLayouter { impl FlexLayouter {
/// Create a new flex layouter. /// Create a new flex layouter.
pub fn new(ctx: FlexContext) -> FlexLayouter { pub fn new(ctx: FlexContext) -> FlexLayouter {
let stack = StackLayouter::new(StackContext {
spaces: ctx.spaces,
axes: ctx.axes,
});
FlexLayouter { FlexLayouter {
ctx, ctx,
units: vec![], units: vec![],
stack,
stack: StackLayouter::new(StackContext::from_flex_ctx(ctx, Flow::Vertical)), usable: stack.usable().x,
run: FlexRun { content: vec![], size: Size2D::zero() },
usable_width: ctx.space.usable().x, space: None,
run: FlexRun {
content: vec![],
size: Size2D::zero()
},
cached_glue: None,
} }
} }
@ -111,12 +85,12 @@ impl FlexLayouter {
self.units.push(FlexUnit::Boxed(layout)); self.units.push(FlexUnit::Boxed(layout));
} }
/// Add a glue box which can be replaced by a line break. /// Add a space box which can be replaced by a run break.
pub fn add_glue(&mut self, glue: Size2D) { pub fn add_space(&mut self, space: Size) {
self.units.push(FlexUnit::Glue(glue)); self.units.push(FlexUnit::Space(space));
} }
/// Add a forced line break. /// Add a forced run break.
pub fn add_break(&mut self) { pub fn add_break(&mut self) {
self.units.push(FlexUnit::Break); self.units.push(FlexUnit::Break);
} }
@ -133,70 +107,71 @@ impl FlexLayouter {
for unit in units { for unit in units {
match unit { match unit {
FlexUnit::Boxed(boxed) => self.layout_box(boxed)?, FlexUnit::Boxed(boxed) => self.layout_box(boxed)?,
FlexUnit::Glue(glue) => self.layout_glue(glue), FlexUnit::Space(space) => {
FlexUnit::Break => self.layout_break()?, self.space = Some(space);
}
FlexUnit::Break => {
self.space = None;
self.finish_run()?;
},
} }
} }
// Finish the last flex run. // Finish the last flex run.
self.finish_run()?; self.finish_run()?;
self.stack.finish() Ok(self.stack.finish())
} }
/// Layout a content box into the current flex run or start a new run if /// Layout a content box into the current flex run or start a new run if
/// it does not fit. /// it does not fit.
fn layout_box(&mut self, boxed: Layout) -> LayoutResult<()> { fn layout_box(&mut self, boxed: Layout) -> LayoutResult<()> {
let glue_width = self.cached_glue.unwrap_or(Size2D::zero()).x; let size = boxed.dimensions.generalized(self.ctx.axes);
let new_line_width = self.run.size.x + glue_width + boxed.dimensions.x;
if self.overflows_line(new_line_width) { let space = self.space.unwrap_or(Size::zero());
self.cached_glue = None; let new_run_size = self.run.size.x + space + size.x;
// If the box does not even fit on its own line, then we try if new_run_size > self.usable {
// it in the next space, or we have to give up if there is none. self.space = None;
if self.overflows_line(boxed.dimensions.x) {
if self.ctx.followup_spaces.is_some() { while size.x > self.usable {
self.stack.finish_layout(true)?; if self.stack.in_last_space() {
return self.layout_box(boxed); Err(LayoutError::NotEnoughSpace("cannot fix box into flex run"))?;
} else {
return Err(LayoutError::NotEnoughSpace("cannot fit box into flex run"));
} }
self.stack.finish_layout(true);
self.usable = self.stack.usable().x;
} }
self.finish_run()?; self.finish_run()?;
} }
self.flush_glue(); if let Some(space) = self.space.take() {
if self.run.size.x > Size::zero() && self.run.size.x + space <= self.usable {
self.run.size.x += space;
}
}
let dimensions = boxed.dimensions;
self.run.content.push((self.run.size.x, boxed)); self.run.content.push((self.run.size.x, boxed));
self.grow_run(dimensions); self.run.size.x += size.x;
self.run.size.y = crate::size::max(self.run.size.y, size.y);
Ok(()) Ok(())
} }
fn layout_glue(&mut self, glue: Size2D) {
self.cached_glue = Some(glue);
}
fn layout_break(&mut self) -> LayoutResult<()> {
self.cached_glue = None;
self.finish_run()
}
/// Finish the current flex run. /// Finish the current flex run.
fn finish_run(&mut self) -> LayoutResult<()> { fn finish_run(&mut self) -> LayoutResult<()> {
self.run.size.y += self.ctx.flex_spacing;
let mut actions = LayoutActionList::new(); let mut actions = LayoutActionList::new();
for (x, layout) in self.run.content.drain(..) { for (x, layout) in self.run.content.drain(..) {
let position = Size2D::with_x(x); let position = Size2D::with_x(x).specialized(self.ctx.axes);
actions.add_layout(position, layout); actions.add_layout(position, layout);
} }
self.run.size.y += self.ctx.flex_spacing;
self.stack.add(Layout { self.stack.add(Layout {
dimensions: self.run.size, dimensions: self.run.size.specialized(self.ctx.axes),
actions: actions.into_vec(), actions: actions.into_vec(),
debug_render: false, debug_render: false,
})?; })?;
@ -206,25 +181,8 @@ impl FlexLayouter {
Ok(()) Ok(())
} }
fn flush_glue(&mut self) {
if let Some(glue) = self.cached_glue.take() {
if self.run.size.x > Size::zero() && !self.overflows_line(self.run.size.x + glue.x) {
self.grow_run(glue);
}
}
}
fn grow_run(&mut self, dimensions: Size2D) {
self.run.size.x += dimensions.x;
self.run.size.y = crate::size::max(self.run.size.y, dimensions.y);
}
/// Whether this layouter contains any items. /// Whether this layouter contains any items.
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
self.units.is_empty() self.units.is_empty()
} }
fn overflows_line(&self, line: Size) -> bool {
line > self.usable_width
}
} }

View File

@ -41,11 +41,6 @@ impl StackLayouter {
} }
} }
/// This layouter's context.
pub fn ctx(&self) -> StackContext {
self.ctx
}
/// Add a sublayout. /// Add a sublayout.
pub fn add(&mut self, layout: Layout) -> LayoutResult<()> { pub fn add(&mut self, layout: Layout) -> LayoutResult<()> {
let size = layout.dimensions.generalized(self.ctx.axes); let size = layout.dimensions.generalized(self.ctx.axes);
@ -53,12 +48,11 @@ impl StackLayouter {
// Search for a suitable space to insert the box. // Search for a suitable space to insert the box.
while !self.usable.fits(new_dimensions) { while !self.usable.fits(new_dimensions) {
if self.active_space == self.ctx.spaces.len() - 1 { if self.in_last_space() {
return Err(LayoutError::NotEnoughSpace("box is to large for stack spaces")); Err(LayoutError::NotEnoughSpace("cannot fit box into stack"))?;
} }
self.finish_layout()?; self.finish_layout(true);
self.start_new_space(true);
new_dimensions = self.size_with(size); new_dimensions = self.size_with(size);
} }
@ -80,33 +74,36 @@ impl StackLayouter {
} }
/// Add space after the last layout. /// Add space after the last layout.
pub fn add_space(&mut self, space: Size) -> LayoutResult<()> { pub fn add_space(&mut self, space: Size) {
if self.dimensions.y + space > self.usable.y { if self.dimensions.y + space > self.usable.y {
self.finish_layout()?; self.finish_layout(false);
self.start_new_space(false);
} else { } else {
self.dimensions.y += space; self.dimensions.y += space;
} }
Ok(())
} }
/// Finish the layouting. /// Finish the layouting.
/// ///
/// The layouter is not consumed by this to prevent ownership problems. /// The layouter is not consumed by this to prevent ownership problems.
/// Nevertheless, it should not be used further. /// Nevertheless, it should not be used further.
pub fn finish(&mut self) -> LayoutResult<MultiLayout> { pub fn finish(&mut self) -> MultiLayout {
if self.include_empty || !self.boxes.is_empty() { if self.include_empty || !self.boxes.is_empty() {
self.finish_layout()?; self.finish_boxes();
} }
Ok(std::mem::replace(&mut self.layouts, MultiLayout::new())) std::mem::replace(&mut self.layouts, MultiLayout::new())
} }
/// Finish the current layout and start a new one in a new space. /// Finish the current layout and start a new one in a new space.
/// ///
/// If `start_new_empty` is true, a new empty layout will be started. Otherwise, /// If `include_empty` is true, the followup layout will even be
/// the new layout only appears once new content is added. /// part of the finished multi-layout if it would be empty.
pub fn finish_layout(&mut self) -> LayoutResult<()> { pub fn finish_layout(&mut self, include_empty: bool) {
self.finish_boxes();
self.start_new_space(include_empty);
}
/// Compose all cached boxes into a layout.
fn finish_boxes(&mut self) {
let mut actions = LayoutActionList::new(); let mut actions = LayoutActionList::new();
let space = self.ctx.spaces[self.active_space]; let space = self.ctx.spaces[self.active_space];
@ -116,7 +113,7 @@ impl StackLayouter {
for (offset, layout_anchor, layout) in self.boxes.drain(..) { for (offset, layout_anchor, layout) in self.boxes.drain(..) {
let general_position = anchor - layout_anchor + Size2D::with_y(offset * factor); let general_position = anchor - layout_anchor + Size2D::with_y(offset * factor);
let position = general_position.specialized(self.ctx.axes) + start; let position = start + general_position.specialized(self.ctx.axes);
actions.add_layout(position, layout); actions.add_layout(position, layout);
} }
@ -130,8 +127,6 @@ impl StackLayouter {
actions: actions.into_vec(), actions: actions.into_vec(),
debug_render: true, debug_render: true,
}); });
Ok(())
} }
/// Set up layouting in the next space. Should be preceded by `finish_layout`. /// Set up layouting in the next space. Should be preceded by `finish_layout`.
@ -139,19 +134,34 @@ impl StackLayouter {
/// If `include_empty` is true, the new empty layout will always be added when /// If `include_empty` is true, the new empty layout will always be added when
/// finishing this stack. Otherwise, the new layout only appears if new /// finishing this stack. Otherwise, the new layout only appears if new
/// content is added to it. /// content is added to it.
pub fn start_new_space(&mut self, include_empty: bool) { fn start_new_space(&mut self, include_empty: bool) {
self.active_space = (self.active_space + 1).min(self.ctx.spaces.len() - 1); self.active_space = (self.active_space + 1).min(self.ctx.spaces.len() - 1);
self.usable = self.ctx.spaces[self.active_space].usable().generalized(self.ctx.axes); self.usable = self.ctx.spaces[self.active_space].usable().generalized(self.ctx.axes);
self.dimensions = start_dimensions(self.usable, self.ctx.axes); self.dimensions = start_dimensions(self.usable, self.ctx.axes);
self.include_empty = include_empty; self.include_empty = include_empty;
} }
/// The remaining space for new layouts. /// This layouter's context.
pub fn ctx(&self) -> StackContext {
self.ctx
}
/// The (generalized) usable area of the current space.
pub fn usable(&self) -> Size2D {
self.usable
}
/// The (specialized) remaining area for new layouts in the current space.
pub fn remaining(&self) -> Size2D { pub fn remaining(&self) -> Size2D {
Size2D::new(self.usable.x, self.usable.y - self.dimensions.y) Size2D::new(self.usable.x, self.usable.y - self.dimensions.y)
.specialized(self.ctx.axes) .specialized(self.ctx.axes)
} }
/// Whether this layouter is in its last space.
pub fn in_last_space(&self) -> bool {
self.active_space == self.ctx.spaces.len() - 1
}
/// The combined size of the so-far included boxes with the other size. /// The combined size of the so-far included boxes with the other size.
fn size_with(&self, other: Size2D) -> Size2D { fn size_with(&self, other: Size2D) -> Size2D {
Size2D { Size2D {

View File

@ -59,7 +59,7 @@ impl<'a, 'p> TreeLayouter<'a, 'p> {
if self.set_newline { if self.set_newline {
let space = paragraph_spacing(&self.style); let space = paragraph_spacing(&self.style);
self.stack.add_space(space)?; self.stack.add_space(space);
self.set_newline = false; self.set_newline = false;
} }
@ -81,7 +81,7 @@ impl<'a, 'p> TreeLayouter<'a, 'p> {
/// Finish the layout. /// Finish the layout.
fn finish(mut self) -> LayoutResult<MultiLayout> { fn finish(mut self) -> LayoutResult<MultiLayout> {
self.finish_flex()?; self.finish_flex()?;
self.stack.finish() Ok(self.stack.finish())
} }
/// Layout a function. /// Layout a function.
@ -136,8 +136,7 @@ impl<'a, 'p> TreeLayouter<'a, 'p> {
Command::FinishLayout => { Command::FinishLayout => {
self.finish_flex()?; self.finish_flex()?;
self.stack.finish_layout()?; self.stack.finish_layout(true);
self.stack.start_new_space(true);
self.start_new_flex(); self.start_new_flex();
} }

View File

@ -17,6 +17,7 @@
pub extern crate toddle; pub extern crate toddle;
use std::cell::RefCell; use std::cell::RefCell;
use smallvec::smallvec;
use toddle::query::{FontLoader, FontProvider, SharedFontLoader}; use toddle::query::{FontLoader, FontProvider, SharedFontLoader};
use crate::func::Scope; use crate::func::Scope;
@ -93,27 +94,22 @@ impl<'p> Typesetter<'p> {
/// Layout a syntax tree and return the produced layout. /// Layout a syntax tree and return the produced layout.
pub fn layout(&self, tree: &SyntaxTree) -> LayoutResult<MultiLayout> { pub fn layout(&self, tree: &SyntaxTree) -> LayoutResult<MultiLayout> {
let space = LayoutSpace { Ok(layout_tree(
dimensions: self.page_style.dimensions,
padding: self.page_style.margins,
};
let pages = layout_tree(
&tree, &tree,
LayoutContext { LayoutContext {
loader: &self.loader, loader: &self.loader,
style: &self.text_style, style: &self.text_style,
space, spaces: smallvec![LayoutSpace {
followup_spaces: Some(space), dimensions: self.page_style.dimensions,
shrink_to_fit: false, padding: self.page_style.margins,
shrink_to_fit: false,
}],
axes: LayoutAxes { axes: LayoutAxes {
primary: AlignedAxis::new(Axis::LeftToRight, Alignment::Left).unwrap(), primary: AlignedAxis::new(Axis::LeftToRight, Alignment::Origin),
secondary: AlignedAxis::new(Axis::TopToBottom, Alignment::Top).unwrap(), secondary: AlignedAxis::new(Axis::TopToBottom, Alignment::Origin),
}, },
}, },
)?; )?)
Ok(pages)
} }
/// Process source code directly into a layout. /// Process source code directly into a layout.

View File

@ -40,16 +40,6 @@ macro_rules! error_type {
}; };
} }
/// Whether an expression matches a pattern.
macro_rules! matches {
($val:expr, $($pattern:tt)*) => (
match $val {
$($pattern)* => true,
_ => false,
}
);
}
/// Create a `Debug` implementation from a `Display` implementation. /// Create a `Debug` implementation from a `Display` implementation.
macro_rules! debug_display { macro_rules! debug_display {
($type:ident) => ( ($type:ident) => (