Generalize tree layouter 🌲

This commit is contained in:
Laurenz 2019-11-16 10:37:30 +01:00
parent 0917d89bb8
commit 261ef9e33a
10 changed files with 169 additions and 172 deletions

View File

@ -249,8 +249,8 @@ impl<'d, W: Write> ExportProcess<'d, W> {
}, },
LayoutAction::SetFont(id, size) => { LayoutAction::SetFont(id, size) => {
active_font = (self.font_remap[id], *size); active_font = (self.font_remap[id], size.to_pt());
text.tf(active_font.0 as u32 + 1, *size); text.tf(active_font.0 as u32 + 1, size.to_pt());
} }
LayoutAction::WriteText(string) => { LayoutAction::WriteText(string) => {

View File

@ -12,8 +12,9 @@ pub mod helpers;
/// Useful imports for creating your own functions. /// Useful imports for creating your own functions.
pub mod prelude { pub mod prelude {
pub use crate::func::{Command, CommandList, Function}; pub use crate::func::{Command, CommandList, Function};
pub use crate::layout::{layout_tree, Layout, LayoutContext, MultiLayout}; pub use crate::layout::{layout_tree, Layout, MultiLayout, LayoutContext, LayoutSpace};
pub use crate::layout::{Flow, Alignment, LayoutError, LayoutResult}; pub use crate::layout::{LayoutAxes, AlignedAxis, Axis, Alignment};
pub use crate::layout::{LayoutError, LayoutResult};
pub use crate::syntax::{SyntaxTree, FuncHeader, FuncArgs, Expression, Spanned, Span}; pub use crate::syntax::{SyntaxTree, FuncHeader, FuncArgs, Expression, Spanned, Span};
pub use crate::syntax::{parse, ParseContext, ParseError, ParseResult}; pub use crate::syntax::{parse, ParseContext, ParseError, ParseResult};
pub use crate::size::{Size, Size2D, SizeBox}; pub use crate::size::{Size, Size2D, SizeBox};
@ -88,13 +89,16 @@ where T: Debug + PartialEq + 'static
#[derive(Debug)] #[derive(Debug)]
pub enum Command<'a> { pub enum Command<'a> {
LayoutTree(&'a SyntaxTree), LayoutTree(&'a SyntaxTree),
Add(Layout), Add(Layout),
AddMany(MultiLayout), AddMultiple(MultiLayout),
AddFlex(Layout),
SetAlignment(Alignment),
SetStyle(TextStyle),
FinishLayout,
FinishFlexRun, FinishFlexRun,
FinishFlexLayout,
FinishLayout,
SetStyle(TextStyle),
SetAxes(LayoutAxes),
} }
/// A sequence of commands requested for execution by a function. /// A sequence of commands requested for execution by a function.

View File

@ -4,7 +4,7 @@ use std::fmt::{self, Display, Formatter};
use std::io::{self, Write}; use std::io::{self, Write};
use super::Layout; use super::Layout;
use crate::size::Size2D; use crate::size::{Size, Size2D};
use LayoutAction::*; use LayoutAction::*;
/// A layouting action. /// A layouting action.
@ -13,7 +13,7 @@ pub enum LayoutAction {
/// Move to an absolute position. /// Move to an absolute position.
MoveAbsolute(Size2D), MoveAbsolute(Size2D),
/// Set the font by index and font size. /// Set the font by index and font size.
SetFont(usize, f32), SetFont(usize, Size),
/// Write text starting at the current position. /// Write text starting at the current position.
WriteText(String), WriteText(String),
/// Visualize a box for debugging purposes. /// Visualize a box for debugging purposes.
@ -26,7 +26,7 @@ impl LayoutAction {
pub fn serialize<W: Write>(&self, f: &mut W) -> io::Result<()> { pub fn serialize<W: Write>(&self, f: &mut W) -> io::Result<()> {
match self { match self {
MoveAbsolute(s) => write!(f, "m {:.4} {:.4}", s.x.to_pt(), s.y.to_pt()), MoveAbsolute(s) => write!(f, "m {:.4} {:.4}", s.x.to_pt(), s.y.to_pt()),
SetFont(i, s) => write!(f, "f {} {}", i, s), SetFont(i, s) => write!(f, "f {} {}", i, s.to_pt()),
WriteText(s) => write!(f, "w {}", s), WriteText(s) => write!(f, "w {}", s),
DebugBox(p, s) => write!( DebugBox(p, s) => write!(
f, f,
@ -69,9 +69,9 @@ debug_display!(LayoutAction);
pub struct LayoutActionList { pub struct LayoutActionList {
pub origin: Size2D, pub origin: Size2D,
actions: Vec<LayoutAction>, actions: Vec<LayoutAction>,
active_font: (usize, f32), active_font: (usize, Size),
next_pos: Option<Size2D>, next_pos: Option<Size2D>,
next_font: Option<(usize, f32)>, next_font: Option<(usize, Size)>,
} }
impl LayoutActionList { impl LayoutActionList {
@ -80,7 +80,7 @@ impl LayoutActionList {
LayoutActionList { LayoutActionList {
actions: vec![], actions: vec![],
origin: Size2D::zero(), origin: Size2D::zero(),
active_font: (std::usize::MAX, 0.0), active_font: (std::usize::MAX, Size::zero()),
next_pos: None, next_pos: None,
next_font: None, next_font: None,
} }

View File

@ -75,16 +75,18 @@ impl FlexLayouter {
} }
} }
/// This layouter's context.
pub fn ctx(&self) -> FlexContext {
self.ctx
}
/// Add a sublayout. /// Add a sublayout.
pub fn add(&mut self, layout: Layout) { pub fn add(&mut self, layout: Layout) {
self.units.push(FlexUnit::Boxed(layout)); self.units.push(FlexUnit::Boxed(layout));
} }
/// Add multiple sublayouts from a multi-layout.
pub fn add_multiple(&mut self, layouts: MultiLayout) {
for layout in layouts {
self.add(layout);
}
}
/// Add a space box which can be replaced by a run break. /// Add a space box which can be replaced by a run break.
pub fn add_space(&mut self, space: Size) { pub fn add_space(&mut self, space: Size) {
self.units.push(FlexUnit::Space(space)); self.units.push(FlexUnit::Space(space));
@ -181,6 +183,11 @@ impl FlexLayouter {
Ok(()) Ok(())
} }
/// This layouter's context.
pub fn ctx(&self) -> FlexContext {
self.ctx
}
/// 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()

View File

@ -192,7 +192,7 @@ impl LayoutSpace {
} }
/// The axes along which the content is laid out. /// The axes along which the content is laid out.
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub struct LayoutAxes { pub struct LayoutAxes {
pub primary: AlignedAxis, pub primary: AlignedAxis,
pub secondary: AlignedAxis, pub secondary: AlignedAxis,

View File

@ -1,3 +1,4 @@
use smallvec::smallvec;
use super::*; use super::*;
/// Layouts boxes stack-like. /// Layouts boxes stack-like.
@ -66,7 +67,7 @@ impl StackLayouter {
} }
/// Add multiple sublayouts from a multi-layout. /// Add multiple sublayouts from a multi-layout.
pub fn add_many(&mut self, layouts: MultiLayout) -> LayoutResult<()> { pub fn add_multiple(&mut self, layouts: MultiLayout) -> LayoutResult<()> {
for layout in layouts { for layout in layouts {
self.add(layout)?; self.add(layout)?;
} }
@ -135,7 +136,7 @@ impl StackLayouter {
/// 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.
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.next_space();
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;
@ -151,10 +152,20 @@ impl StackLayouter {
self.usable self.usable
} }
/// The (specialized) remaining area for new layouts in the current space. /// The remaining spaces for new layouts in the current space.
pub fn remaining(&self) -> Size2D { pub fn remaining(&self, shrink_to_fit: bool) -> LayoutSpaces {
Size2D::new(self.usable.x, self.usable.y - self.dimensions.y) let mut spaces = smallvec![LayoutSpace {
.specialized(self.ctx.axes) dimensions: Size2D::new(self.usable.x, self.usable.y - self.dimensions.y)
.specialized(self.ctx.axes),
padding: SizeBox::zero(),
shrink_to_fit,
}];
for space in &self.ctx.spaces[self.next_space()..] {
spaces.push(space.usable_space(shrink_to_fit));
}
spaces
} }
/// Whether this layouter is in its last space. /// Whether this layouter is in its last space.
@ -162,6 +173,10 @@ impl StackLayouter {
self.active_space == self.ctx.spaces.len() - 1 self.active_space == self.ctx.spaces.len() - 1
} }
fn next_space(&self) -> usize {
(self.active_space + 1).min(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

@ -13,16 +13,6 @@ pub struct TextContext<'a, 'p> {
pub style: &'a TextStyle, pub style: &'a TextStyle,
} }
impl<'a, 'p> TextContext<'a, 'p> {
/// Create a text context from a generic layout context.
pub fn from_layout_ctx(ctx: LayoutContext<'a, 'p>) -> TextContext<'a, 'p> {
TextContext {
loader: ctx.loader,
style: ctx.style,
}
}
}
/// Layouts text into a box. /// Layouts text into a box.
/// ///
/// There is no complex layout involved. The text is simply laid out left- /// There is no complex layout involved. The text is simply laid out left-
@ -81,7 +71,7 @@ impl<'a, 'p> TextLayouter<'a, 'p> {
} }
Ok(Layout { Ok(Layout {
dimensions: Size2D::new(self.width, Size::pt(self.ctx.style.font_size)), dimensions: Size2D::new(self.width, self.ctx.style.font_size),
actions: self.actions.into_vec(), actions: self.actions.into_vec(),
debug_render: false, debug_render: false,
}) })
@ -107,15 +97,16 @@ impl<'a, 'p> TextLayouter<'a, 'p> {
let glyph = font let glyph = font
.read_table::<CharMap>()? .read_table::<CharMap>()?
.get(c) .get(c)
.expect("layout text: font should have char"); .expect("select_font: font should have char");
let glyph_width = font let glyph_width = font
.read_table::<HorizontalMetrics>()? .read_table::<HorizontalMetrics>()?
.get(glyph) .get(glyph)
.expect("layout text: font should have glyph") .expect("select_font: font should have glyph")
.advance_width as f32; .advance_width as f32;
let char_width = font_unit_to_size(glyph_width) * self.ctx.style.font_size; let char_width = font_unit_to_size(glyph_width)
* self.ctx.style.font_size.to_pt();
return Ok((index, char_width)); return Ok((index, char_width));
} }

View File

@ -13,8 +13,6 @@ struct TreeLayouter<'a, 'p> {
stack: StackLayouter, stack: StackLayouter,
flex: FlexLayouter, flex: FlexLayouter,
style: Cow<'a, TextStyle>, style: Cow<'a, TextStyle>,
alignment: Alignment,
set_newline: bool,
} }
impl<'a, 'p> TreeLayouter<'a, 'p> { impl<'a, 'p> TreeLayouter<'a, 'p> {
@ -22,51 +20,41 @@ impl<'a, 'p> TreeLayouter<'a, 'p> {
fn new(ctx: LayoutContext<'a, 'p>) -> TreeLayouter<'a, 'p> { fn new(ctx: LayoutContext<'a, 'p>) -> TreeLayouter<'a, 'p> {
TreeLayouter { TreeLayouter {
ctx, ctx,
stack: StackLayouter::new(StackContext::from_layout_ctx(ctx)), stack: StackLayouter::new(StackContext {
spaces: ctx.spaces,
axes: ctx.axes,
}),
flex: FlexLayouter::new(FlexContext { flex: FlexLayouter::new(FlexContext {
space: ctx.space.usable_space(), flex_spacing: flex_spacing(&ctx.style),
followup_spaces: ctx.followup_spaces.map(|s| s.usable_space()), spaces: ctx.spaces.iter().map(|space| space.usable_space(true)).collect(),
shrink_to_fit: true, axes: ctx.axes,
.. FlexContext::from_layout_ctx(ctx, flex_spacing(&ctx.style))
}), }),
style: Cow::Borrowed(ctx.style), style: Cow::Borrowed(ctx.style),
alignment: ctx.alignment,
set_newline: false,
} }
} }
/// Layout the tree into a box. /// Layout a syntax tree.
fn layout(&mut self, tree: &SyntaxTree) -> LayoutResult<()> { fn layout(&mut self, tree: &SyntaxTree) -> LayoutResult<()> {
for node in &tree.nodes { for node in &tree.nodes {
match &node.val { match &node.val {
Node::Text(text) => { Node::Text(text) => {
let layout = self.layout_text(text)?; self.flex.add(layout_text(text, TextContext {
self.flex.add(layout); loader: &self.ctx.loader,
self.set_newline = true; style: &self.style,
})?);
} }
Node::Space => { Node::Space => {
// Only add a space if there was any content before.
if !self.flex.is_empty() { if !self.flex.is_empty() {
let layout = self.layout_text(" ")?; self.flex.add_space(self.style.word_spacing * self.style.font_size);
self.flex.add_glue(layout.dimensions);
} }
} }
// Finish the current flex layouting process.
Node::Newline => { Node::Newline => {
self.finish_flex()?; if !self.flex.is_empty() {
self.finish_paragraph()?;
if self.set_newline {
let space = paragraph_spacing(&self.style);
self.stack.add_space(space);
self.set_newline = false;
} }
self.start_new_flex();
} }
// Toggle the text styles.
Node::ToggleItalics => self.style.to_mut().toggle_class(FontClass::Italic), Node::ToggleItalics => self.style.to_mut().toggle_class(FontClass::Italic),
Node::ToggleBold => self.style.to_mut().toggle_class(FontClass::Bold), Node::ToggleBold => self.style.to_mut().toggle_class(FontClass::Bold),
Node::ToggleMonospace => self.style.to_mut().toggle_class(FontClass::Monospace), Node::ToggleMonospace => self.style.to_mut().toggle_class(FontClass::Monospace),
@ -78,115 +66,98 @@ impl<'a, 'p> TreeLayouter<'a, 'p> {
Ok(()) Ok(())
} }
/// Layout a function.
fn layout_func(&mut self, func: &FuncCall) -> LayoutResult<()> {
// Finish the current flex layout on a copy to find out how
// much space would be remaining if we finished.
let mut lookahead = self.stack.clone();
lookahead.add_multiple(self.flex.clone().finish()?)?;
let spaces = lookahead.remaining(true);
let commands = func.body.val.layout(LayoutContext {
style: &self.style,
spaces,
.. self.ctx
})?;
for command in commands {
self.execute(command)?;
}
Ok(())
}
fn execute(&mut self, command: Command) -> LayoutResult<()> {
match command {
Command::LayoutTree(tree) => self.layout(tree)?,
Command::Add(layout) => self.flex.add(layout),
Command::AddMultiple(layouts) => self.flex.add_multiple(layouts),
Command::FinishFlexRun => self.flex.add_break(),
Command::FinishFlexLayout => self.finish_paragraph()?,
Command::FinishLayout => self.finish_layout(true)?,
Command::SetStyle(style) => *self.style.to_mut() = style,
Command::SetAxes(axes) => {
if axes.secondary != self.ctx.axes.secondary {
self.stack.set_axis(axes.secondary);
} else if axes.primary != self.ctx.axes.primary {
self.flex.set_axis(axes.primary);
}
self.ctx.axes = axes;
}
}
Ok(())
}
/// Finish the layout. /// Finish the layout.
fn finish(mut self) -> LayoutResult<MultiLayout> { fn finish(mut self) -> LayoutResult<MultiLayout> {
self.finish_flex()?; self.finish_flex()?;
Ok(self.stack.finish()) Ok(self.stack.finish())
} }
/// Layout a function. /// Finish the current stack layout.
fn layout_func(&mut self, func: &FuncCall) -> LayoutResult<()> { fn finish_layout(&mut self, include_empty: bool) -> LayoutResult<()> {
// Finish the current flex layout on a copy to find out how self.finish_flex()?;
// much space would be remaining if we finished. self.stack.finish_layout(include_empty);
self.start_new_flex();
let mut lookahead_stack = self.stack.clone();
let layouts = self.flex.clone().finish()?;
lookahead_stack.add_many(layouts)?;
let remaining = lookahead_stack.remaining();
let mut ctx = self.ctx;
ctx.style = &self.style;
ctx.flow = Flow::Vertical;
ctx.shrink_to_fit = true;
ctx.space.dimensions = remaining;
ctx.space.padding = SizeBox::zero();
if let Some(space) = ctx.followup_spaces.as_mut() {
*space = space.usable_space();
}
let commands = func.body.val.layout(ctx)?;
for command in commands {
match command {
Command::LayoutTree(tree) => self.layout(tree)?,
Command::Add(layout) => {
self.finish_flex()?;
self.stack.add(layout)?;
self.set_newline = true;
self.start_new_flex();
}
Command::AddMany(layouts) => {
self.finish_flex()?;
self.stack.add_many(layouts)?;
self.set_newline = true;
self.start_new_flex();
}
Command::AddFlex(layout) => self.flex.add(layout),
Command::SetAlignment(alignment) => {
self.finish_flex()?;
self.alignment = alignment;
self.start_new_flex();
}
Command::SetStyle(style) => *self.style.to_mut() = style,
Command::FinishLayout => {
self.finish_flex()?;
self.stack.finish_layout(true);
self.start_new_flex();
}
Command::FinishFlexRun => self.flex.add_break(),
}
}
Ok(()) Ok(())
} }
/// Add text to the flex layout. If `glue` is true, the text will be a glue /// Finish the current flex layout and add space after it.
/// part in the flex layouter. For details, see [`FlexLayouter`]. fn finish_paragraph(&mut self) -> LayoutResult<()> {
fn layout_text(&mut self, text: &str) -> LayoutResult<Layout> { self.finish_flex()?;
let ctx = TextContext { self.stack.add_space(paragraph_spacing(&self.style));
loader: &self.ctx.loader, self.start_new_flex();
style: &self.style, Ok(())
};
layout_text(text, ctx)
} }
/// Finish the current flex layout and add it the stack. /// Finish the current flex layout and add it the stack.
fn finish_flex(&mut self) -> LayoutResult<()> { fn finish_flex(&mut self) -> LayoutResult<()> {
if self.flex.is_empty() { if !self.flex.is_empty() {
return Ok(()); let layouts = self.flex.finish()?;
self.stack.add_multiple(layouts)?;
} }
let layouts = self.flex.finish()?;
self.stack.add_many(layouts)?;
Ok(()) Ok(())
} }
/// Start a new flex layout. /// Start a new flex layout.
fn start_new_flex(&mut self) { fn start_new_flex(&mut self) {
let mut ctx = self.flex.ctx(); self.flex = FlexLayouter::new(FlexContext {
ctx.space.dimensions = self.stack.remaining(); flex_spacing: flex_spacing(&self.style),
ctx.alignment = self.alignment; spaces: self.stack.remaining(true),
ctx.flex_spacing = flex_spacing(&self.style); axes: self.ctx.axes,
});
self.flex = FlexLayouter::new(ctx);
} }
} }
fn flex_spacing(style: &TextStyle) -> Size { fn flex_spacing(style: &TextStyle) -> Size {
(style.line_spacing - 1.0) * Size::pt(style.font_size) (style.line_spacing - 1.0) * style.font_size
} }
fn paragraph_spacing(style: &TextStyle) -> Size { fn paragraph_spacing(style: &TextStyle) -> Size {
let line_height = Size::pt(style.font_size); (style.paragraph_spacing - 1.0) * style.font_size
let space_factor = style.line_spacing * style.paragraph_spacing - 1.0;
line_height * space_factor
} }

View File

@ -1,6 +1,28 @@
use crate::func::prelude::*; use crate::func::prelude::*;
use Command::*; use Command::*;
/// ↩ `line.break`, `n`: Ends the current line.
#[derive(Debug, PartialEq)]
pub struct Linebreak;
function! {
data: Linebreak,
parse: plain,
layout(_, _) { Ok(commands![FinishFlexRun]) }
}
/// ↕ `paragraph.break`: Ends the current paragraph.
///
/// This has the same effect as two subsequent newlines.
#[derive(Debug, PartialEq)]
pub struct Parbreak;
function! {
data: Parbreak,
parse: plain,
layout(_, _) { Ok(commands![FinishFlexLayout]) }
}
/// 📜 `page.break`: Ends the current page. /// 📜 `page.break`: Ends the current page.
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub struct Pagebreak; pub struct Pagebreak;
@ -8,23 +30,7 @@ pub struct Pagebreak;
function! { function! {
data: Pagebreak, data: Pagebreak,
parse: plain, parse: plain,
layout(_, _) { Ok(commands![FinishLayout]) }
layout(_, _) {
Ok(commands![FinishLayout])
}
}
/// 🔙 `line.break`, `n`: Ends the current line.
#[derive(Debug, PartialEq)]
pub struct Linebreak;
function! {
data: Linebreak,
parse: plain,
layout(_, _) {
Ok(commands![FinishFlexRun])
}
} }
/// 📐 `align`: Aligns content in different ways. /// 📐 `align`: Aligns content in different ways.

View File

@ -13,7 +13,9 @@ pub struct TextStyle {
/// leftmost possible one. /// leftmost possible one.
pub fallback: Vec<FontClass>, pub fallback: Vec<FontClass>,
/// The font size. /// The font size.
pub font_size: f32, pub font_size: Size,
/// The word spacing (as a multiple of the font size).
pub word_spacing: f32,
/// The line spacing (as a multiple of the font size). /// The line spacing (as a multiple of the font size).
pub line_spacing: f32, pub line_spacing: f32,
/// The paragraphs spacing (as a multiple of the font size). /// The paragraphs spacing (as a multiple of the font size).
@ -63,7 +65,8 @@ impl Default for TextStyle {
TextStyle { TextStyle {
classes: vec![Regular], classes: vec![Regular],
fallback: vec![Serif], fallback: vec![Serif],
font_size: 11.0, font_size: Size::pt(11.0),
word_spacing: 0.25,
line_spacing: 1.2, line_spacing: 1.2,
paragraph_spacing: 1.5, paragraph_spacing: 1.5,
} }