mirror of
https://github.com/typst/typst
synced 2025-05-14 04:56:26 +08:00
Refactor the main layouter ♻
This commit is contained in:
parent
0727713424
commit
a3c667895e
@ -19,7 +19,6 @@ use crate::layout::{Layout, LayoutAction, MultiLayout};
|
||||
use crate::size::Size;
|
||||
|
||||
/// Exports layouts into _PDFs_.
|
||||
#[derive(Debug)]
|
||||
pub struct PdfExporter {}
|
||||
|
||||
impl PdfExporter {
|
||||
|
@ -1,6 +1,6 @@
|
||||
use super::*;
|
||||
|
||||
/// Flex-layouting of boxes.
|
||||
/// Layouts boxes flex-like.
|
||||
///
|
||||
/// The boxes are arranged in "lines", each line having the height of its
|
||||
/// biggest box. When a box does not fit on a line anymore horizontally,
|
||||
@ -74,9 +74,9 @@ impl FlexLayouter {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a reference to this layouter's context.
|
||||
pub fn ctx(&self) -> &FlexContext {
|
||||
&self.ctx
|
||||
/// This layouter's context.
|
||||
pub fn ctx(&self) -> FlexContext {
|
||||
self.ctx
|
||||
}
|
||||
|
||||
/// Add a sublayout.
|
||||
|
@ -2,7 +2,6 @@
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::io::{self, Write};
|
||||
use std::mem;
|
||||
|
||||
use toddle::query::{FontClass, SharedFontLoader};
|
||||
use toddle::Error as FontError;
|
||||
@ -13,16 +12,23 @@ use crate::style::TextStyle;
|
||||
use crate::syntax::{FuncCall, Node, SyntaxTree};
|
||||
|
||||
mod actions;
|
||||
mod tree;
|
||||
mod flex;
|
||||
mod stacked;
|
||||
mod text;
|
||||
|
||||
pub use actions::{LayoutAction, LayoutActionList};
|
||||
pub use flex::{FlexContext, FlexLayouter};
|
||||
pub use stacked::{StackContext, StackLayouter};
|
||||
pub use text::{layout_text, TextContext};
|
||||
/// Different kinds of layouters (fully re-exported).
|
||||
pub mod layouters {
|
||||
pub use super::tree::layout_tree;
|
||||
pub use super::flex::{FlexLayouter, FlexContext};
|
||||
pub use super::stacked::{StackLayouter, StackContext};
|
||||
pub use super::text::{layout_text, TextContext};
|
||||
}
|
||||
|
||||
/// A box layout has a fixed width and height and composes of actions.
|
||||
pub use actions::{LayoutAction, LayoutActionList};
|
||||
pub use layouters::*;
|
||||
|
||||
/// A sequence of layouting actions inside a box.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Layout {
|
||||
/// The size of the box.
|
||||
@ -50,19 +56,19 @@ impl Layout {
|
||||
}
|
||||
}
|
||||
|
||||
/// A collection of box layouts.
|
||||
/// A collection of layouts.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MultiLayout {
|
||||
pub layouts: Vec<Layout>,
|
||||
}
|
||||
|
||||
impl MultiLayout {
|
||||
/// Create an empty multibox layout.
|
||||
/// Create an empty multi-layout.
|
||||
pub fn new() -> MultiLayout {
|
||||
MultiLayout { layouts: vec![] }
|
||||
}
|
||||
|
||||
/// Extract a single sublayout and panic if this layout does not have
|
||||
/// Extract the single sublayout. This panics if the layout does not have
|
||||
/// exactly one child.
|
||||
pub fn into_single(mut self) -> Layout {
|
||||
if self.layouts.len() != 1 {
|
||||
@ -105,8 +111,7 @@ impl<'a> IntoIterator for &'a MultiLayout {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// The context for layouting.
|
||||
/// The general context for layouting.
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct LayoutContext<'a, 'p> {
|
||||
pub loader: &'a SharedFontLoader<'p>,
|
||||
@ -115,7 +120,7 @@ pub struct LayoutContext<'a, 'p> {
|
||||
pub extra_space: Option<LayoutSpace>,
|
||||
}
|
||||
|
||||
/// Spacial constraints for layouting.
|
||||
/// Spacial layouting constraints.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct LayoutSpace {
|
||||
/// The maximum size of the box to layout in.
|
||||
@ -125,12 +130,12 @@ pub struct LayoutSpace {
|
||||
/// The alignment to use for the content.
|
||||
pub alignment: Alignment,
|
||||
/// Whether to shrink the dimensions to fit the content or the keep the
|
||||
/// original ones.
|
||||
/// dimensions from the layout space.
|
||||
pub shrink_to_fit: bool,
|
||||
}
|
||||
|
||||
impl LayoutSpace {
|
||||
/// The actually usable area.
|
||||
/// The actually usable area (dimensions minus padding).
|
||||
pub fn usable(&self) -> Size2D {
|
||||
Size2D {
|
||||
x: self.dimensions.x - self.padding.left - self.padding.right,
|
||||
@ -146,157 +151,6 @@ pub enum Alignment {
|
||||
Right,
|
||||
}
|
||||
|
||||
pub fn layout_tree(tree: &SyntaxTree, ctx: LayoutContext) -> LayoutResult<MultiLayout> {
|
||||
let mut layouter = Layouter::new(ctx);
|
||||
layouter.layout(tree)?;
|
||||
layouter.finish()
|
||||
}
|
||||
|
||||
/// Transforms a syntax tree into a box layout.
|
||||
struct Layouter<'a, 'p> {
|
||||
ctx: LayoutContext<'a, 'p>,
|
||||
stack_layouter: StackLayouter,
|
||||
flex_layouter: FlexLayouter,
|
||||
style: Cow<'a, TextStyle>,
|
||||
}
|
||||
|
||||
impl<'a, 'p> Layouter<'a, 'p> {
|
||||
/// Create a new layouter.
|
||||
fn new(ctx: LayoutContext<'a, 'p>) -> Layouter<'a, 'p> {
|
||||
Layouter {
|
||||
ctx,
|
||||
stack_layouter: StackLayouter::new(StackContext { space: ctx.space }),
|
||||
flex_layouter: FlexLayouter::new(FlexContext {
|
||||
space: LayoutSpace {
|
||||
dimensions: ctx.space.usable(),
|
||||
padding: SizeBox::zero(),
|
||||
alignment: ctx.space.alignment,
|
||||
shrink_to_fit: true,
|
||||
},
|
||||
flex_spacing: (ctx.style.line_spacing - 1.0) * Size::pt(ctx.style.font_size),
|
||||
}),
|
||||
style: Cow::Borrowed(ctx.style),
|
||||
}
|
||||
}
|
||||
|
||||
/// Layout the tree into a box.
|
||||
fn layout(&mut self, tree: &SyntaxTree) -> LayoutResult<()> {
|
||||
// Walk all nodes and layout them.
|
||||
for node in &tree.nodes {
|
||||
match node {
|
||||
// Layout a single piece of text.
|
||||
Node::Text(text) => self.layout_text(text, false)?,
|
||||
|
||||
// Add a space.
|
||||
Node::Space => {
|
||||
if !self.flex_layouter.is_empty() {
|
||||
self.layout_text(" ", true)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Finish the current flex layout and add it to the box layouter.
|
||||
Node::Newline => {
|
||||
// Finish the current paragraph into a box and add it.
|
||||
self.layout_flex()?;
|
||||
|
||||
// Add some paragraph spacing.
|
||||
let size = Size::pt(self.style.font_size)
|
||||
* (self.style.line_spacing * self.style.paragraph_spacing - 1.0);
|
||||
self.stack_layouter.add_space(size)?;
|
||||
}
|
||||
|
||||
// Toggle the text styles.
|
||||
Node::ToggleItalics => self.style.to_mut().toggle_class(FontClass::Italic),
|
||||
Node::ToggleBold => self.style.to_mut().toggle_class(FontClass::Bold),
|
||||
Node::ToggleMonospace => self.style.to_mut().toggle_class(FontClass::Monospace),
|
||||
|
||||
// Execute a function.
|
||||
Node::Func(func) => self.layout_func(func)?,
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn finish(mut self) -> LayoutResult<MultiLayout> {
|
||||
// If there are remainings, add them to the layout.
|
||||
if !self.flex_layouter.is_empty() {
|
||||
self.layout_flex()?;
|
||||
}
|
||||
|
||||
Ok(MultiLayout {
|
||||
layouts: vec![self.stack_layouter.finish()],
|
||||
})
|
||||
}
|
||||
|
||||
/// Layout a piece of text into a box.
|
||||
fn layout_text(&mut self, text: &str, glue: bool) -> LayoutResult<()> {
|
||||
let boxed = layout_text(
|
||||
text,
|
||||
TextContext {
|
||||
loader: &self.ctx.loader,
|
||||
style: &self.style,
|
||||
},
|
||||
)?;
|
||||
|
||||
if glue {
|
||||
self.flex_layouter.add_glue(boxed);
|
||||
} else {
|
||||
self.flex_layouter.add(boxed);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Finish the current flex run and return the resulting box.
|
||||
fn layout_flex(&mut self) -> LayoutResult<()> {
|
||||
if self.flex_layouter.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut layout = FlexLayouter::new(FlexContext {
|
||||
space: LayoutSpace {
|
||||
dimensions: self.stack_layouter.ctx().space.usable(),
|
||||
padding: SizeBox::zero(),
|
||||
alignment: self.ctx.space.alignment,
|
||||
shrink_to_fit: true,
|
||||
},
|
||||
flex_spacing: (self.style.line_spacing - 1.0) * Size::pt(self.style.font_size),
|
||||
});
|
||||
mem::swap(&mut layout, &mut self.flex_layouter);
|
||||
|
||||
let boxed = layout.finish()?;
|
||||
|
||||
self.stack_layouter.add(boxed)
|
||||
}
|
||||
|
||||
/// Layout a function.
|
||||
fn layout_func(&mut self, func: &FuncCall) -> LayoutResult<()> {
|
||||
let commands = func.body.layout(LayoutContext {
|
||||
loader: &self.ctx.loader,
|
||||
style: &self.style,
|
||||
space: LayoutSpace {
|
||||
dimensions: self.stack_layouter.remaining(),
|
||||
padding: SizeBox::zero(),
|
||||
alignment: self.ctx.space.alignment,
|
||||
shrink_to_fit: true,
|
||||
},
|
||||
extra_space: self.ctx.extra_space,
|
||||
})?;
|
||||
|
||||
for command in commands {
|
||||
match command {
|
||||
Command::Layout(tree) => self.layout(tree)?,
|
||||
Command::Add(layout) => self.stack_layouter.add(layout)?,
|
||||
Command::AddMany(layouts) => self.stack_layouter.add_many(layouts)?,
|
||||
Command::ToggleStyleClass(class) => self.style.to_mut().toggle_class(class),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// The error type for layouting.
|
||||
pub enum LayoutError {
|
||||
/// There is not enough space to add an item.
|
||||
|
@ -1,6 +1,6 @@
|
||||
use super::*;
|
||||
|
||||
/// Stack-like layouting of boxes.
|
||||
/// Layouts boxes stack-like.
|
||||
///
|
||||
/// The boxes are arranged vertically, each layout gettings it's own "line".
|
||||
pub struct StackLayouter {
|
||||
@ -11,7 +11,7 @@ pub struct StackLayouter {
|
||||
cursor: Size2D,
|
||||
}
|
||||
|
||||
/// The context for the [`StackLayouter`].
|
||||
/// The context for stack layouting.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct StackContext {
|
||||
/// The space to layout the boxes in.
|
||||
@ -46,9 +46,9 @@ impl StackLayouter {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a reference to this layouter's context.
|
||||
pub fn ctx(&self) -> &StackContext {
|
||||
&self.ctx
|
||||
/// This layouter's context.
|
||||
pub fn ctx(&self) -> StackContext {
|
||||
self.ctx
|
||||
}
|
||||
|
||||
/// Add a sublayout to the bottom.
|
||||
|
148
src/layout/tree.rs
Normal file
148
src/layout/tree.rs
Normal file
@ -0,0 +1,148 @@
|
||||
use super::*;
|
||||
|
||||
/// Layouts syntax trees into boxes.
|
||||
pub fn layout_tree(tree: &SyntaxTree, ctx: LayoutContext) -> LayoutResult<MultiLayout> {
|
||||
let mut layouter = TreeLayouter::new(ctx);
|
||||
layouter.layout(tree)?;
|
||||
layouter.finish()
|
||||
}
|
||||
|
||||
struct TreeLayouter<'a, 'p> {
|
||||
ctx: LayoutContext<'a, 'p>,
|
||||
stack: StackLayouter,
|
||||
flex: FlexLayouter,
|
||||
style: Cow<'a, TextStyle>,
|
||||
}
|
||||
|
||||
impl<'a, 'p> TreeLayouter<'a, 'p> {
|
||||
/// Create a new layouter.
|
||||
fn new(ctx: LayoutContext<'a, 'p>) -> TreeLayouter<'a, 'p> {
|
||||
TreeLayouter {
|
||||
ctx,
|
||||
stack: StackLayouter::new(StackContext { space: ctx.space }),
|
||||
flex: FlexLayouter::new(FlexContext {
|
||||
space: LayoutSpace {
|
||||
dimensions: ctx.space.usable(),
|
||||
padding: SizeBox::zero(),
|
||||
alignment: ctx.space.alignment,
|
||||
shrink_to_fit: true,
|
||||
},
|
||||
flex_spacing: flex_spacing(&ctx.style),
|
||||
}),
|
||||
style: Cow::Borrowed(ctx.style),
|
||||
}
|
||||
}
|
||||
|
||||
/// Layout the tree into a box.
|
||||
fn layout(&mut self, tree: &SyntaxTree) -> LayoutResult<()> {
|
||||
for node in &tree.nodes {
|
||||
match node {
|
||||
Node::Text(text) => self.layout_text(text, false)?,
|
||||
|
||||
Node::Space => {
|
||||
// Only add a space if there was any content before.
|
||||
if !self.flex.is_empty() {
|
||||
self.layout_text(" ", true)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Finish the current flex layouting process.
|
||||
Node::Newline => {
|
||||
self.layout_flex()?;
|
||||
|
||||
let space = paragraph_spacing(&self.style);
|
||||
self.stack.add_space(space)?;
|
||||
}
|
||||
|
||||
// Toggle the text styles.
|
||||
Node::ToggleItalics => self.style.to_mut().toggle_class(FontClass::Italic),
|
||||
Node::ToggleBold => self.style.to_mut().toggle_class(FontClass::Bold),
|
||||
Node::ToggleMonospace => self.style.to_mut().toggle_class(FontClass::Monospace),
|
||||
|
||||
Node::Func(func) => self.layout_func(func)?,
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Finish the layout.
|
||||
fn finish(mut self) -> LayoutResult<MultiLayout> {
|
||||
// If there are remainings, add them to the layout.
|
||||
if !self.flex.is_empty() {
|
||||
self.layout_flex()?;
|
||||
}
|
||||
|
||||
Ok(MultiLayout {
|
||||
layouts: vec![self.stack.finish()],
|
||||
})
|
||||
}
|
||||
|
||||
/// Add text to the flex layout. If `glue` is true, the text will be a glue
|
||||
/// part in the flex layouter. For details, see [`FlexLayouter`].
|
||||
fn layout_text(&mut self, text: &str, glue: bool) -> LayoutResult<()> {
|
||||
let ctx = TextContext {
|
||||
loader: &self.ctx.loader,
|
||||
style: &self.style,
|
||||
};
|
||||
|
||||
let layout = layout_text(text, ctx)?;
|
||||
|
||||
if glue {
|
||||
self.flex.add_glue(layout);
|
||||
} else {
|
||||
self.flex.add(layout);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Finish the current flex layout and add it the stack.
|
||||
fn layout_flex(&mut self) -> LayoutResult<()> {
|
||||
if self.flex.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut ctx = self.flex.ctx();
|
||||
ctx.space.dimensions = self.stack.remaining();
|
||||
ctx.flex_spacing = flex_spacing(&self.style);
|
||||
|
||||
let next = FlexLayouter::new(ctx);
|
||||
let flex = std::mem::replace(&mut self.flex, next);
|
||||
let boxed = flex.finish()?;
|
||||
|
||||
self.stack.add(boxed)
|
||||
}
|
||||
|
||||
/// Layout a function.
|
||||
fn layout_func(&mut self, func: &FuncCall) -> LayoutResult<()> {
|
||||
let mut ctx = self.ctx;
|
||||
ctx.style = &self.style;
|
||||
ctx.space.dimensions = self.stack.remaining();
|
||||
ctx.space.padding = SizeBox::zero();
|
||||
ctx.space.shrink_to_fit = true;
|
||||
|
||||
let commands = func.body.layout(ctx)?;
|
||||
|
||||
for command in commands {
|
||||
match command {
|
||||
Command::Layout(tree) => self.layout(tree)?,
|
||||
Command::Add(layout) => self.stack.add(layout)?,
|
||||
Command::AddMany(layouts) => self.stack.add_many(layouts)?,
|
||||
Command::ToggleStyleClass(class) => self.style.to_mut().toggle_class(class),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn flex_spacing(style: &TextStyle) -> Size {
|
||||
(style.line_spacing - 1.0) * Size::pt(style.font_size)
|
||||
}
|
||||
|
||||
fn paragraph_spacing(style: &TextStyle) -> Size {
|
||||
let line_height = Size::pt(style.font_size);
|
||||
let space_factor = style.line_spacing * style.paragraph_spacing - 1.0;
|
||||
line_height * space_factor
|
||||
}
|
@ -153,7 +153,7 @@ impl<'s> Parser<'s> {
|
||||
|
||||
/// Parse the arguments to a function.
|
||||
fn parse_func_args(&mut self) -> ParseResult<(Vec<Expression>, HashMap<String, Expression>)> {
|
||||
let mut args = vec![];
|
||||
let mut args = Vec::new();
|
||||
let kwargs = HashMap::new();
|
||||
|
||||
let mut comma = false;
|
||||
|
Loading…
x
Reference in New Issue
Block a user