summaryrefslogtreecommitdiff
path: root/src/layout
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2020-10-07 17:07:44 +0200
committerLaurenz <laurmaedje@gmail.com>2020-10-07 17:07:44 +0200
commit537545e7f8351d7677c396456e46568f5a5e2a7a (patch)
treef4c7614293246db06c7fa7496458da01b15c3b84 /src/layout
parentca1256c924f3672feb76dbc2bc2e309eb4fc4cf5 (diff)
Evaluation and node-based layouting 🚀
Diffstat (limited to 'src/layout')
-rw-r--r--src/layout/mod.rs234
-rw-r--r--src/layout/nodes/document.rs52
-rw-r--r--src/layout/nodes/fixed.rs42
-rw-r--r--src/layout/nodes/mod.rs167
-rw-r--r--src/layout/nodes/pad.rs53
-rw-r--r--src/layout/nodes/par.rs (renamed from src/layout/line.rs)157
-rw-r--r--src/layout/nodes/spacing.rs51
-rw-r--r--src/layout/nodes/stack.rs (renamed from src/layout/stack.rs)257
-rw-r--r--src/layout/nodes/text.rs51
-rw-r--r--src/layout/primitive.rs64
-rw-r--r--src/layout/tree.rs234
11 files changed, 735 insertions, 627 deletions
diff --git a/src/layout/mod.rs b/src/layout/mod.rs
index 912ca010..f709da1a 100644
--- a/src/layout/mod.rs
+++ b/src/layout/mod.rs
@@ -1,88 +1,24 @@
-//! Layouting of syntax trees.
+//! Layouting of documents.
+pub mod nodes;
pub mod primitive;
-mod line;
-mod stack;
-mod tree;
-
-pub use line::*;
pub use primitive::*;
-pub use stack::*;
-pub use tree::*;
-use crate::diag::Diag;
+use async_trait::async_trait;
+
use crate::eval::{PageState, State, TextState};
use crate::font::SharedFontLoader;
use crate::geom::{Insets, Point, Rect, Size, SizeExt};
use crate::shaping::Shaped;
-use crate::syntax::{Deco, Spanned, SynTree};
-use crate::{Feedback, Pass};
-
-/// Layout a syntax tree and return the produced layout.
-pub async fn layout(
- tree: &SynTree,
- state: State,
- loader: SharedFontLoader,
-) -> Pass<Vec<BoxLayout>> {
- let space = LayoutSpace {
- size: state.page.size,
- insets: state.page.insets(),
- expansion: Spec2::new(true, true),
- };
-
- let constraints = LayoutConstraints {
- root: true,
- base: space.usable(),
- spaces: vec![space],
- repeat: true,
- };
-
- let mut ctx = LayoutContext {
- loader,
- state,
- constraints,
- f: Feedback::new(),
- };
-
- let layouts = layout_tree(&tree, &mut ctx).await;
- Pass::new(layouts, ctx.f)
-}
-
-/// A finished box with content at fixed positions.
-#[derive(Debug, Clone, PartialEq)]
-pub struct BoxLayout {
- /// The size of the box.
- pub size: Size,
- /// The elements composing this layout.
- pub elements: Vec<(Point, LayoutElement)>,
-}
+use crate::syntax::SynTree;
-impl BoxLayout {
- /// Create a new empty collection.
- pub fn new(size: Size) -> Self {
- Self { size, elements: vec![] }
- }
+use nodes::Document;
- /// Add an element at a position.
- pub fn push(&mut self, pos: Point, element: LayoutElement) {
- self.elements.push((pos, element));
- }
-
- /// Add all elements of another collection, placing them relative to the
- /// given position.
- pub fn push_layout(&mut self, pos: Point, more: Self) {
- for (subpos, element) in more.elements {
- self.push(pos + subpos.to_vec2(), element);
- }
- }
-}
-
-/// A layout element, the basic building block layouts are composed of.
-#[derive(Debug, Clone, PartialEq)]
-pub enum LayoutElement {
- /// Shaped text.
- Text(Shaped),
+/// Layout a document and return the produced layouts.
+pub async fn layout(document: &Document, loader: SharedFontLoader) -> Vec<BoxLayout> {
+ let mut ctx = LayoutContext { loader };
+ document.layout(&mut ctx).await
}
/// The context for layouting.
@@ -90,33 +26,43 @@ pub enum LayoutElement {
pub struct LayoutContext {
/// The font loader to query fonts from when typesetting text.
pub loader: SharedFontLoader,
- /// The active state.
- pub state: State,
- /// The active constraints.
- pub constraints: LayoutConstraints,
- /// The accumulated feedback.
- pub f: Feedback,
}
-impl LayoutContext {
- /// Add a diagnostic to the feedback.
- pub fn diag(&mut self, diag: Spanned<Diag>) {
- self.f.diags.push(diag);
- }
+/// Layout a node.
+#[async_trait(?Send)]
+pub trait Layout {
+ /// Layout the node in the given layout context.
+ ///
+ /// This signature looks pretty horrible due to async in trait methods, but
+ /// it's actually just the following:
+ /// ```rust,ignore
+ /// async fn layout(
+ /// &self,
+ /// ctx: &mut LayoutContext,
+ /// constraints: LayoutConstraints,
+ /// ) -> Vec<LayoutItem>;
+ /// ```
+ async fn layout(
+ &self,
+ ctx: &mut LayoutContext,
+ constraints: LayoutConstraints,
+ ) -> Vec<LayoutItem>;
+}
- /// Add a decoration to the feedback.
- pub fn deco(&mut self, deco: Spanned<Deco>) {
- self.f.decos.push(deco);
- }
+/// An item that is produced by [layouting] a node.
+///
+/// [layouting]: trait.Layout.html#method.layout
+#[derive(Debug, Clone, PartialEq)]
+pub enum LayoutItem {
+ /// Spacing that should be added to the parent.
+ Spacing(f64),
+ /// A box that should be aligned in the parent.
+ Box(BoxLayout, Gen2<GenAlign>),
}
/// The constraints for layouting a single node.
#[derive(Debug, Clone)]
pub struct LayoutConstraints {
- /// Whether this layouting process is the root page-building process.
- pub root: bool,
- /// The unpadded size of this container (the base 100% for relative sizes).
- pub base: Size,
/// The spaces to layout into.
pub spaces: Vec<LayoutSpace>,
/// Whether to spill over into copies of the last space or finish layouting
@@ -127,92 +73,44 @@ pub struct LayoutConstraints {
/// The space into which content is laid out.
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct LayoutSpace {
+ /// The full size of this container (the base for relative sizes).
+ pub base: Size,
/// The maximum size of the rectangle to layout into.
pub size: Size,
- /// Padding that should be respected on each side.
- pub insets: Insets,
- /// Whether to expand the size of the resulting layout to the full size of
- /// this space or to shrink it to fit the content.
- pub expansion: Spec2<bool>,
}
-impl LayoutSpace {
- /// The position of the padded start in the space.
- pub fn start(&self) -> Point {
- Point::new(-self.insets.x0, -self.insets.y0)
+/// A finished box with content at fixed positions.
+#[derive(Debug, Clone, PartialEq)]
+pub struct BoxLayout {
+ /// The size of the box.
+ pub size: Size,
+ /// The elements composing this layout.
+ pub elements: Vec<(Point, LayoutElement)>,
+}
+
+impl BoxLayout {
+ /// Create a new empty collection.
+ pub fn new(size: Size) -> Self {
+ Self { size, elements: vec![] }
}
- /// The actually usable area (size minus padding).
- pub fn usable(&self) -> Size {
- self.size + self.insets.size()
+ /// Add an element at a position.
+ pub fn push(&mut self, pos: Point, element: LayoutElement) {
+ self.elements.push((pos, element));
}
- /// The inner layout space with size reduced by the padding, zero padding of
- /// its own and no layout expansion.
- pub fn inner(&self) -> Self {
- Self {
- size: self.usable(),
- insets: Insets::ZERO,
- expansion: Spec2::new(false, false),
+ /// Add all elements of another collection, placing them relative to the
+ /// given position.
+ pub fn push_layout(&mut self, pos: Point, more: Self) {
+ for (subpos, element) in more.elements {
+ self.push(pos + subpos.to_vec2(), element);
}
}
}
-/// Commands executable by the layouting engine.
+/// A layout element, the basic building block layouts are composed of.
#[derive(Debug, Clone, PartialEq)]
-pub enum Command {
- /// Layout the given tree in the current context (i.e. not nested). The
- /// content of the tree is not laid out into a separate box and then added,
- /// but simply laid out flatly in the active layouting process.
- ///
- /// This has the effect that the content fits nicely into the active line
- /// layouting, enabling functions to e.g. change the style of some piece of
- /// text while keeping it part of the current paragraph.
- LayoutSyntaxTree(SynTree),
-
- /// Add a finished layout.
- Add(BoxLayout, Gen2<GenAlign>),
- /// Add spacing of the given kind along the given axis. The
- /// kind defines how the spacing interacts with surrounding spacing.
- AddSpacing(f64, SpacingKind, GenAxis),
-
- /// Start a new line.
- BreakLine,
- /// Start a new page, which will be part of the finished layout even if it
- /// stays empty (since the page break is a _hard_ space break).
- BreakPage,
-
- /// Update the text style.
- SetTextState(TextState),
- /// Update the page style.
- SetPageState(PageState),
- /// Update the alignment for future boxes added to this layouting process.
- SetAlignment(Gen2<GenAlign>),
-}
-
-/// Defines how spacing interacts with surrounding spacing.
-///
-/// There are two options for interaction: Hard and soft spacing. Typically,
-/// hard spacing is used when a fixed amount of space needs to be inserted no
-/// matter what. In contrast, soft spacing can be used to insert a default
-/// spacing between e.g. two words or paragraphs that can still be overridden by
-/// a hard space.
-#[derive(Debug, Copy, Clone, Eq, PartialEq)]
-pub enum SpacingKind {
- /// Hard spaces are always laid out and consume surrounding soft space.
- Hard,
- /// Soft spaces are not laid out if they are touching a hard space and
- /// consume neighbouring soft spaces with higher levels.
- Soft(u32),
-}
-
-impl SpacingKind {
- /// The standard spacing kind used for paragraph spacing.
- pub const PARAGRAPH: Self = Self::Soft(1);
-
- /// The standard spacing kind used for line spacing.
- pub const LINE: Self = Self::Soft(2);
-
- /// The standard spacing kind used for word spacing.
- pub const WORD: Self = Self::Soft(1);
+pub enum LayoutElement {
+ /// Shaped text.
+ Text(Shaped),
}
diff --git a/src/layout/nodes/document.rs b/src/layout/nodes/document.rs
new file mode 100644
index 00000000..af7a31e6
--- /dev/null
+++ b/src/layout/nodes/document.rs
@@ -0,0 +1,52 @@
+use super::*;
+
+/// The top-level layouting node.
+#[derive(Debug, Clone, PartialEq)]
+pub struct Document {
+ pub runs: Vec<Pages>,
+}
+
+impl Document {
+ /// Create a new document.
+ pub fn new() -> Self {
+ Self { runs: vec![] }
+ }
+
+ /// Layout the document.
+ pub async fn layout(&self, ctx: &mut LayoutContext) -> Vec<BoxLayout> {
+ let mut layouts = vec![];
+ for run in &self.runs {
+ layouts.extend(run.layout(ctx).await);
+ }
+ layouts
+ }
+}
+
+/// A variable-length run of pages that all have the same properties.
+#[derive(Debug, Clone, PartialEq)]
+pub struct Pages {
+ /// The size of the pages.
+ pub size: Size,
+ /// The layout node that produces the actual pages.
+ pub child: LayoutNode,
+}
+
+impl Pages {
+ /// Layout the page run.
+ pub async fn layout(&self, ctx: &mut LayoutContext) -> Vec<BoxLayout> {
+ let constraints = LayoutConstraints {
+ spaces: vec![LayoutSpace { base: self.size, size: self.size }],
+ repeat: true,
+ };
+
+ self.child
+ .layout(ctx, constraints)
+ .await
+ .into_iter()
+ .filter_map(|item| match item {
+ LayoutItem::Spacing(_) => None,
+ LayoutItem::Box(layout, _) => Some(layout),
+ })
+ .collect()
+ }
+}
diff --git a/src/layout/nodes/fixed.rs b/src/layout/nodes/fixed.rs
new file mode 100644
index 00000000..0d438879
--- /dev/null
+++ b/src/layout/nodes/fixed.rs
@@ -0,0 +1,42 @@
+use super::*;
+use crate::geom::Linear;
+
+/// A node that can fix its child's width and height.
+#[derive(Debug, Clone, PartialEq)]
+pub struct Fixed {
+ pub width: Option<Linear>,
+ pub height: Option<Linear>,
+ pub child: LayoutNode,
+}
+
+#[async_trait(?Send)]
+impl Layout for Fixed {
+ async fn layout(
+ &self,
+ ctx: &mut LayoutContext,
+ constraints: LayoutConstraints,
+ ) -> Vec<LayoutItem> {
+ let space = constraints.spaces[0];
+ let size = Size::new(
+ self.width
+ .map(|w| w.eval(space.base.width))
+ .unwrap_or(space.size.width),
+ self.height
+ .map(|h| h.eval(space.base.height))
+ .unwrap_or(space.size.height),
+ );
+
+ self.child
+ .layout(ctx, LayoutConstraints {
+ spaces: vec![LayoutSpace { base: size, size }],
+ repeat: false,
+ })
+ .await
+ }
+}
+
+impl From<Fixed> for LayoutNode {
+ fn from(fixed: Fixed) -> Self {
+ Self::dynamic(fixed)
+ }
+}
diff --git a/src/layout/nodes/mod.rs b/src/layout/nodes/mod.rs
new file mode 100644
index 00000000..44c18284
--- /dev/null
+++ b/src/layout/nodes/mod.rs
@@ -0,0 +1,167 @@
+//! Layout nodes.
+
+mod document;
+mod fixed;
+mod pad;
+mod par;
+mod spacing;
+mod stack;
+mod text;
+
+pub use document::*;
+pub use fixed::*;
+pub use pad::*;
+pub use par::*;
+pub use spacing::*;
+pub use stack::*;
+pub use text::*;
+
+use std::any::Any;
+use std::fmt::{self, Debug, Formatter};
+use std::ops::Deref;
+
+use async_trait::async_trait;
+
+use super::*;
+
+/// A self-contained, styled layout node.
+#[derive(Clone, PartialEq)]
+pub enum LayoutNode {
+ /// A spacing node.
+ Spacing(Spacing),
+ /// A text node.
+ Text(Text),
+ /// A dynamic that can implement custom layouting behaviour.
+ Dyn(Dynamic),
+}
+
+impl LayoutNode {
+ /// Create a new model node form a type implementing `DynNode`.
+ pub fn dynamic<T: DynNode>(inner: T) -> Self {
+ Self::Dyn(Dynamic::new(inner))
+ }
+}
+
+impl Debug for LayoutNode {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ match self {
+ Self::Spacing(spacing) => spacing.fmt(f),
+ Self::Text(text) => text.fmt(f),
+ Self::Dyn(boxed) => boxed.fmt(f),
+ }
+ }
+}
+
+#[async_trait(?Send)]
+impl Layout for LayoutNode {
+ async fn layout(
+ &self,
+ ctx: &mut LayoutContext,
+ constraints: LayoutConstraints,
+ ) -> Vec<LayoutItem> {
+ match self {
+ Self::Spacing(spacing) => spacing.layout(ctx, constraints).await,
+ Self::Text(text) => text.layout(ctx, constraints).await,
+ Self::Dyn(boxed) => boxed.layout(ctx, constraints).await,
+ }
+ }
+}
+
+/// A wrapper around a boxed dynamic node.
+///
+/// _Note_: This is needed because the compiler can't `derive(PartialEq)` for
+/// [`LayoutNode`] when directly putting the boxed node in there, see
+/// the [Rust Issue].
+///
+/// [`LayoutNode`]: enum.LayoutNode.html
+/// [Rust Issue]: https://github.com/rust-lang/rust/issues/31740
+#[derive(Clone)]
+pub struct Dynamic(pub Box<dyn DynNode>);
+
+impl Dynamic {
+ /// Wrap a type implementing `DynNode`.
+ pub fn new<T: DynNode>(inner: T) -> Self {
+ Self(Box::new(inner))
+ }
+}
+
+impl PartialEq for Dynamic {
+ fn eq(&self, other: &Self) -> bool {
+ &self.0 == &other.0
+ }
+}
+
+impl Deref for Dynamic {
+ type Target = dyn DynNode;
+
+ fn deref(&self) -> &Self::Target {
+ self.0.as_ref()
+ }
+}
+
+impl Debug for Dynamic {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ self.0.fmt(f)
+ }
+}
+
+impl From<Dynamic> for LayoutNode {
+ fn from(dynamic: Dynamic) -> Self {
+ Self::Dyn(dynamic)
+ }
+}
+
+/// A dynamic node, which can implement custom layouting behaviour.
+///
+/// This trait just combines the requirements for types to qualify as dynamic
+/// nodes. The interesting part happens in the inherited trait [`Layout`].
+///
+/// The trait itself also contains three helper methods to make `Box<dyn
+/// DynNode>` able to implement `Clone` and `PartialEq`. However, these are
+/// automatically provided by a blanket impl as long as the type in question
+/// implements[`Layout`], `Debug`, `PartialEq`, `Clone` and is `'static`.
+///
+/// [`Layout`]: ../trait.Layout.html
+pub trait DynNode: Debug + Layout + 'static {
+ /// Convert into a `dyn Any` to enable downcasting.
+ fn as_any(&self) -> &dyn Any;
+
+ /// Check for equality with another trait object.
+ fn dyn_eq(&self, other: &dyn DynNode) -> bool;
+
+ /// Clone into a trait object.
+ fn dyn_clone(&self) -> Box<dyn DynNode>;
+}
+
+impl<T> DynNode for T
+where
+ T: Debug + Layout + PartialEq + Clone + 'static,
+{
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+
+ fn dyn_eq(&self, other: &dyn DynNode) -> bool {
+ if let Some(other) = other.as_any().downcast_ref::<Self>() {
+ self == other
+ } else {
+ false
+ }
+ }
+
+ fn dyn_clone(&self) -> Box<dyn DynNode> {
+ Box::new(self.clone())
+ }
+}
+
+impl Clone for Box<dyn DynNode> {
+ fn clone(&self) -> Self {
+ self.dyn_clone()
+ }
+}
+
+impl PartialEq for Box<dyn DynNode> {
+ fn eq(&self, other: &Self) -> bool {
+ self.dyn_eq(other.as_ref())
+ }
+}
diff --git a/src/layout/nodes/pad.rs b/src/layout/nodes/pad.rs
new file mode 100644
index 00000000..10a9e2c6
--- /dev/null
+++ b/src/layout/nodes/pad.rs
@@ -0,0 +1,53 @@
+use super::*;
+use crate::geom::Linear;
+
+/// A node that pads its child at the sides.
+#[derive(Debug, Clone, PartialEq)]
+pub struct Pad {
+ pub padding: Sides<Linear>,
+ pub child: LayoutNode,
+}
+
+#[async_trait(?Send)]
+impl Layout for Pad {
+ async fn layout(
+ &self,
+ ctx: &mut LayoutContext,
+ constraints: LayoutConstraints,
+ ) -> Vec<LayoutItem> {
+ self.child
+ .layout(ctx, LayoutConstraints {
+ spaces: constraints
+ .spaces
+ .into_iter()
+ .map(|space| LayoutSpace {
+ base: space.base + self.padding.insets(space.base).size(),
+ size: space.size + self.padding.insets(space.size).size(),
+ })
+ .collect(),
+ repeat: constraints.repeat,
+ })
+ .await
+ .into_iter()
+ .map(|item| match item {
+ LayoutItem::Box(boxed, align) => {
+ let padding = self.padding.insets(boxed.size);
+ let padded = boxed.size - padding.size();
+
+ let mut outer = BoxLayout::new(padded);
+ let start = Point::new(-padding.x0, -padding.y0);
+ outer.push_layout(start, boxed);
+
+ LayoutItem::Box(outer, align)
+ }
+ item => item,
+ })
+ .collect()
+ }
+}
+
+impl From<Pad> for LayoutNode {
+ fn from(pad: Pad) -> Self {
+ Self::dynamic(pad)
+ }
+}
diff --git a/src/layout/line.rs b/src/layout/nodes/par.rs
index ae3bd969..38b11529 100644
--- a/src/layout/line.rs
+++ b/src/layout/nodes/par.rs
@@ -1,16 +1,66 @@
-//! Arranging boxes into lines.
-//!
-//! The boxes are laid out along the cross axis as long as they fit into a line.
-//! When necessary, a line break is inserted and the new line is offset along
-//! the main axis by the height of the previous line plus extra line spacing.
-//!
-//! Internally, the line layouter uses a stack layouter to stack the finished
-//! lines on top of each.
-
use super::*;
+/// A node that arranges its children into a paragraph.
+///
+/// Boxes are laid out along the cross axis as long as they fit into a line.
+/// When necessary, a line break is inserted and the new line is offset along
+/// the main axis by the height of the previous line plus extra line spacing.
+#[derive(Debug, Clone, PartialEq)]
+pub struct Par {
+ pub dirs: Gen2<Dir>,
+ pub line_spacing: f64,
+ pub children: Vec<LayoutNode>,
+ pub aligns: Gen2<GenAlign>,
+ pub expand: Spec2<bool>,
+}
+
+#[async_trait(?Send)]
+impl Layout for Par {
+ async fn layout(
+ &self,
+ ctx: &mut LayoutContext,
+ constraints: LayoutConstraints,
+ ) -> Vec<LayoutItem> {
+ let mut layouter = LineLayouter::new(LineContext {
+ dirs: self.dirs,
+ spaces: constraints.spaces,
+ repeat: constraints.repeat,
+ line_spacing: self.line_spacing,
+ expand: self.expand,
+ });
+
+ for child in &self.children {
+ let items = child
+ .layout(ctx, LayoutConstraints {
+ spaces: layouter.remaining(),
+ repeat: constraints.repeat,
+ })
+ .await;
+
+ for item in items {
+ match item {
+ LayoutItem::Spacing(amount) => layouter.push_spacing(amount),
+ LayoutItem::Box(boxed, aligns) => layouter.push_box(boxed, aligns),
+ }
+ }
+ }
+
+ layouter
+ .finish()
+ .into_iter()
+ .map(|boxed| LayoutItem::Box(boxed, self.aligns))
+ .collect()
+ }
+}
+
+impl From<Par> for LayoutNode {
+ fn from(par: Par) -> Self {
+ Self::dynamic(par)
+ }
+}
+
/// Performs the line layouting.
-pub struct LineLayouter {
+struct LineLayouter {
/// The context used for line layouting.
ctx: LineContext,
/// The underlying layouter that stacks the finished lines.
@@ -21,26 +71,30 @@ pub struct LineLayouter {
/// The context for line layouting.
#[derive(Debug, Clone)]
-pub struct LineContext {
+struct LineContext {
/// The layout directions.
- pub dirs: Gen2<Dir>,
+ dirs: Gen2<Dir>,
/// The spaces to layout into.
- pub spaces: Vec<LayoutSpace>,
+ spaces: Vec<LayoutSpace>,
/// Whether to spill over into copies of the last space or finish layouting
/// when the last space is used up.
- pub repeat: bool,
+ repeat: bool,
/// The spacing to be inserted between each pair of lines.
- pub line_spacing: f64,
+ line_spacing: f64,
+ /// Whether to expand the size of the resulting layout to the full size of
+ /// this space or to shrink it to fit the content.
+ expand: Spec2<bool>,
}
impl LineLayouter {
/// Create a new line layouter.
- pub fn new(ctx: LineContext) -> Self {
+ fn new(ctx: LineContext) -> Self {
Self {
stack: StackLayouter::new(StackContext {
spaces: ctx.spaces.clone(),
dirs: ctx.dirs,
repeat: ctx.repeat,
+ expand: ctx.expand,
}),
ctx,
run: LineRun::new(),
@@ -48,7 +102,7 @@ impl LineLayouter {
}
/// Add a layout.
- pub fn add(&mut self, layout: BoxLayout, aligns: Gen2<GenAlign>) {
+ fn push_box(&mut self, layout: BoxLayout, aligns: Gen2<GenAlign>) {
let dirs = self.ctx.dirs;
if let Some(prev) = self.run.aligns {
if aligns.main != prev.main {
@@ -67,6 +121,8 @@ impl LineLayouter {
let mut rest_run = LineRun::new();
rest_run.size.main = self.run.size.main;
+
+ // FIXME: Alignment in non-expanding parent.
rest_run.usable = Some(match aligns.cross {
GenAlign::Start => unreachable!("start > x"),
GenAlign::Center => usable - 2.0 * self.run.size.cross,
@@ -76,15 +132,11 @@ impl LineLayouter {
self.finish_line();
// Move back up in the stack layouter.
- self.stack.add_spacing(-rest_run.size.main, SpacingKind::Hard);
+ self.stack.push_spacing(-rest_run.size.main - self.ctx.line_spacing);
self.run = rest_run;
}
}
- if let LastSpacing::Soft(spacing, _) = self.run.last_spacing {
- self.add_cross_spacing(spacing, SpacingKind::Hard);
- }
-
let size = layout.size.switch(dirs);
let usable = self.usable();
@@ -105,7 +157,12 @@ impl LineLayouter {
self.run.size.cross += size.cross;
self.run.size.main = self.run.size.main.max(size.main);
- self.run.last_spacing = LastSpacing::None;
+ }
+
+ /// Add spacing to the line.
+ fn push_spacing(&mut self, mut spacing: f64) {
+ spacing = spacing.min(self.usable().cross);
+ self.run.size.cross += spacing;
}
/// The remaining usable size of the line.
@@ -125,66 +182,35 @@ impl LineLayouter {
usable
}
- /// Finish the line and add spacing to the underlying stack.
- pub fn add_main_spacing(&mut self, spacing: f64, kind: SpacingKind) {
- self.finish_line_if_not_empty();
- self.stack.add_spacing(spacing, kind)
- }
-
- /// Add spacing to the line.
- pub fn add_cross_spacing(&mut self, mut spacing: f64, kind: SpacingKind) {
- match kind {
- SpacingKind::Hard => {
- spacing = spacing.min(self.usable().cross);
- self.run.size.cross += spacing;
- self.run.last_spacing = LastSpacing::Hard;
- }
-
- // A soft space is cached since it might be consumed by a hard
- // spacing.
- SpacingKind::Soft(level) => {
- let consumes = match self.run.last_spacing {
- LastSpacing::None => true,
- LastSpacing::Soft(_, prev) if level < prev => true,
- _ => false,
- };
-
- if consumes {
- self.run.last_spacing = LastSpacing::Soft(spacing, level);
- }
- }
- }
- }
-
/// Update the layouting spaces.
///
/// If `replace_empty` is true, the current space is replaced if there are
/// no boxes laid out into it yet. Otherwise, the followup spaces are
/// replaced.
- pub fn set_spaces(&mut self, spaces: Vec<LayoutSpace>, replace_empty: bool) {
+ fn set_spaces(&mut self, spaces: Vec<LayoutSpace>, replace_empty: bool) {
self.stack.set_spaces(spaces, replace_empty && self.line_is_empty());
}
/// Update the line spacing.
- pub fn set_line_spacing(&mut self, line_spacing: f64) {
+ fn set_line_spacing(&mut self, line_spacing: f64) {
self.ctx.line_spacing = line_spacing;
}
/// The remaining inner spaces. If something is laid out into these spaces,
/// it will fit into this layouter's underlying stack.
- pub fn remaining(&self) -> Vec<LayoutSpace> {
+ fn remaining(&self) -> Vec<LayoutSpace> {
let mut spaces = self.stack.remaining();
*spaces[0].size.get_mut(self.ctx.dirs.main.axis()) -= self.run.size.main;
spaces
}
/// Whether the currently set line is empty.
- pub fn line_is_empty(&self) -> bool {
+ fn line_is_empty(&self) -> bool {
self.run.size == Gen2::ZERO && self.run.layouts.is_empty()
}
/// Finish everything up and return the final collection of boxes.
- pub fn finish(mut self) -> Vec<BoxLayout> {
+ fn finish(mut self) -> Vec<BoxLayout> {
self.finish_line_if_not_empty();
self.stack.finish()
}
@@ -192,13 +218,13 @@ impl LineLayouter {
/// Finish the active space and start a new one.
///
/// At the top level, this is a page break.
- pub fn finish_space(&mut self, hard: bool) {
+ fn finish_space(&mut self, hard: bool) {
self.finish_line_if_not_empty();
self.stack.finish_space(hard)
}
/// Finish the active line and start a new one.
- pub fn finish_line(&mut self) {
+ fn finish_line(&mut self) {
let dirs = self.ctx.dirs;
let mut layout = BoxLayout::new(self.run.size.switch(dirs).to_size());
@@ -216,10 +242,9 @@ impl LineLayouter {
layout.push_layout(pos, child);
}
- self.stack.add(layout, aligns);
-
+ self.stack.push_box(layout, aligns);
+ self.stack.push_spacing(self.ctx.line_spacing);
self.run = LineRun::new();
- self.stack.add_spacing(self.ctx.line_spacing, SpacingKind::LINE);
}
fn finish_line_if_not_empty(&mut self) {
@@ -245,9 +270,6 @@ struct LineRun {
/// The amount of cross-space left by another run on the same line or `None`
/// if this is the only run so far.
usable: Option<f64>,
- /// The spacing state. This influences how new spacing is handled, e.g. hard
- /// spacing may override soft spacing.
- last_spacing: LastSpacing,
}
impl LineRun {
@@ -257,7 +279,6 @@ impl LineRun {
size: Gen2::ZERO,
aligns: None,
usable: None,
- last_spacing: LastSpacing::Hard,
}
}
}
diff --git a/src/layout/nodes/spacing.rs b/src/layout/nodes/spacing.rs
new file mode 100644
index 00000000..66af0d17
--- /dev/null
+++ b/src/layout/nodes/spacing.rs
@@ -0,0 +1,51 @@
+use std::fmt::{self, Debug, Formatter};
+
+use super::*;
+
+/// A node that inserts spacing.
+#[derive(Copy, Clone, PartialEq)]
+pub struct Spacing {
+ pub amount: f64,
+ pub softness: Softness,
+}
+
+#[async_trait(?Send)]
+impl Layout for Spacing {
+ async fn layout(
+ &self,
+ ctx: &mut LayoutContext,
+ constraints: LayoutConstraints,
+ ) -> Vec<LayoutItem> {
+ vec![LayoutItem::Spacing(self.amount)]
+ }
+}
+
+impl Debug for Spacing {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ match self.softness {
+ Softness::Soft => write!(f, "Soft({})", self.amount),
+ Softness::Hard => write!(f, "Hard({})", self.amount),
+ }
+ }
+}
+
+impl From<Spacing> for LayoutNode {
+ fn from(spacing: Spacing) -> Self {
+ Self::Spacing(spacing)
+ }
+}
+
+/// Defines how spacing interacts with surrounding spacing.
+///
+/// Hard spacing assures that a fixed amount of spacing will always be inserted.
+/// Soft spacing will be consumed by previous soft spacing or neighbouring hard
+/// spacing and can be used to insert overridable spacing, e.g. between words or
+/// paragraphs.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
+pub enum Softness {
+ /// Soft spacing is not laid out if it directly follows other soft spacing
+ /// or if it touches hard spacing.
+ Soft,
+ /// Hard spacing is always laid out and consumes surrounding soft spacing.
+ Hard,
+}
diff --git a/src/layout/stack.rs b/src/layout/nodes/stack.rs
index cca2a315..983175b8 100644
--- a/src/layout/stack.rs
+++ b/src/layout/nodes/stack.rs
@@ -1,39 +1,93 @@
-//! Arranging boxes into a stack along the main axis.
-//!
-//! Individual layouts can be aligned at `Start`, `Center` or `End` along both
-//! axes. These alignments are with respect to the size of the finished layout
-//! and not the total usable size. This means that a later layout can have
-//! influence on the position of an earlier one. Consider the following example.
-//! ```typst
-//! [align: right][A word.]
-//! [align: left][A sentence with a couple more words.]
-//! ```
-//! The resulting layout looks like this:
-//! ```text
-//! |--------------------------------------|
-//! | A word. |
-//! | |
-//! | A sentence with a couple more words. |
-//! |--------------------------------------|
-//! ```
-//! The position of the first aligned box thus depends on the length of the
-//! sentence in the second box.
-
use super::*;
+use crate::geom::Linear;
+
+/// A node that stacks and aligns its children.
+///
+/// # Alignment
+/// Individual layouts can be aligned at `Start`, `Center` or `End` along both
+/// axes. These alignments are with processed with respect to the size of the
+/// finished layout and not the total usable size. This means that a later
+/// layout can have influence on the position of an earlier one. Consider the
+/// following example.
+/// ```typst
+/// [align: right][A word.]
+/// [align: left][A sentence with a couple more words.]
+/// ```
+/// The resulting layout looks like this:
+/// ```text
+/// |--------------------------------------|
+/// | A word. |
+/// | |
+/// | A sentence with a couple more words. |
+/// |--------------------------------------|
+/// ```
+/// The position of the first aligned box thus depends on the length of the
+/// sentence in the second box.
+#[derive(Debug, Clone, PartialEq)]
+pub struct Stack {
+ pub dirs: Gen2<Dir>,
+ pub children: Vec<LayoutNode>,
+ pub aligns: Gen2<GenAlign>,
+ pub expand: Spec2<bool>,
+}
+
+#[async_trait(?Send)]
+impl Layout for Stack {
+ async fn layout(
+ &self,
+ ctx: &mut LayoutContext,
+ constraints: LayoutConstraints,
+ ) -> Vec<LayoutItem> {
+ let mut layouter = StackLayouter::new(StackContext {
+ dirs: self.dirs,
+ spaces: constraints.spaces,
+ repeat: constraints.repeat,
+ expand: self.expand,
+ });
+
+ for child in &self.children {
+ let items = child
+ .layout(ctx, LayoutConstraints {
+ spaces: layouter.remaining(),
+ repeat: constraints.repeat,
+ })
+ .await;
+
+ for item in items {
+ match item {
+ LayoutItem::Spacing(amount) => layouter.push_spacing(amount),
+ LayoutItem::Box(boxed, aligns) => layouter.push_box(boxed, aligns),
+ }
+ }
+ }
+
+ layouter
+ .finish()
+ .into_iter()
+ .map(|boxed| LayoutItem::Box(boxed, self.aligns))
+ .collect()
+ }
+}
+
+impl From<Stack> for LayoutNode {
+ fn from(stack: Stack) -> Self {
+ Self::dynamic(stack)
+ }
+}
/// Performs the stack layouting.
-pub struct StackLayouter {
+pub(super) struct StackLayouter {
/// The context used for stack layouting.
- ctx: StackContext,
+ pub ctx: StackContext,
/// The finished layouts.
- layouts: Vec<BoxLayout>,
+ pub layouts: Vec<BoxLayout>,
/// The in-progress space.
- pub(super) space: Space,
+ pub space: Space,
}
/// The context for stack layouting.
#[derive(Debug, Clone)]
-pub struct StackContext {
+pub(super) struct StackContext {
/// The layouting directions.
pub dirs: Gen2<Dir>,
/// The spaces to layout into.
@@ -41,6 +95,9 @@ pub struct StackContext {
/// Whether to spill over into copies of the last space or finish layouting
/// when the last space is used up.
pub repeat: bool,
+ /// Whether to expand the size of the resulting layout to the full size of
+ /// this space or to shrink it to fit the content.
+ pub expand: Spec2<bool>,
}
impl StackLayouter {
@@ -50,12 +107,12 @@ impl StackLayouter {
Self {
ctx,
layouts: vec![],
- space: Space::new(0, true, space.usable()),
+ space: Space::new(0, true, space.size),
}
}
/// Add a layout to the stack.
- pub fn add(&mut self, layout: BoxLayout, aligns: Gen2<GenAlign>) {
+ pub fn push_box(&mut self, layout: BoxLayout, aligns: Gen2<GenAlign>) {
// If the alignment cannot be fitted in this space, finish it.
//
// TODO: Issue warning for non-fitting alignment in non-repeating
@@ -64,14 +121,9 @@ impl StackLayouter {
self.finish_space(true);
}
- // Add a possibly cached soft spacing.
- if let LastSpacing::Soft(spacing, _) = self.space.last_spacing {
- self.add_spacing(spacing, SpacingKind::Hard);
- }
-
// TODO: Issue warning about overflow if there is overflow in a
// non-repeating context.
- if !self.usable().fits(layout.size) && self.ctx.repeat {
+ if !self.space.usable.fits(layout.size) && self.ctx.repeat {
self.skip_to_fitting_space(layout.size);
}
@@ -82,49 +134,27 @@ impl StackLayouter {
// again.
self.space.layouts.push((layout, aligns));
self.space.allowed_align = aligns.main;
- self.space.last_spacing = LastSpacing::None;
}
/// Add spacing to the stack.
- pub fn add_spacing(&mut self, mut spacing: f64, kind: SpacingKind) {
- match kind {
- // A hard space is simply an empty box.
- SpacingKind::Hard => {
- self.space.last_spacing = LastSpacing::Hard;
-
- // Reduce the spacing such that it definitely fits.
- let axis = self.ctx.dirs.main.axis();
- spacing = spacing.min(self.usable().get(axis));
-
- let size = Gen2::new(spacing, 0.0);
- self.update_metrics(size);
- self.space.layouts.push((
- BoxLayout::new(size.switch(self.ctx.dirs).to_size()),
- Gen2::default(),
- ));
- }
-
- // A soft space is cached if it is not consumed by a hard space or
- // previous soft space with higher level.
- SpacingKind::Soft(level) => {
- let consumes = match self.space.last_spacing {
- LastSpacing::None => true,
- LastSpacing::Soft(_, prev) if level < prev => true,
- _ => false,
- };
-
- if consumes {
- self.space.last_spacing = LastSpacing::Soft(spacing, level);
- }
- }
- }
+ pub fn push_spacing(&mut self, mut spacing: f64) {
+ // Reduce the spacing such that it definitely fits.
+ let axis = self.ctx.dirs.main.axis();
+ spacing = spacing.min(self.space.usable.get(axis));
+
+ let size = Gen2::new(spacing, 0.0);
+ self.update_metrics(size);
+ self.space.layouts.push((
+ BoxLayout::new(size.switch(self.ctx.dirs).to_size()),
+ Gen2::default(),
+ ));
}
fn update_metrics(&mut self, added: Gen2<f64>) {
- let mut size = self.space.size.switch(self.ctx.dirs);
- size.cross = size.cross.max(added.cross);
- size.main += added.main;
- self.space.size = size.switch(self.ctx.dirs).to_size();
+ let mut used = self.space.used.switch(self.ctx.dirs);
+ used.cross = used.cross.max(added.cross);
+ used.main += added.main;
+ self.space.used = used.switch(self.ctx.dirs).to_size();
*self.space.usable.get_mut(self.ctx.dirs.main.axis()) -= added.main;
}
@@ -148,7 +178,7 @@ impl StackLayouter {
pub fn skip_to_fitting_space(&mut self, size: Size) {
let start = self.next_space();
for (index, space) in self.ctx.spaces[start ..].iter().enumerate() {
- if space.usable().fits(size) {
+ if space.size.fits(size) {
self.finish_space(true);
self.start_space(start + index, true);
break;
@@ -160,29 +190,22 @@ impl StackLayouter {
/// it will fit into this stack.
pub fn remaining(&self) -> Vec<LayoutSpace> {
let mut spaces = vec![LayoutSpace {
- size: self.usable(),
- insets: Insets::ZERO,
- expansion: Spec2::new(false, false),
+ base: self.space.size,
+ size: self.space.usable,
}];
- for space in &self.ctx.spaces[self.next_space() ..] {
- spaces.push(space.inner());
- }
-
+ spaces.extend(&self.ctx.spaces[self.next_space() ..]);
spaces
}
/// The remaining usable size.
pub fn usable(&self) -> Size {
self.space.usable
- - Gen2::new(self.space.last_spacing.soft_or_zero(), 0.0)
- .switch(self.ctx.dirs)
- .to_size()
}
/// Whether the current layout space is empty.
pub fn space_is_empty(&self) -> bool {
- self.space.size == Size::ZERO && self.space.layouts.is_empty()
+ self.space.used == Size::ZERO && self.space.layouts.is_empty()
}
/// Whether the current layout space is the last in the followup list.
@@ -208,23 +231,18 @@ impl StackLayouter {
// expand if necessary.)
let space = self.ctx.spaces[self.space.index];
- let start = space.start();
- let padded_size = {
- let mut used_size = self.space.size;
-
- let usable = space.usable();
- if space.expansion.horizontal {
- used_size.width = usable.width;
+ let layout_size = {
+ let mut used_size = self.space.used;
+ if self.ctx.expand.horizontal {
+ used_size.width = space.size.width;
}
- if space.expansion.vertical {
- used_size.height = usable.height;
+ if self.ctx.expand.vertical {
+ used_size.height = space.size.height;
}
-
used_size
};
- let unpadded_size = padded_size - space.insets.size();
- let mut layout = BoxLayout::new(unpadded_size);
+ let mut layout = BoxLayout::new(layout_size);
// ------------------------------------------------------------------ //
// Step 2: Forward pass. Create a bounding box for each layout in which
@@ -233,10 +251,10 @@ impl StackLayouter {
let mut bounds = vec![];
let mut bound = Rect {
- x0: start.x,
- y0: start.y,
- x1: start.x + self.space.size.width,
- y1: start.y + self.space.size.height,
+ x0: 0.0,
+ y0: 0.0,
+ x1: layout_size.width,
+ y1: layout_size.height,
};
for (layout, _) in &self.space.layouts {
@@ -294,7 +312,7 @@ impl StackLayouter {
fn start_space(&mut self, index: usize, hard: bool) {
let space = self.ctx.spaces[index];
- self.space = Space::new(index, hard, space.usable());
+ self.space = Space::new(index, hard, space.size);
}
fn next_space(&self) -> usize {
@@ -304,6 +322,7 @@ impl StackLayouter {
/// A layout space composed of subspaces which can have different directions and
/// alignments.
+#[derive(Debug)]
pub(super) struct Space {
/// The index of this space in `ctx.spaces`.
index: usize,
@@ -311,50 +330,26 @@ pub(super) struct Space {
hard: bool,
/// The so-far accumulated layouts.
layouts: Vec<(BoxLayout, Gen2<GenAlign>)>,
- /// The size of this space.
+ /// The full size of this space.
size: Size,
+ /// The used size of this space.
+ used: Size,
/// The remaining space.
usable: Size,
/// Which alignments for new boxes are still allowed.
pub(super) allowed_align: GenAlign,
- /// The spacing state. This influences how new spacing is handled, e.g. hard
- /// spacing may override soft spacing.
- last_spacing: LastSpacing,
}
impl Space {
- fn new(index: usize, hard: bool, usable: Size) -> Self {
+ fn new(index: usize, hard: bool, size: Size) -> Self {
Self {
index,
hard,
layouts: vec![],
- size: Size::ZERO,
- usable,
+ size,
+ used: Size::ZERO,
+ usable: size,
allowed_align: GenAlign::Start,
- last_spacing: LastSpacing::Hard,
- }
- }
-}
-
-/// The spacing kind of the most recently inserted item in a layouting process.
-///
-/// Since the last inserted item may not be spacing at all, this can be `None`.
-#[derive(Debug, Copy, Clone, PartialEq)]
-pub(crate) enum LastSpacing {
- /// The last item was hard spacing.
- Hard,
- /// The last item was soft spacing with the given width and level.
- Soft(f64, u32),
- /// The last item wasn't spacing.
- None,
-}
-
-impl LastSpacing {
- /// The width of the soft space if this is a soft space or zero otherwise.
- fn soft_or_zero(self) -> f64 {
- match self {
- LastSpacing::Soft(space, _) => space,
- _ => 0.0,
}
}
}
diff --git a/src/layout/nodes/text.rs b/src/layout/nodes/text.rs
new file mode 100644
index 00000000..b0c4a458
--- /dev/null
+++ b/src/layout/nodes/text.rs
@@ -0,0 +1,51 @@
+use std::fmt::{self, Debug, Formatter};
+use std::rc::Rc;
+
+use fontdock::{FallbackTree, FontVariant};
+
+use super::*;
+use crate::shaping;
+
+/// A text node.
+#[derive(Clone, PartialEq)]
+pub struct Text {
+ pub text: String,
+ pub size: f64,
+ pub dir: Dir,
+ pub fallback: Rc<FallbackTree>,
+ pub variant: FontVariant,
+ pub aligns: Gen2<GenAlign>,
+}
+
+#[async_trait(?Send)]
+impl Layout for Text {
+ async fn layout(
+ &self,
+ ctx: &mut LayoutContext,
+ _constraints: LayoutConstraints,
+ ) -> Vec<LayoutItem> {
+ let mut loader = ctx.loader.borrow_mut();
+ let boxed = shaping::shape(
+ &self.text,
+ self.size,
+ self.dir,
+ &mut loader,
+ &self.fallback,
+ self.variant,
+ )
+ .await;
+ vec![LayoutItem::Box(boxed, self.aligns)]
+ }
+}
+
+impl Debug for Text {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ write!(f, "Text({})", self.text)
+ }
+}
+
+impl From<Text> for LayoutNode {
+ fn from(text: Text) -> Self {
+ Self::Text(text)
+ }
+}
diff --git a/src/layout/primitive.rs b/src/layout/primitive.rs
index f932b85b..b641b5c7 100644
--- a/src/layout/primitive.rs
+++ b/src/layout/primitive.rs
@@ -2,7 +2,7 @@
use std::fmt::{self, Display, Formatter};
-use crate::geom::{Point, Size, Vec2};
+use crate::geom::{Insets, Linear, Point, Size, Vec2};
/// Generic access to a structure's components.
pub trait Get<Index> {
@@ -126,6 +126,11 @@ impl<T> Gen2<T> {
}
}
+impl Gen2<f64> {
+ /// The instance that has both components set to zero.
+ pub const ZERO: Self = Self { main: 0.0, cross: 0.0 };
+}
+
impl<T> Get<GenAxis> for Gen2<T> {
type Component = T;
@@ -155,11 +160,6 @@ impl<T> Switch for Gen2<T> {
}
}
-impl Gen2<f64> {
- /// The instance that has both components set to zero.
- pub const ZERO: Self = Self { main: 0.0, cross: 0.0 };
-}
-
/// A generic container with two components for the two specific axes.
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)]
pub struct Spec2<T> {
@@ -176,6 +176,26 @@ impl<T> Spec2<T> {
}
}
+impl Spec2<f64> {
+ /// The instance that has both components set to zero.
+ pub const ZERO: Self = Self { horizontal: 0.0, vertical: 0.0 };
+
+ /// Convert to a 2D vector.
+ pub fn to_vec2(self) -> Vec2 {
+ Vec2::new(self.horizontal, self.vertical)
+ }
+
+ /// Convert to a point.
+ pub fn to_point(self) -> Point {
+ Point::new(self.horizontal, self.vertical)
+ }
+
+ /// Convert to a size.
+ pub fn to_size(self) -> Size {
+ Size::new(self.horizontal, self.vertical)
+ }
+}
+
impl<T> Get<SpecAxis> for Spec2<T> {
type Component = T;
@@ -205,26 +225,6 @@ impl<T> Switch for Spec2<T> {
}
}
-impl Spec2<f64> {
- /// The instance that has both components set to zero.
- pub const ZERO: Self = Self { horizontal: 0.0, vertical: 0.0 };
-
- /// Convert to a 2D vector.
- pub fn to_vec2(self) -> Vec2 {
- Vec2::new(self.horizontal, self.vertical)
- }
-
- /// Convert to a point.
- pub fn to_point(self) -> Point {
- Point::new(self.horizontal, self.vertical)
- }
-
- /// Convert to a size.
- pub fn to_size(self) -> Size {
- Size::new(self.horizontal, self.vertical)
- }
-}
-
/// The two generic layouting axes.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum GenAxis {
@@ -444,6 +444,18 @@ impl<T> Sides<T> {
}
}
+impl Sides<Linear> {
+ /// The absolute insets.
+ pub fn insets(self, Size { width, height }: Size) -> Insets {
+ Insets {
+ x0: -self.left.eval(width),
+ y0: -self.top.eval(height),
+ x1: -self.right.eval(width),
+ y1: -self.bottom.eval(height),
+ }
+ }
+}
+
impl<T> Get<Side> for Sides<T> {
type Component = T;
diff --git a/src/layout/tree.rs b/src/layout/tree.rs
deleted file mode 100644
index 4e53cedc..00000000
--- a/src/layout/tree.rs
+++ /dev/null
@@ -1,234 +0,0 @@
-//! Layouting of syntax trees.
-
-use fontdock::FontStyle;
-
-use super::*;
-use crate::eval::Eval;
-use crate::shaping;
-use crate::syntax::*;
-use crate::DynFuture;
-
-/// Layout a syntax tree in a given context.
-pub async fn layout_tree(tree: &SynTree, ctx: &mut LayoutContext) -> Vec<BoxLayout> {
- let mut layouter = TreeLayouter::new(ctx);
- layouter.layout_tree(tree).await;
- layouter.finish()
-}
-
-/// Layouts trees.
-struct TreeLayouter<'a> {
- ctx: &'a mut LayoutContext,
- constraints: LayoutConstraints,
- layouter: LineLayouter,
-}
-
-impl<'a> TreeLayouter<'a> {
- fn new(ctx: &'a mut LayoutContext) -> Self {
- let layouter = LineLayouter::new(LineContext {
- spaces: ctx.constraints.spaces.clone(),
- dirs: ctx.state.dirs,
- repeat: ctx.constraints.repeat,
- line_spacing: ctx.state.text.line_spacing(),
- });
-
- Self {
- layouter,
- constraints: ctx.constraints.clone(),
- ctx,
- }
- }
-
- fn finish(self) -> Vec<BoxLayout> {
- self.layouter.finish()
- }
-
- fn layout_tree<'t>(&'t mut self, tree: &'t SynTree) -> DynFuture<'t, ()> {
- Box::pin(async move {
- for node in tree {
- self.layout_node(node).await;
- }
- })
- }
-
- async fn layout_node(&mut self, node: &Spanned<SynNode>) {
- let decorate = |this: &mut Self, deco: Deco| {
- this.ctx.f.decos.push(deco.span_with(node.span));
- };
-
- match &node.v {
- SynNode::Space => self.layout_space(),
- SynNode::Text(text) => {
- if self.ctx.state.text.emph {
- decorate(self, Deco::Emph);
- }
- if self.ctx.state.text.strong {
- decorate(self, Deco::Strong);
- }
- self.layout_text(text).await;
- }
-
- SynNode::Linebreak => self.layouter.finish_line(),
- SynNode::Parbreak => self.layout_parbreak(),
- SynNode::Emph => {
- self.ctx.state.text.emph ^= true;
- decorate(self, Deco::Emph);
- }
- SynNode::Strong => {
- self.ctx.state.text.strong ^= true;
- decorate(self, Deco::Strong);
- }
-
- SynNode::Heading(heading) => self.layout_heading(heading).await,
- SynNode::Raw(raw) => self.layout_raw(raw).await,
-
- SynNode::Expr(expr) => {
- self.layout_expr(expr.span_with(node.span)).await;
- }
- }
- }
-
- fn layout_space(&mut self) {
- self.layouter
- .add_cross_spacing(self.ctx.state.text.word_spacing(), SpacingKind::WORD);
- }
-
- fn layout_parbreak(&mut self) {
- self.layouter
- .add_main_spacing(self.ctx.state.text.par_spacing(), SpacingKind::PARAGRAPH);
- }
-
- async fn layout_text(&mut self, text: &str) {
- let mut variant = self.ctx.state.text.variant;
-
- if self.ctx.state.text.strong {
- variant.weight = variant.weight.thicken(300);
- }
-
- if self.ctx.state.text.emph {
- variant.style = match variant.style {
- FontStyle::Normal => FontStyle::Italic,
- FontStyle::Italic => FontStyle::Normal,
- FontStyle::Oblique => FontStyle::Normal,
- }
- }
-
- let boxed = shaping::shape(
- text,
- self.ctx.state.text.font_size(),
- self.ctx.state.dirs.cross,
- &mut self.ctx.loader.borrow_mut(),
- &self.ctx.state.text.fallback,
- variant,
- )
- .await;
-
- self.layouter.add(boxed, self.ctx.state.aligns);
- }
-
- async fn layout_heading(&mut self, heading: &NodeHeading) {
- let style = self.ctx.state.text.clone();
-
- let factor = 1.5 - 0.1 * heading.level.v as f64;
- self.ctx.state.text.font_size.scale *= factor;
- self.ctx.state.text.strong = true;
-
- self.layout_parbreak();
- self.layout_tree(&heading.contents).await;
- self.layout_parbreak();
-
- self.ctx.state.text = style;
- }
-
- async fn layout_raw(&mut self, raw: &NodeRaw) {
- if !raw.inline {
- self.layout_parbreak();
- }
-
- // TODO: Make this more efficient.
- let fallback = self.ctx.state.text.fallback.clone();
- self.ctx.state.text.fallback.list.insert(0, "monospace".to_string());
- self.ctx.state.text.fallback.flatten();
-
- let mut first = true;
- for line in &raw.lines {
- if !first {
- self.layouter.finish_line();
- }
- first = false;
- self.layout_text(line).await;
- }
-
- self.ctx.state.text.fallback = fallback;
-
- if !raw.inline {
- self.layout_parbreak();
- }
- }
-
- async fn layout_expr(&mut self, expr: Spanned<&Expr>) {
- self.ctx.constraints = LayoutConstraints {
- root: false,
- base: self.constraints.base,
- spaces: self.layouter.remaining(),
- repeat: self.constraints.repeat,
- };
-
- let val = expr.v.eval(self.ctx).await;
- let commands = val.span_with(expr.span).into_commands();
- for command in commands {
- self.execute_command(command, expr.span).await;
- }
- }
-
- async fn execute_command(&mut self, command: Command, span: Span) {
- use Command::*;
- match command {
- LayoutSyntaxTree(tree) => self.layout_tree(&tree).await,
-
- Add(layout, aligns) => self.layouter.add(layout, aligns),
- AddSpacing(space, kind, axis) => match axis {
- GenAxis::Main => self.layouter.add_main_spacing(space, kind),
- GenAxis::Cross => self.layouter.add_cross_spacing(space, kind),
- },
-
- BreakLine => self.layouter.finish_line(),
- BreakPage => {
- if self.constraints.root {
- self.layouter.finish_space(true)
- } else {
- self.ctx.diag(error!(
- span,
- "page break can only be issued from root context",
- ));
- }
- }
-
- SetTextState(style) => {
- self.layouter.set_line_spacing(style.line_spacing());
- self.ctx.state.text = style;
- }
- SetPageState(style) => {
- if self.constraints.root {
- self.ctx.state.page = style;
-
- // The line layouter has no idea of page styles and thus we
- // need to recompute the layouting space resulting of the
- // new page style and update it within the layouter.
- let space = LayoutSpace {
- size: style.size,
- insets: style.insets(),
- expansion: Spec2::new(true, true),
- };
- self.constraints.base = space.usable();
- self.layouter.set_spaces(vec![space], true);
- } else {
- self.ctx.diag(error!(
- span,
- "page style can only be changed from root context",
- ));
- }
- }
- SetAlignment(aligns) => self.ctx.state.aligns = aligns,
- }
- }
-}