mirror of
https://github.com/typst/typst
synced 2025-05-14 17:15:28 +08:00
Implement flex and box layouting 📏
This commit is contained in:
parent
b53ad6b1ec
commit
968e121697
@ -60,7 +60,7 @@ impl Font {
|
|||||||
|
|
||||||
// Create a conversion function between font units and sizes.
|
// Create a conversion function between font units and sizes.
|
||||||
let font_unit_ratio = 1.0 / (head.units_per_em as f32);
|
let font_unit_ratio = 1.0 / (head.units_per_em as f32);
|
||||||
let font_unit_to_size = |x| Size::from_points(font_unit_ratio * x as f32);
|
let font_unit_to_size = |x| Size::points(font_unit_ratio * x as f32);
|
||||||
|
|
||||||
// Find out the name of the font.
|
// Find out the name of the font.
|
||||||
let font_name = name.get_decoded(NameEntry::PostScriptName)
|
let font_name = name.get_decoded(NameEntry::PostScriptName)
|
||||||
|
@ -1,17 +1,18 @@
|
|||||||
//! Definitive layouting of boxes.
|
//! Block-style layouting of boxes.
|
||||||
|
|
||||||
use crate::doc::{Document, Page, TextAction};
|
use crate::doc::{Document, Page, TextAction};
|
||||||
use crate::font::Font;
|
use crate::font::Font;
|
||||||
use super::{Layouter, LayoutContext, Size2D};
|
use crate::size::{Size, Size2D};
|
||||||
|
use super::LayoutSpace;
|
||||||
|
|
||||||
|
|
||||||
/// A box layout has a fixed width and height and consists of actions.
|
/// A box layout has a fixed width and height and composes of actions.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct BoxLayout {
|
pub struct BoxLayout {
|
||||||
/// The size of the box.
|
/// The size of the box.
|
||||||
dimensions: Size2D,
|
pub dimensions: Size2D,
|
||||||
/// The actions composing this layout.
|
/// The actions composing this layout.
|
||||||
actions: Vec<TextAction>,
|
pub actions: Vec<TextAction>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BoxLayout {
|
impl BoxLayout {
|
||||||
@ -28,25 +29,68 @@ impl BoxLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Layouts boxes block-style.
|
/// The context for layouting boxes.
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Copy, Clone)]
|
||||||
pub struct BoxLayouter<'a, 'p> {
|
pub struct BoxContext {
|
||||||
ctx: &'a LayoutContext<'a, 'p>,
|
/// The space to layout the boxes in.
|
||||||
actions: Vec<TextAction>,
|
pub space: LayoutSpace,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, 'p> BoxLayouter<'a, 'p> {
|
/// Layouts boxes block-style.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct BoxLayouter {
|
||||||
|
ctx: BoxContext,
|
||||||
|
actions: Vec<TextAction>,
|
||||||
|
dimensions: Size2D,
|
||||||
|
usable: Size2D,
|
||||||
|
cursor: Size2D,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BoxLayouter {
|
||||||
/// Create a new box layouter.
|
/// Create a new box layouter.
|
||||||
pub fn new(ctx: &'a LayoutContext<'a, 'p>) -> BoxLayouter<'a, 'p> {
|
pub fn new(ctx: BoxContext) -> BoxLayouter {
|
||||||
|
let space = ctx.space;
|
||||||
BoxLayouter {
|
BoxLayouter {
|
||||||
ctx,
|
ctx,
|
||||||
actions: vec![],
|
actions: vec![],
|
||||||
|
dimensions: Size2D::zero(),
|
||||||
|
usable: space.usable(),
|
||||||
|
cursor: Size2D::new(space.padding.left, space.padding.right),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a sublayout.
|
/// Add a sublayout.
|
||||||
pub fn add_box(&mut self, layout: BoxLayout) {
|
pub fn add_box(&mut self, layout: BoxLayout) {
|
||||||
unimplemented!()
|
// In the flow direction (vertical) add the layout and in the second
|
||||||
|
// direction just consider the maximal size of any child layout.
|
||||||
|
let new = Size2D {
|
||||||
|
x: crate::size::max(self.dimensions.x, layout.dimensions.x),
|
||||||
|
y: self.dimensions.y + layout.dimensions.y,
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.overflows(new) {
|
||||||
|
panic!("box layouter: would overflow in add_box");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply the dimensions because they fit.
|
||||||
|
self.dimensions = new;
|
||||||
|
|
||||||
|
// Move all actions into this layout and translate absolute positions.
|
||||||
|
self.actions.push(TextAction::MoveAbsolute(self.cursor));
|
||||||
|
self.actions.extend(super::translate_actions(self.cursor, layout.actions));
|
||||||
|
|
||||||
|
// Adjust the cursor.
|
||||||
|
self.cursor.y += layout.dimensions.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add some space in between two boxes.
|
||||||
|
pub fn add_space(&mut self, space: Size) {
|
||||||
|
if self.overflows(self.dimensions + Size2D::with_y(space)) {
|
||||||
|
panic!("box layouter: would overflow in add_space");
|
||||||
|
}
|
||||||
|
|
||||||
|
self.cursor.y += space;
|
||||||
|
self.dimensions.y += space;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a sublayout at an absolute position.
|
/// Add a sublayout at an absolute position.
|
||||||
@ -54,20 +98,34 @@ impl<'a, 'p> BoxLayouter<'a, 'p> {
|
|||||||
self.actions.push(TextAction::MoveAbsolute(position));
|
self.actions.push(TextAction::MoveAbsolute(position));
|
||||||
self.actions.extend(layout.actions);
|
self.actions.extend(layout.actions);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl Layouter for BoxLayouter<'_, '_> {
|
/// Whether this layouter contains any items.
|
||||||
type Layout = BoxLayout;
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.actions.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
/// Finish the layouting and create a box layout from this.
|
/// The remaining space for new boxes.
|
||||||
fn finish(self) -> BoxLayout {
|
pub fn remaining(&self) -> Size2D {
|
||||||
BoxLayout {
|
Size2D {
|
||||||
dimensions: self.ctx.space.dimensions.clone(),
|
x: self.usable.x,
|
||||||
actions: self.actions
|
y: self.usable.y - self.dimensions.y,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_empty(&self) -> bool {
|
/// Finish the layouting and create a box layout from this.
|
||||||
self.actions.is_empty()
|
pub fn finish(self) -> BoxLayout {
|
||||||
|
BoxLayout {
|
||||||
|
dimensions: if self.ctx.space.shrink_to_fit {
|
||||||
|
self.dimensions.padded(self.ctx.space.padding)
|
||||||
|
} else {
|
||||||
|
self.ctx.space.dimensions
|
||||||
|
},
|
||||||
|
actions: self.actions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the given box is bigger than what we can hold.
|
||||||
|
fn overflows(&self, dimensions: Size2D) -> bool {
|
||||||
|
dimensions.x > self.usable.x || dimensions.y > self.usable.y
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,61 +1,171 @@
|
|||||||
//! Flexible and lazy layouting of boxes.
|
//! Flexible and lazy layouting of boxes.
|
||||||
|
|
||||||
use super::{Layouter, LayoutContext, BoxLayout};
|
use crate::doc::TextAction;
|
||||||
|
use crate::size::Size2D;
|
||||||
|
use super::{LayoutSpace, BoxLayout};
|
||||||
|
|
||||||
|
|
||||||
/// A flex layout consists of a yet unarranged list of boxes.
|
/// A flex layout consists of a yet unarranged list of boxes.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct FlexLayout {
|
pub struct FlexLayout {
|
||||||
/// The sublayouts composing this layout.
|
/// The sublayouts composing this layout.
|
||||||
layouts: Vec<BoxLayout>,
|
pub units: Vec<FlexUnit>,
|
||||||
|
/// The layout space to arrange in.
|
||||||
|
pub ctx: FlexContext,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A unit in a flex layout.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum FlexUnit {
|
||||||
|
/// A content unit to be arranged flexibly.
|
||||||
|
Boxed(BoxLayout),
|
||||||
|
/// A unit which acts as glue between two [`FlexUnit::Boxed`] units and
|
||||||
|
/// is only present if there was no flow break in between the two surrounding boxes.
|
||||||
|
Glue(BoxLayout),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FlexLayout {
|
impl FlexLayout {
|
||||||
/// Compute the layout.
|
|
||||||
pub fn into_box(self) -> BoxLayout {
|
|
||||||
// TODO: Do the justification.
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Layouts boxes next to each other (inline-style) lazily.
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct FlexLayouter<'a, 'p> {
|
|
||||||
ctx: &'a LayoutContext<'a, 'p>,
|
|
||||||
layouts: Vec<BoxLayout>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, 'p> FlexLayouter<'a, 'p> {
|
|
||||||
/// Create a new flex layouter.
|
/// Create a new flex layouter.
|
||||||
pub fn new(ctx: &'a LayoutContext<'a, 'p>) -> FlexLayouter<'a, 'p> {
|
pub fn new(ctx: FlexContext) -> FlexLayout {
|
||||||
FlexLayouter {
|
FlexLayout {
|
||||||
ctx,
|
ctx,
|
||||||
layouts: vec![],
|
units: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a sublayout.
|
/// Add a sublayout.
|
||||||
pub fn add_box(&mut self, layout: BoxLayout) {
|
pub fn add_box(&mut self, layout: BoxLayout) {
|
||||||
self.layouts.push(layout);
|
self.units.push(FlexUnit::Boxed(layout));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a glue layout which can be replaced by a line break.
|
||||||
|
pub fn add_glue(&mut self, glue: BoxLayout) {
|
||||||
|
self.units.push(FlexUnit::Glue(glue));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add all sublayouts of another flex layout.
|
/// Add all sublayouts of another flex layout.
|
||||||
pub fn add_flexible(&mut self, layout: FlexLayout) {
|
pub fn add_flexible(&mut self, layout: FlexLayout) {
|
||||||
self.layouts.extend(layout.layouts);
|
self.units.extend(layout.units);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether this layouter contains any items.
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.units.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute the justified layout.
|
||||||
|
pub fn into_box(self) -> BoxLayout {
|
||||||
|
FlexFinisher::new(self).finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Layouter for FlexLayouter<'_, '_> {
|
/// The context for flex layouting.
|
||||||
type Layout = FlexLayout;
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub struct FlexContext {
|
||||||
|
/// The space to layout the boxes in.
|
||||||
|
pub space: LayoutSpace,
|
||||||
|
/// The flex spacing (like line spacing).
|
||||||
|
pub flex_spacing: f32,
|
||||||
|
}
|
||||||
|
|
||||||
/// Finish the layouting and create a flexible layout from this.
|
/// Finishes a flex layout by justifying the positions of the individual boxes.
|
||||||
fn finish(self) -> FlexLayout {
|
#[derive(Debug)]
|
||||||
FlexLayout {
|
struct FlexFinisher {
|
||||||
layouts: self.layouts
|
units: Vec<FlexUnit>,
|
||||||
|
ctx: FlexContext,
|
||||||
|
actions: Vec<TextAction>,
|
||||||
|
dimensions: Size2D,
|
||||||
|
usable: Size2D,
|
||||||
|
cursor: Size2D,
|
||||||
|
line: Size2D,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FlexFinisher {
|
||||||
|
/// Create the finisher from the layout.
|
||||||
|
fn new(layout: FlexLayout) -> FlexFinisher {
|
||||||
|
let space = layout.ctx.space;
|
||||||
|
FlexFinisher {
|
||||||
|
units: layout.units,
|
||||||
|
ctx: layout.ctx,
|
||||||
|
actions: vec![],
|
||||||
|
dimensions: Size2D::zero(),
|
||||||
|
usable: space.usable(),
|
||||||
|
cursor: Size2D::new(space.padding.left, space.padding.top),
|
||||||
|
line: Size2D::zero(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_empty(&self) -> bool {
|
/// Finish the flex layout into the justified box layout.
|
||||||
self.layouts.is_empty()
|
fn finish(mut self) -> BoxLayout {
|
||||||
|
// Move the units out of the layout.
|
||||||
|
let units = self.units;
|
||||||
|
self.units = vec![];
|
||||||
|
|
||||||
|
// Arrange the units.
|
||||||
|
for unit in units {
|
||||||
|
match unit {
|
||||||
|
FlexUnit::Boxed(boxed) => self.boxed(boxed),
|
||||||
|
FlexUnit::Glue(glue) => self.glue(glue),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush everything to get the correct dimensions.
|
||||||
|
self.newline();
|
||||||
|
|
||||||
|
BoxLayout {
|
||||||
|
dimensions: if self.ctx.space.shrink_to_fit {
|
||||||
|
self.dimensions.padded(self.ctx.space.padding)
|
||||||
|
} else {
|
||||||
|
self.ctx.space.dimensions
|
||||||
|
},
|
||||||
|
actions: self.actions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Layout the box.
|
||||||
|
fn boxed(&mut self, boxed: BoxLayout) {
|
||||||
|
// Move to the next line if necessary.
|
||||||
|
if self.line.x + boxed.dimensions.x > self.usable.x {
|
||||||
|
// If it still does not fit, we stand no chance.
|
||||||
|
if boxed.dimensions.x > self.usable.x {
|
||||||
|
panic!("flex layouter: box is to wide");
|
||||||
|
}
|
||||||
|
|
||||||
|
self.newline();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.append(boxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Layout the glue.
|
||||||
|
fn glue(&mut self, glue: BoxLayout) {
|
||||||
|
// Only add the glue if it fits on the line, otherwise move to the next line.
|
||||||
|
if self.line.x + glue.dimensions.x > self.usable.x {
|
||||||
|
self.newline();
|
||||||
|
} else {
|
||||||
|
self.append(glue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Append a box to the layout without checking anything.
|
||||||
|
fn append(&mut self, layout: BoxLayout) {
|
||||||
|
// Move all actions into this layout and translate absolute positions.
|
||||||
|
self.actions.push(TextAction::MoveAbsolute(self.cursor));
|
||||||
|
self.actions.extend(super::translate_actions(self.cursor, layout.actions));
|
||||||
|
|
||||||
|
// Adjust the sizes.
|
||||||
|
self.line.x += layout.dimensions.x;
|
||||||
|
self.line.y = crate::size::max(self.line.y, layout.dimensions.y);
|
||||||
|
self.cursor.x += layout.dimensions.x;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move to the next line.
|
||||||
|
fn newline(&mut self) {
|
||||||
|
self.line.y *= self.ctx.flex_spacing;
|
||||||
|
self.dimensions.x = crate::size::max(self.dimensions.x, self.line.x);
|
||||||
|
self.dimensions.y += self.line.y;
|
||||||
|
self.cursor.x = self.ctx.space.padding.left;
|
||||||
|
self.cursor.y += self.line.y;
|
||||||
|
self.line = Size2D::zero();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,28 +1,20 @@
|
|||||||
//! The layouting engine.
|
//! The layouting engine.
|
||||||
|
|
||||||
|
use crate::doc::TextAction;
|
||||||
use crate::font::{FontLoader, FontError};
|
use crate::font::{FontLoader, FontError};
|
||||||
use crate::size::{Size2D, SizeBox};
|
use crate::size::{Size, Size2D, SizeBox};
|
||||||
use crate::syntax::{SyntaxTree, Node};
|
use crate::syntax::{SyntaxTree, Node};
|
||||||
use crate::style::TextStyle;
|
use crate::style::TextStyle;
|
||||||
|
|
||||||
mod boxed;
|
use self::flex::{FlexLayout, FlexContext};
|
||||||
mod flex;
|
use self::boxed::{BoxLayout, BoxContext, BoxLayouter};
|
||||||
|
use self::text::TextContext;
|
||||||
|
|
||||||
pub use flex::{FlexLayout, FlexLayouter};
|
pub mod text;
|
||||||
pub use boxed::{BoxLayout, BoxLayouter};
|
pub mod boxed;
|
||||||
|
pub mod flex;
|
||||||
|
|
||||||
|
|
||||||
/// Types that layout components and can be finished into some kind of layout.
|
|
||||||
pub trait Layouter {
|
|
||||||
type Layout;
|
|
||||||
|
|
||||||
/// Finish the layouting and create the layout from this.
|
|
||||||
fn finish(self) -> Self::Layout;
|
|
||||||
|
|
||||||
/// Whether this layouter contains any items.
|
|
||||||
fn is_empty(&self) -> bool;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A collection of layouted content.
|
/// A collection of layouted content.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum Layout {
|
pub enum Layout {
|
||||||
@ -44,54 +36,152 @@ pub struct LayoutContext<'a, 'p> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Spacial constraints for layouting.
|
/// Spacial constraints for layouting.
|
||||||
#[derive(Debug, 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.
|
||||||
pub dimensions: Size2D,
|
pub dimensions: Size2D,
|
||||||
/// Padding that should be respected on each side.
|
/// Padding that should be respected on each side.
|
||||||
pub padding: SizeBox,
|
pub padding: SizeBox,
|
||||||
|
/// Whether to shrink the dimensions to fit the content or the keep the
|
||||||
|
/// original ones.
|
||||||
|
pub shrink_to_fit: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LayoutSpace {
|
||||||
|
/// The actually usable area.
|
||||||
|
pub fn usable(&self) -> Size2D {
|
||||||
|
Size2D {
|
||||||
|
x: self.dimensions.x - self.padding.left - self.padding.right,
|
||||||
|
y: self.dimensions.y - self.padding.top - self.padding.bottom,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Layout a syntax tree in a given context.
|
/// Layout a syntax tree in a given context.
|
||||||
pub fn layout(tree: &SyntaxTree, ctx: &LayoutContext) -> LayoutResult<BoxLayout> {
|
pub fn layout(tree: &SyntaxTree, ctx: &LayoutContext) -> LayoutResult<BoxLayout> {
|
||||||
// The top-level layouter and the sub-level layouter.
|
Layouter::new(tree, ctx).layout()
|
||||||
let mut box_layouter = BoxLayouter::new(ctx);
|
}
|
||||||
let mut flex_layouter = FlexLayouter::new(ctx);
|
|
||||||
|
|
||||||
// The current text style.
|
/// Transforms a syntax tree into a box layout.
|
||||||
let mut italic = false;
|
#[derive(Debug)]
|
||||||
let mut bold = false;
|
struct Layouter<'a, 'p> {
|
||||||
|
tree: &'a SyntaxTree,
|
||||||
|
box_layouter: BoxLayouter,
|
||||||
|
flex_layout: FlexLayout,
|
||||||
|
flex_ctx: FlexContext,
|
||||||
|
text_ctx: TextContext<'a, 'p>,
|
||||||
|
}
|
||||||
|
|
||||||
// Walk all nodes and layout them.
|
impl<'a, 'p> Layouter<'a, 'p> {
|
||||||
for node in &tree.nodes {
|
/// Create a new layouter.
|
||||||
match node {
|
fn new(tree: &'a SyntaxTree, ctx: &LayoutContext<'a, 'p>) -> Layouter<'a, 'p> {
|
||||||
Node::Text(text) => {
|
// The top-level context for arranging paragraphs.
|
||||||
unimplemented!()
|
let box_ctx = BoxContext { space: ctx.space };
|
||||||
},
|
|
||||||
Node::Space => {
|
// The sub-level context for arranging pieces of text.
|
||||||
unimplemented!()
|
let flex_ctx = FlexContext {
|
||||||
},
|
space: LayoutSpace {
|
||||||
Node::Newline => {
|
dimensions: ctx.space.usable(),
|
||||||
unimplemented!()
|
padding: SizeBox::zero(),
|
||||||
|
shrink_to_fit: true,
|
||||||
},
|
},
|
||||||
|
flex_spacing: ctx.style.line_spacing,
|
||||||
|
};
|
||||||
|
|
||||||
// Toggle the text styles.
|
// The mutable context for layouting single pieces of text.
|
||||||
Node::ToggleItalics => italic = !italic,
|
let text_ctx = TextContext {
|
||||||
Node::ToggleBold => bold = !bold,
|
loader: &ctx.loader,
|
||||||
|
style: ctx.style.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
Node::Func(func) => {
|
Layouter {
|
||||||
unimplemented!()
|
tree,
|
||||||
}
|
box_layouter: BoxLayouter::new(box_ctx),
|
||||||
|
flex_layout: FlexLayout::new(flex_ctx),
|
||||||
|
flex_ctx,
|
||||||
|
text_ctx,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there are remainings, add them to the layout.
|
/// Layout the tree into a box.
|
||||||
if !flex_layouter.is_empty() {
|
fn layout(mut self) -> LayoutResult<BoxLayout> {
|
||||||
let boxed = flex_layouter.finish().into_box();
|
// Walk all nodes and layout them.
|
||||||
box_layouter.add_box(boxed);
|
for node in &self.tree.nodes {
|
||||||
|
match node {
|
||||||
|
// Layout a single piece of text.
|
||||||
|
Node::Text(text) => {
|
||||||
|
let boxed = self::text::layout(text, &self.text_ctx)?;
|
||||||
|
self.flex_layout.add_box(boxed);
|
||||||
|
},
|
||||||
|
Node::Space => {
|
||||||
|
if !self.flex_layout.is_empty() {
|
||||||
|
let boxed = self::text::layout(" ", &self.text_ctx)?;
|
||||||
|
self.flex_layout.add_glue(boxed);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Finish the current flex layout and add it to the box layouter.
|
||||||
|
// Then start a new flex layouting process.
|
||||||
|
Node::Newline => {
|
||||||
|
// Finish the current paragraph into a box and add it.
|
||||||
|
self.add_paragraph_spacing();
|
||||||
|
let boxed = self.flex_layout.into_box();
|
||||||
|
self.box_layouter.add_box(boxed);
|
||||||
|
|
||||||
|
// Create a fresh flex layout for the next paragraph.
|
||||||
|
self.flex_ctx.space.dimensions = self.box_layouter.remaining();
|
||||||
|
self.flex_layout = FlexLayout::new(self.flex_ctx);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Toggle the text styles.
|
||||||
|
Node::ToggleItalics => self.text_ctx.style.italic = !self.text_ctx.style.italic,
|
||||||
|
Node::ToggleBold => self.text_ctx.style.bold = !self.text_ctx.style.bold,
|
||||||
|
|
||||||
|
// Execute a function.
|
||||||
|
Node::Func(_) => unimplemented!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there are remainings, add them to the layout.
|
||||||
|
if !self.flex_layout.is_empty() {
|
||||||
|
self.add_paragraph_spacing();
|
||||||
|
let boxed = self.flex_layout.into_box();
|
||||||
|
self.box_layouter.add_box(boxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(self.box_layouter.finish())
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(box_layouter.finish())
|
/// Add the spacing between two paragraphs.
|
||||||
|
fn add_paragraph_spacing(&mut self) {
|
||||||
|
let size = Size::points(self.text_ctx.style.font_size)
|
||||||
|
* (self.text_ctx.style.line_spacing * self.text_ctx.style.paragraph_spacing - 1.0);
|
||||||
|
self.box_layouter.add_space(size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Translate a stream of text actions by an offset.
|
||||||
|
pub fn translate_actions<I>(offset: Size2D, actions: I) -> TranslatedActions<I::IntoIter>
|
||||||
|
where I: IntoIterator<Item=TextAction> {
|
||||||
|
TranslatedActions { offset, iter: actions.into_iter() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An iterator over the translated text actions, created by [`translate_actions`].
|
||||||
|
pub struct TranslatedActions<I> where I: Iterator<Item=TextAction> {
|
||||||
|
offset: Size2D,
|
||||||
|
iter: I,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<I> Iterator for TranslatedActions<I> where I: Iterator<Item=TextAction> {
|
||||||
|
type Item = TextAction;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<TextAction> {
|
||||||
|
use TextAction::*;
|
||||||
|
self.iter.next().map(|action| match action {
|
||||||
|
MoveAbsolute(pos) => MoveAbsolute(pos + self.offset),
|
||||||
|
a => a,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The error type for layouting.
|
/// The error type for layouting.
|
||||||
|
62
src/layout/text.rs
Normal file
62
src/layout/text.rs
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
//! Layouting of text into boxes.
|
||||||
|
|
||||||
|
use crate::doc::TextAction;
|
||||||
|
use crate::font::FontQuery;
|
||||||
|
use crate::size::{Size, Size2D};
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
|
||||||
|
/// The context for text layouting.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct TextContext<'a, 'p> {
|
||||||
|
/// Loads fonts matching queries.
|
||||||
|
pub loader: &'a FontLoader<'p>,
|
||||||
|
/// Base style to set text with.
|
||||||
|
pub style: TextStyle,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Layout one piece of text without any breaks as one continous box.
|
||||||
|
pub fn layout(text: &str, ctx: &TextContext) -> LayoutResult<BoxLayout> {
|
||||||
|
let mut actions = Vec::new();
|
||||||
|
let mut active_font = std::usize::MAX;
|
||||||
|
let mut buffer = String::new();
|
||||||
|
let mut width = Size::zero();
|
||||||
|
|
||||||
|
// Walk the characters.
|
||||||
|
for character in text.chars() {
|
||||||
|
// Retrieve the best font for this character.
|
||||||
|
let (index, font) = ctx.loader.get(FontQuery {
|
||||||
|
families: ctx.style.font_families.clone(),
|
||||||
|
italic: ctx.style.italic,
|
||||||
|
bold: ctx.style.bold,
|
||||||
|
character,
|
||||||
|
}).ok_or_else(|| LayoutError::NoSuitableFont(character))?;
|
||||||
|
|
||||||
|
// Add the char width to the total box width.
|
||||||
|
let char_width = font.widths[font.map(character) as usize] * ctx.style.font_size;
|
||||||
|
width += char_width;
|
||||||
|
|
||||||
|
// Change the font if necessary.
|
||||||
|
if active_font != index {
|
||||||
|
if !buffer.is_empty() {
|
||||||
|
actions.push(TextAction::WriteText(buffer));
|
||||||
|
buffer = String::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
actions.push(TextAction::SetFont(index, ctx.style.font_size));
|
||||||
|
active_font = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.push(character);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the remaining characters.
|
||||||
|
if !buffer.is_empty() {
|
||||||
|
actions.push(TextAction::WriteText(buffer));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(BoxLayout {
|
||||||
|
dimensions: Size2D::new(width, Size::points(ctx.style.font_size)),
|
||||||
|
actions,
|
||||||
|
})
|
||||||
|
}
|
@ -26,11 +26,10 @@
|
|||||||
//! // (two sans-serif fonts and a fallback for the emoji).
|
//! // (two sans-serif fonts and a fallback for the emoji).
|
||||||
//! let mut typesetter = Typesetter::new();
|
//! let mut typesetter = Typesetter::new();
|
||||||
//! typesetter.add_font_provider(FileSystemFontProvider::new("../fonts", vec![
|
//! typesetter.add_font_provider(FileSystemFontProvider::new("../fonts", vec![
|
||||||
//! ("CMU-SansSerif-Regular.ttf", font_info!(["Computer Modern", SansSerif])),
|
//! ("CMU-Serif-Regular.ttf", font_info!(["Computer Modern", Serif])),
|
||||||
//! ("CMU-SansSerif-Italic.ttf", font_info!(["Computer Modern", SansSerif], italic)),
|
//! ("CMU-Serif-Italic.ttf", font_info!(["Computer Modern", Serif], italic)),
|
||||||
//! ("NotoEmoji-Regular.ttf", font_info!(["NotoEmoji", "Noto", SansSerif, Serif, Monospace])),
|
//! ("NotoEmoji-Regular.ttf", font_info!(["NotoEmoji", "Noto", SansSerif, Serif, Monospace])),
|
||||||
//! ]));
|
//! ]));
|
||||||
//!
|
|
||||||
//! // Typeset the source code into a document.
|
//! // Typeset the source code into a document.
|
||||||
//! let document = typesetter.typeset(src).unwrap();
|
//! let document = typesetter.typeset(src).unwrap();
|
||||||
//!
|
//!
|
||||||
@ -49,7 +48,8 @@ use crate::doc::Document;
|
|||||||
use crate::font::{Font, FontLoader, FontProvider};
|
use crate::font::{Font, FontLoader, FontProvider};
|
||||||
use crate::func::Scope;
|
use crate::func::Scope;
|
||||||
use crate::parsing::{parse, ParseContext, ParseResult, ParseError};
|
use crate::parsing::{parse, ParseContext, ParseResult, ParseError};
|
||||||
use crate::layout::{layout, LayoutContext, LayoutSpace, LayoutError, LayoutResult, BoxLayout};
|
use crate::layout::{layout, LayoutContext, LayoutSpace, LayoutError, LayoutResult};
|
||||||
|
use crate::layout::boxed::BoxLayout;
|
||||||
use crate::style::{PageStyle, TextStyle};
|
use crate::style::{PageStyle, TextStyle};
|
||||||
use crate::syntax::SyntaxTree;
|
use crate::syntax::SyntaxTree;
|
||||||
|
|
||||||
@ -125,6 +125,7 @@ impl<'p> Typesetter<'p> {
|
|||||||
space: LayoutSpace {
|
space: LayoutSpace {
|
||||||
dimensions: self.page_style.dimensions,
|
dimensions: self.page_style.dimensions,
|
||||||
padding: self.page_style.margins,
|
padding: self.page_style.margins,
|
||||||
|
shrink_to_fit: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
58
src/size.rs
58
src/size.rs
@ -14,7 +14,7 @@ pub struct Size {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A position or extent in 2-dimensional space.
|
/// A position or extent in 2-dimensional space.
|
||||||
#[derive(Copy, Clone, PartialEq, PartialOrd, Default)]
|
#[derive(Copy, Clone, PartialEq, Default)]
|
||||||
pub struct Size2D {
|
pub struct Size2D {
|
||||||
/// The horizontal coordinate.
|
/// The horizontal coordinate.
|
||||||
pub x: Size,
|
pub x: Size,
|
||||||
@ -23,7 +23,7 @@ pub struct Size2D {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A size in four directions.
|
/// A size in four directions.
|
||||||
#[derive(Copy, Clone, PartialEq, PartialOrd, Default)]
|
#[derive(Copy, Clone, PartialEq, Default)]
|
||||||
pub struct SizeBox {
|
pub struct SizeBox {
|
||||||
/// The left extent.
|
/// The left extent.
|
||||||
pub left: Size,
|
pub left: Size,
|
||||||
@ -38,23 +38,23 @@ pub struct SizeBox {
|
|||||||
impl Size {
|
impl Size {
|
||||||
/// Create a zeroed size.
|
/// Create a zeroed size.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn zero() -> Size { Size { points: 0.0 } }
|
pub fn zero() -> Size { Size::default() }
|
||||||
|
|
||||||
/// Create a size from an amount of points.
|
/// Create a size from an amount of points.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn from_points(points: f32) -> Size { Size { points } }
|
pub fn points(points: f32) -> Size { Size { points } }
|
||||||
|
|
||||||
/// Create a size from an amount of inches.
|
/// Create a size from an amount of inches.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn from_inches(inches: f32) -> Size { Size { points: 72.0 * inches } }
|
pub fn inches(inches: f32) -> Size { Size { points: 72.0 * inches } }
|
||||||
|
|
||||||
/// Create a size from an amount of millimeters.
|
/// Create a size from an amount of millimeters.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn from_mm(mm: f32) -> Size { Size { points: 2.83465 * mm } }
|
pub fn mm(mm: f32) -> Size { Size { points: 2.83465 * mm } }
|
||||||
|
|
||||||
/// Create a size from an amount of centimeters.
|
/// Create a size from an amount of centimeters.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn from_cm(cm: f32) -> Size { Size { points: 28.3465 * cm } }
|
pub fn cm(cm: f32) -> Size { Size { points: 28.3465 * cm } }
|
||||||
|
|
||||||
/// Convert this size into points.
|
/// Convert this size into points.
|
||||||
#[inline]
|
#[inline]
|
||||||
@ -74,13 +74,30 @@ impl Size {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Size2D {
|
impl Size2D {
|
||||||
/// Create a new 2D vector from two sizes.
|
/// Create a new vector from two sizes.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn new(x: Size, y: Size) -> Size2D { Size2D { x, y } }
|
pub fn new(x: Size, y: Size) -> Size2D { Size2D { x, y } }
|
||||||
|
|
||||||
/// Create a zeroed vector.
|
/// Create a vector with all set to zero.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn zero() -> Size2D { Size2D { x: Size::zero(), y: Size::zero() } }
|
pub fn zero() -> Size2D { Size2D::default() }
|
||||||
|
|
||||||
|
/// Create a new vector with `x` set to zero and `y` to a value.
|
||||||
|
#[inline]
|
||||||
|
pub fn with_x(x: Size) -> Size2D { Size2D { x, y: Size::zero() } }
|
||||||
|
|
||||||
|
/// Create a new vector with `y` set to zero and `x` to a value.
|
||||||
|
#[inline]
|
||||||
|
pub fn with_y(y: Size) -> Size2D { Size2D { x: Size::zero(), y } }
|
||||||
|
|
||||||
|
/// Return a [`Size2D`] padded by the paddings of the given box.
|
||||||
|
#[inline]
|
||||||
|
pub fn padded(&self, padding: SizeBox) -> Size2D {
|
||||||
|
Size2D {
|
||||||
|
x: self.x + padding.left + padding.right,
|
||||||
|
y: self.y + padding.top + padding.bottom,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SizeBox {
|
impl SizeBox {
|
||||||
@ -90,18 +107,25 @@ impl SizeBox {
|
|||||||
SizeBox { left, top, right, bottom }
|
SizeBox { left, top, right, bottom }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a zeroed vector.
|
/// Create a box with all set to zero.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn zero() -> SizeBox {
|
pub fn zero() -> SizeBox {
|
||||||
SizeBox {
|
SizeBox::default()
|
||||||
left: Size::zero(),
|
|
||||||
top: Size::zero(),
|
|
||||||
right: Size::zero(),
|
|
||||||
bottom: Size::zero(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The maximum of two sizes.
|
||||||
|
pub fn max(a: Size, b: Size) -> Size {
|
||||||
|
if a >= b { a } else { b }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The minimum of two sizes.
|
||||||
|
pub fn min(a: Size, b: Size) -> Size {
|
||||||
|
if a <= b { a } else { b }
|
||||||
|
}
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------------------------//
|
||||||
|
|
||||||
impl Display for Size {
|
impl Display for Size {
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
write!(f, "{}pt", self.points)
|
write!(f, "{}pt", self.points)
|
||||||
|
20
src/style.rs
20
src/style.rs
@ -9,6 +9,10 @@ use crate::size::{Size, Size2D, SizeBox};
|
|||||||
pub struct TextStyle {
|
pub struct TextStyle {
|
||||||
/// A fallback list of font families to use.
|
/// A fallback list of font families to use.
|
||||||
pub font_families: Vec<FontFamily>,
|
pub font_families: Vec<FontFamily>,
|
||||||
|
/// Whether the font is in italics.
|
||||||
|
pub italic: bool,
|
||||||
|
/// Whether the font is bold.
|
||||||
|
pub bold: bool,
|
||||||
/// The font size.
|
/// The font size.
|
||||||
pub font_size: f32,
|
pub font_size: f32,
|
||||||
/// The line spacing (as a multiple of the font size).
|
/// The line spacing (as a multiple of the font size).
|
||||||
@ -22,7 +26,9 @@ impl Default for TextStyle {
|
|||||||
use FontFamily::*;
|
use FontFamily::*;
|
||||||
TextStyle {
|
TextStyle {
|
||||||
// Default font family, font size and line spacing.
|
// Default font family, font size and line spacing.
|
||||||
font_families: vec![SansSerif, Serif, Monospace],
|
font_families: vec![Serif, SansSerif, Monospace],
|
||||||
|
italic: false,
|
||||||
|
bold: false,
|
||||||
font_size: 11.0,
|
font_size: 11.0,
|
||||||
line_spacing: 1.25,
|
line_spacing: 1.25,
|
||||||
paragraph_spacing: 1.5,
|
paragraph_spacing: 1.5,
|
||||||
@ -44,16 +50,16 @@ impl Default for PageStyle {
|
|||||||
PageStyle {
|
PageStyle {
|
||||||
// A4 paper.
|
// A4 paper.
|
||||||
dimensions: Size2D {
|
dimensions: Size2D {
|
||||||
x: Size::from_mm(210.0),
|
x: Size::mm(210.0),
|
||||||
y: Size::from_mm(297.0),
|
y: Size::mm(297.0),
|
||||||
},
|
},
|
||||||
|
|
||||||
// All the same margins.
|
// All the same margins.
|
||||||
margins: SizeBox {
|
margins: SizeBox {
|
||||||
left: Size::from_cm(3.0),
|
left: Size::cm(2.5),
|
||||||
top: Size::from_cm(3.0),
|
top: Size::cm(2.5),
|
||||||
right: Size::from_cm(3.0),
|
right: Size::cm(2.5),
|
||||||
bottom: Size::from_cm(3.0),
|
bottom: Size::cm(2.5),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user