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;
|
use crate::size::Size;
|
||||||
|
|
||||||
/// Exports layouts into _PDFs_.
|
/// Exports layouts into _PDFs_.
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct PdfExporter {}
|
pub struct PdfExporter {}
|
||||||
|
|
||||||
impl PdfExporter {
|
impl PdfExporter {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
/// Flex-layouting of boxes.
|
/// Layouts boxes flex-like.
|
||||||
///
|
///
|
||||||
/// The boxes are arranged in "lines", each line having the height of its
|
/// 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,
|
/// 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.
|
/// This layouter's context.
|
||||||
pub fn ctx(&self) -> &FlexContext {
|
pub fn ctx(&self) -> FlexContext {
|
||||||
&self.ctx
|
self.ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a sublayout.
|
/// Add a sublayout.
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::io::{self, Write};
|
use std::io::{self, Write};
|
||||||
use std::mem;
|
|
||||||
|
|
||||||
use toddle::query::{FontClass, SharedFontLoader};
|
use toddle::query::{FontClass, SharedFontLoader};
|
||||||
use toddle::Error as FontError;
|
use toddle::Error as FontError;
|
||||||
@ -13,16 +12,23 @@ use crate::style::TextStyle;
|
|||||||
use crate::syntax::{FuncCall, Node, SyntaxTree};
|
use crate::syntax::{FuncCall, Node, SyntaxTree};
|
||||||
|
|
||||||
mod actions;
|
mod actions;
|
||||||
|
mod tree;
|
||||||
mod flex;
|
mod flex;
|
||||||
mod stacked;
|
mod stacked;
|
||||||
mod text;
|
mod text;
|
||||||
|
|
||||||
pub use actions::{LayoutAction, LayoutActionList};
|
/// Different kinds of layouters (fully re-exported).
|
||||||
pub use flex::{FlexContext, FlexLayouter};
|
pub mod layouters {
|
||||||
pub use stacked::{StackContext, StackLayouter};
|
pub use super::tree::layout_tree;
|
||||||
pub use text::{layout_text, TextContext};
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Layout {
|
pub struct Layout {
|
||||||
/// The size of the box.
|
/// The size of the box.
|
||||||
@ -50,19 +56,19 @@ impl Layout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A collection of box layouts.
|
/// A collection of layouts.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct MultiLayout {
|
pub struct MultiLayout {
|
||||||
pub layouts: Vec<Layout>,
|
pub layouts: Vec<Layout>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MultiLayout {
|
impl MultiLayout {
|
||||||
/// Create an empty multibox layout.
|
/// Create an empty multi-layout.
|
||||||
pub fn new() -> MultiLayout {
|
pub fn new() -> MultiLayout {
|
||||||
MultiLayout { layouts: vec![] }
|
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.
|
/// exactly one child.
|
||||||
pub fn into_single(mut self) -> Layout {
|
pub fn into_single(mut self) -> Layout {
|
||||||
if self.layouts.len() != 1 {
|
if self.layouts.len() != 1 {
|
||||||
@ -105,8 +111,7 @@ impl<'a> IntoIterator for &'a MultiLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The general context for layouting.
|
||||||
/// The context for layouting.
|
|
||||||
#[derive(Copy, Clone)]
|
#[derive(Copy, Clone)]
|
||||||
pub struct LayoutContext<'a, 'p> {
|
pub struct LayoutContext<'a, 'p> {
|
||||||
pub loader: &'a SharedFontLoader<'p>,
|
pub loader: &'a SharedFontLoader<'p>,
|
||||||
@ -115,7 +120,7 @@ pub struct LayoutContext<'a, 'p> {
|
|||||||
pub extra_space: Option<LayoutSpace>,
|
pub extra_space: Option<LayoutSpace>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Spacial constraints for layouting.
|
/// Spacial layouting constraints.
|
||||||
#[derive(Debug, Copy, Clone)]
|
#[derive(Debug, Copy, Clone)]
|
||||||
pub struct LayoutSpace {
|
pub struct LayoutSpace {
|
||||||
/// The maximum size of the box to layout in.
|
/// The maximum size of the box to layout in.
|
||||||
@ -125,12 +130,12 @@ pub struct LayoutSpace {
|
|||||||
/// The alignment to use for the content.
|
/// The alignment to use for the content.
|
||||||
pub alignment: Alignment,
|
pub alignment: Alignment,
|
||||||
/// Whether to shrink the dimensions to fit the content or the keep the
|
/// 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,
|
pub shrink_to_fit: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LayoutSpace {
|
impl LayoutSpace {
|
||||||
/// The actually usable area.
|
/// The actually usable area (dimensions minus padding).
|
||||||
pub fn usable(&self) -> Size2D {
|
pub fn usable(&self) -> Size2D {
|
||||||
Size2D {
|
Size2D {
|
||||||
x: self.dimensions.x - self.padding.left - self.padding.right,
|
x: self.dimensions.x - self.padding.left - self.padding.right,
|
||||||
@ -146,157 +151,6 @@ pub enum Alignment {
|
|||||||
Right,
|
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.
|
/// The error type for layouting.
|
||||||
pub enum LayoutError {
|
pub enum LayoutError {
|
||||||
/// There is not enough space to add an item.
|
/// There is not enough space to add an item.
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
/// Stack-like layouting of boxes.
|
/// Layouts boxes stack-like.
|
||||||
///
|
///
|
||||||
/// The boxes are arranged vertically, each layout gettings it's own "line".
|
/// The boxes are arranged vertically, each layout gettings it's own "line".
|
||||||
pub struct StackLayouter {
|
pub struct StackLayouter {
|
||||||
@ -11,7 +11,7 @@ pub struct StackLayouter {
|
|||||||
cursor: Size2D,
|
cursor: Size2D,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The context for the [`StackLayouter`].
|
/// The context for stack layouting.
|
||||||
#[derive(Debug, Copy, Clone)]
|
#[derive(Debug, Copy, Clone)]
|
||||||
pub struct StackContext {
|
pub struct StackContext {
|
||||||
/// The space to layout the boxes in.
|
/// The space to layout the boxes in.
|
||||||
@ -46,9 +46,9 @@ impl StackLayouter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a reference to this layouter's context.
|
/// This layouter's context.
|
||||||
pub fn ctx(&self) -> &StackContext {
|
pub fn ctx(&self) -> StackContext {
|
||||||
&self.ctx
|
self.ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a sublayout to the bottom.
|
/// 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.
|
/// Parse the arguments to a function.
|
||||||
fn parse_func_args(&mut self) -> ParseResult<(Vec<Expression>, HashMap<String, Expression>)> {
|
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 kwargs = HashMap::new();
|
||||||
|
|
||||||
let mut comma = false;
|
let mut comma = false;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user