diff --git a/benches/oneshot.rs b/benches/oneshot.rs index bb385688f..6bfb923b4 100644 --- a/benches/oneshot.rs +++ b/benches/oneshot.rs @@ -60,7 +60,7 @@ fn bench_eval(iai: &mut Iai) { fn bench_to_tree(iai: &mut Iai) { let (mut ctx, id) = context(); let module = ctx.evaluate(id).unwrap(); - iai.run(|| module.template.to_pages(ctx.style())); + iai.run(|| module.template.to_document(ctx.style())); } fn bench_layout(iai: &mut Iai) { diff --git a/src/eval/template.rs b/src/eval/template.rs index 8fec398de..82a069f93 100644 --- a/src/eval/template.rs +++ b/src/eval/template.rs @@ -7,9 +7,10 @@ use std::rc::Rc; use crate::diag::StrResult; use crate::geom::{Align, Dir, GenAxis, Length, Linear, Sides, Size}; -use crate::layout::{BlockLevel, BlockNode, InlineLevel, InlineNode, PageNode}; +use crate::layout::{Layout, PackedNode}; use crate::library::{ - Decoration, FlowChild, FlowNode, PadNode, ParChild, ParNode, Spacing, + Decoration, DocumentNode, FlowChild, FlowNode, PadNode, PageNode, ParChild, ParNode, + Spacing, }; use crate::style::Style; use crate::util::EcoString; @@ -36,9 +37,9 @@ enum TemplateNode { /// A decorated template. Decorated(Decoration, Template), /// An inline node builder. - Inline(Rc InlineNode>), - /// An block node builder. - Block(Rc BlockNode>), + Inline(Rc PackedNode>), + /// A block node builder. + Block(Rc PackedNode>), /// Save the current style. Save, /// Restore the last saved style. @@ -57,7 +58,7 @@ impl Template { pub fn from_inline(f: F) -> Self where F: Fn(&Style) -> T + 'static, - T: InlineLevel + Hash + 'static, + T: Layout + Hash + 'static, { let node = TemplateNode::Inline(Rc::new(move |s| f(s).pack())); Self(Rc::new(vec![node])) @@ -67,7 +68,7 @@ impl Template { pub fn from_block(f: F) -> Self where F: Fn(&Style) -> T + 'static, - T: BlockLevel + Hash + 'static, + T: Layout + Hash + 'static, { let node = TemplateNode::Block(Rc::new(move |s| f(s).pack())); Self(Rc::new(vec![node])) @@ -158,10 +159,10 @@ impl Template { /// Build the layout tree resulting from instantiating the template with the /// given style. - pub fn to_pages(&self, style: &Style) -> Vec { + pub fn to_document(&self, style: &Style) -> DocumentNode { let mut builder = Builder::new(style, true); builder.template(self); - builder.build_pages() + builder.build_document() } /// Repeat this template `n` times. @@ -327,13 +328,13 @@ impl Builder { } /// Push an inline node into the active paragraph. - fn inline(&mut self, node: impl Into) { + fn inline(&mut self, node: impl Into) { let align = self.style.aligns.inline; self.flow.par.push(ParChild::Node(node.into(), align)); } /// Push a block node into the active flow, finishing the active paragraph. - fn block(&mut self, node: impl Into) { + fn block(&mut self, node: impl Into) { self.parbreak(); self.flow.push(FlowChild::Node(node.into(), self.style.aligns.block)); self.parbreak(); @@ -359,10 +360,10 @@ impl Builder { } /// Finish building and return the created layout tree. - fn build_pages(mut self) -> Vec { + fn build_document(mut self) -> DocumentNode { assert!(self.page.is_some()); self.pagebreak(true, false); - self.finished + DocumentNode { pages: self.finished } } /// Construct a text node with the given text and settings from the current diff --git a/src/eval/walk.rs b/src/eval/walk.rs index 7b3fb7a4d..134f10c70 100644 --- a/src/eval/walk.rs +++ b/src/eval/walk.rs @@ -3,7 +3,7 @@ use std::rc::Rc; use super::{Eval, EvalContext, Template, Value}; use crate::diag::TypResult; use crate::geom::Spec; -use crate::layout::BlockLevel; +use crate::layout::Layout; use crate::library::{GridNode, ParChild, ParNode, TrackSizing}; use crate::syntax::ast::*; use crate::util::{BoolExt, EcoString}; diff --git a/src/export/pdf.rs b/src/export/pdf.rs index d517aadfc..24944246d 100644 --- a/src/export/pdf.rs +++ b/src/export/pdf.rs @@ -19,13 +19,13 @@ use crate::geom::{self, Color, Em, Length, Paint, Size}; use crate::image::{Image, ImageId, ImageStore}; use crate::Context; -/// Export a collection of frames into a PDF document. +/// Export a collection of frames into a PDF file. /// /// This creates one page per frame. In addition to the frames, you need to pass /// in the context used during compilation such that things like fonts and /// images can be included in the PDF. /// -/// Returns the raw bytes making up the PDF document. +/// Returns the raw bytes making up the PDF file. pub fn pdf(ctx: &Context, frames: &[Rc]) -> Vec { PdfExporter::new(ctx, frames).write() } diff --git a/src/layout/constraints.rs b/src/layout/constraints.rs index fdcda276d..36cfa582c 100644 --- a/src/layout/constraints.rs +++ b/src/layout/constraints.rs @@ -1,7 +1,7 @@ use std::rc::Rc; use crate::frame::Frame; -use crate::geom::{Length, Size, Spec}; +use crate::geom::{Length, Linear, Size, Spec}; /// Constrain a frame with constraints. pub trait Constrain { @@ -68,6 +68,18 @@ impl Constraints { && verify(self.exact, current, Length::approx_eq) && verify(self.base, base, Length::approx_eq) } + + /// Set the appropriate base constraints for linear width and height sizing. + pub fn set_base_if_linear(&mut self, base: Size, sizing: Spec>) { + // The full sizes need to be equal if there is a relative component in + // the sizes. + if sizing.x.map_or(false, |l| l.is_relative()) { + self.base.x = Some(base.w); + } + if sizing.y.map_or(false, |l| l.is_relative()) { + self.base.y = Some(base.h); + } + } } /// Verify a single constraint. diff --git a/src/layout/levels.rs b/src/layout/levels.rs deleted file mode 100644 index a6b8d0503..000000000 --- a/src/layout/levels.rs +++ /dev/null @@ -1,199 +0,0 @@ -use std::fmt::{self, Debug, Formatter}; -use std::hash::{Hash, Hasher}; -use std::rc::Rc; - -use super::*; -use crate::geom::{Length, Size}; - -/// Page-level nodes directly produce frames representing pages. -/// -/// Such nodes create their own regions instead of being supplied with them from -/// some parent. -pub trait PageLevel: Debug { - /// Layout the node, producing one frame per page. - fn layout(&self, ctx: &mut LayoutContext) -> Vec>; -} - -/// Layouts its children onto one or multiple pages. -#[derive(Debug)] -pub struct PageNode { - /// The size of the page. - pub size: Size, - /// The node that produces the actual pages. - pub child: BlockNode, -} - -impl PageLevel for PageNode { - fn layout(&self, ctx: &mut LayoutContext) -> Vec> { - // When one of the lengths is infinite the page fits its content along - // that axis. - let expand = self.size.to_spec().map(Length::is_finite); - let regions = Regions::repeat(self.size, self.size, expand); - self.child.layout(ctx, ®ions).into_iter().map(|c| c.item).collect() - } -} - -impl PageLevel for T -where - T: AsRef<[PageNode]> + Debug + ?Sized, -{ - fn layout(&self, ctx: &mut LayoutContext) -> Vec> { - self.as_ref().iter().flat_map(|node| node.layout(ctx)).collect() - } -} - -/// Block-level nodes can be layouted into a sequence of regions. -/// -/// They return one frame per used region alongside constraints that define -/// whether the result is reusable in other regions. -pub trait BlockLevel: Debug { - /// Layout the node into the given regions, producing constrained frames. - fn layout( - &self, - ctx: &mut LayoutContext, - regions: &Regions, - ) -> Vec>>; - - /// Convert to a packed block-level node. - fn pack(self) -> BlockNode - where - Self: Sized + Hash + 'static, - { - BlockNode { - #[cfg(feature = "layout-cache")] - hash: hash_node(&self), - node: Rc::new(self), - } - } -} - -/// A packed [block-level](BlockLevel) layouting node with precomputed hash. -#[derive(Clone)] -pub struct BlockNode { - node: Rc, - #[cfg(feature = "layout-cache")] - hash: u64, -} - -impl BlockLevel for BlockNode { - fn layout( - &self, - ctx: &mut LayoutContext, - regions: &Regions, - ) -> Vec>> { - #[cfg(not(feature = "layout-cache"))] - return self.node.layout(ctx, regions); - - #[cfg(feature = "layout-cache")] - ctx.layouts.get(self.hash, regions).unwrap_or_else(|| { - ctx.level += 1; - let frames = self.node.layout(ctx, regions); - ctx.level -= 1; - - let entry = FramesEntry::new(frames.clone(), ctx.level); - - #[cfg(debug_assertions)] - if !entry.check(regions) { - eprintln!("node: {:#?}", self.node); - eprintln!("regions: {:#?}", regions); - eprintln!( - "constraints: {:#?}", - frames.iter().map(|c| c.cts).collect::>() - ); - panic!("constraints did not match regions they were created for"); - } - - ctx.layouts.insert(self.hash, entry); - frames - }) - } - - fn pack(self) -> BlockNode - where - Self: Sized + Hash + 'static, - { - self - } -} - -impl Hash for BlockNode { - fn hash(&self, _state: &mut H) { - #[cfg(feature = "layout-cache")] - _state.write_u64(self.hash); - #[cfg(not(feature = "layout-cache"))] - unimplemented!() - } -} - -impl Debug for BlockNode { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - self.node.fmt(f) - } -} - -/// Inline-level nodes are layouted as part of paragraph layout. -/// -/// They only know the width and not the height of the paragraph's region and -/// return only a single frame. -pub trait InlineLevel: Debug { - /// Layout the node into a frame. - fn layout(&self, ctx: &mut LayoutContext, space: Length, base: Size) -> Frame; - - /// Convert to a packed inline-level node. - fn pack(self) -> InlineNode - where - Self: Sized + Hash + 'static, - { - InlineNode { - #[cfg(feature = "layout-cache")] - hash: hash_node(&self), - node: Rc::new(self), - } - } -} - -/// A packed [inline-level](InlineLevel) layouting node with precomputed hash. -#[derive(Clone)] -pub struct InlineNode { - node: Rc, - #[cfg(feature = "layout-cache")] - hash: u64, -} - -impl InlineLevel for InlineNode { - fn layout(&self, ctx: &mut LayoutContext, space: Length, base: Size) -> Frame { - self.node.layout(ctx, space, base) - } - - fn pack(self) -> InlineNode - where - Self: Sized + Hash + 'static, - { - self - } -} - -impl Hash for InlineNode { - fn hash(&self, _state: &mut H) { - #[cfg(feature = "layout-cache")] - _state.write_u64(self.hash); - #[cfg(not(feature = "layout-cache"))] - unimplemented!() - } -} - -impl Debug for InlineNode { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - self.node.fmt(f) - } -} - -/// Hash a node alongside its type id. -#[cfg(feature = "layout-cache")] -fn hash_node(node: &(impl Hash + 'static)) -> u64 { - use std::any::Any; - let mut state = fxhash::FxHasher64::default(); - node.type_id().hash(&mut state); - node.hash(&mut state); - state.finish() -} diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 49ceccf63..7d2837de2 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -1,29 +1,27 @@ -//! Layouting. +//! Layouting infrastructure. mod constraints; #[cfg(feature = "layout-cache")] mod incremental; -mod levels; mod regions; pub use constraints::*; #[cfg(feature = "layout-cache")] pub use incremental::*; -pub use levels::*; pub use regions::*; +use std::fmt::{self, Debug, Formatter}; +use std::hash::{Hash, Hasher}; use std::rc::Rc; use crate::font::FontStore; use crate::frame::Frame; use crate::image::ImageStore; +use crate::library::DocumentNode; use crate::Context; -/// Layout a page-level node into a collection of frames. -pub fn layout(ctx: &mut Context, node: &T) -> Vec> -where - T: PageLevel + ?Sized, -{ +/// Layout a document node into a collection of frames. +pub fn layout(ctx: &mut Context, node: &DocumentNode) -> Vec> { let mut ctx = LayoutContext::new(ctx); node.layout(&mut ctx) } @@ -55,3 +53,98 @@ impl<'a> LayoutContext<'a> { } } } + +/// A node that can be layouted into a sequence of regions. +/// +/// Layout return one frame per used region alongside constraints that define +/// whether the result is reusable in other regions. +pub trait Layout: Debug { + /// Layout the node into the given regions, producing constrained frames. + fn layout( + &self, + ctx: &mut LayoutContext, + regions: &Regions, + ) -> Vec>>; + + /// Convert to a packed node. + fn pack(self) -> PackedNode + where + Self: Sized + Hash + 'static, + { + PackedNode { + #[cfg(feature = "layout-cache")] + hash: { + use std::any::Any; + let mut state = fxhash::FxHasher64::default(); + self.type_id().hash(&mut state); + self.hash(&mut state); + state.finish() + }, + node: Rc::new(self), + } + } +} + +/// A packed layouting node with precomputed hash. +#[derive(Clone)] +pub struct PackedNode { + node: Rc, + #[cfg(feature = "layout-cache")] + hash: u64, +} + +impl Layout for PackedNode { + fn layout( + &self, + ctx: &mut LayoutContext, + regions: &Regions, + ) -> Vec>> { + #[cfg(not(feature = "layout-cache"))] + return self.node.layout(ctx, regions); + + #[cfg(feature = "layout-cache")] + ctx.layouts.get(self.hash, regions).unwrap_or_else(|| { + ctx.level += 1; + let frames = self.node.layout(ctx, regions); + ctx.level -= 1; + + let entry = FramesEntry::new(frames.clone(), ctx.level); + + #[cfg(debug_assertions)] + if !entry.check(regions) { + eprintln!("node: {:#?}", self.node); + eprintln!("regions: {:#?}", regions); + eprintln!( + "constraints: {:#?}", + frames.iter().map(|c| c.cts).collect::>() + ); + panic!("constraints did not match regions they were created for"); + } + + ctx.layouts.insert(self.hash, entry); + frames + }) + } + + fn pack(self) -> PackedNode + where + Self: Sized + Hash + 'static, + { + self + } +} + +impl Hash for PackedNode { + fn hash(&self, _state: &mut H) { + #[cfg(feature = "layout-cache")] + _state.write_u64(self.hash); + #[cfg(not(feature = "layout-cache"))] + unimplemented!() + } +} + +impl Debug for PackedNode { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + self.node.fmt(f) + } +} diff --git a/src/lib.rs b/src/lib.rs index 895430a25..b05a57b37 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,9 +9,9 @@ //! [module], consisting of a scope of values that were exported by the code //! and a template with the contents of the module. This template can be //! instantiated with a style to produce a layout tree, a high-level, fully -//! styled representation of the document. The nodes of this tree are -//! self-contained and order-independent and thus much better suited for -//! layouting than the raw markup. +//! styled representation, rooted in the [document node]. The nodes of this +//! tree are self-contained and order-independent and thus much better suited +//! for layouting than the raw markup. //! - **Layouting:** Next, the tree is [layouted] into a portable version of the //! typeset document. The output of this is a collection of [`Frame`]s (one //! per page), ready for exporting. @@ -24,6 +24,7 @@ //! [evaluate]: eval::eval //! [module]: eval::Module //! [layout tree]: layout::LayoutTree +//! [document node]: library::DocumentNode //! [layouted]: layout::layout //! [PDF]: export::pdf @@ -53,9 +54,9 @@ use crate::eval::{Module, Scope}; use crate::font::FontStore; use crate::frame::Frame; use crate::image::ImageStore; -use crate::layout::PageNode; #[cfg(feature = "layout-cache")] use crate::layout::{EvictionPolicy, LayoutCache}; +use crate::library::DocumentNode; use crate::loading::Loader; use crate::source::{SourceId, SourceStore}; use crate::style::Style; @@ -107,9 +108,9 @@ impl Context { } /// Execute a source file and produce the resulting page nodes. - pub fn execute(&mut self, id: SourceId) -> TypResult> { + pub fn execute(&mut self, id: SourceId) -> TypResult { let module = self.evaluate(id)?; - Ok(module.template.to_pages(&self.style)) + Ok(module.template.to_document(&self.style)) } /// Typeset a source file into a collection of layouted frames. diff --git a/src/library/container.rs b/src/library/container.rs index 4b52139fb..2bcbbd192 100644 --- a/src/library/container.rs +++ b/src/library/container.rs @@ -5,14 +5,13 @@ use super::{ShapeKind, ShapeNode}; pub fn box_(_: &mut EvalContext, args: &mut Args) -> TypResult { let width = args.named("width")?; let height = args.named("height")?; - let fill = args.named("fill")?; let body: Template = args.find().unwrap_or_default(); Ok(Value::Template(Template::from_inline(move |style| { ShapeNode { shape: ShapeKind::Rect, width, height, - fill: fill.map(Paint::Color), + fill: None, child: Some(body.to_flow(style).pack()), } }))) diff --git a/src/library/document.rs b/src/library/document.rs new file mode 100644 index 000000000..fe01d2df2 --- /dev/null +++ b/src/library/document.rs @@ -0,0 +1,16 @@ +use super::prelude::*; +use super::PageNode; + +/// The root layout node, a document consisting of top-level page runs. +#[derive(Debug, Hash)] +pub struct DocumentNode { + /// The page runs. + pub pages: Vec, +} + +impl DocumentNode { + /// Layout the document into a sequence of frames, one per page. + pub fn layout(&self, ctx: &mut LayoutContext) -> Vec> { + self.pages.iter().flat_map(|node| node.layout(ctx)).collect() + } +} diff --git a/src/library/flow.rs b/src/library/flow.rs index cc53d5207..c41fb62c6 100644 --- a/src/library/flow.rs +++ b/src/library/flow.rs @@ -48,7 +48,7 @@ pub struct FlowNode { pub children: Vec, } -impl BlockLevel for FlowNode { +impl Layout for FlowNode { fn layout( &self, ctx: &mut LayoutContext, @@ -63,8 +63,8 @@ impl BlockLevel for FlowNode { pub enum FlowChild { /// Vertical spacing between other children. Spacing(Spacing), - /// Any block node and how to align it in the flow. - Node(BlockNode, Align), + /// A node and how to align it in the flow. + Node(PackedNode, Align), } impl Debug for FlowChild { @@ -157,8 +157,8 @@ impl<'a> FlowLayouter<'a> { self.items.push(FlowItem::Absolute(resolved)); } - /// Layout a block node. - fn layout_node(&mut self, ctx: &mut LayoutContext, node: &BlockNode, align: Align) { + /// Layout a node. + fn layout_node(&mut self, ctx: &mut LayoutContext, node: &PackedNode, align: Align) { let frames = node.layout(ctx, &self.regions); let len = frames.len(); for (i, frame) in frames.into_iter().enumerate() { diff --git a/src/library/grid.rs b/src/library/grid.rs index bafd639ca..692ed210f 100644 --- a/src/library/grid.rs +++ b/src/library/grid.rs @@ -58,7 +58,7 @@ pub struct GridNode { /// Defines sizing of gutter rows and columns between content. pub gutter: Spec>, /// The nodes to be arranged in a grid. - pub children: Vec, + pub children: Vec, } /// Defines how to size a grid cell along an axis. @@ -72,7 +72,7 @@ pub enum TrackSizing { Fractional(Fractional), } -impl BlockLevel for GridNode { +impl Layout for GridNode { fn layout( &self, ctx: &mut LayoutContext, @@ -92,7 +92,7 @@ impl BlockLevel for GridNode { /// Performs grid layout. struct GridLayouter<'a> { /// The children of the grid. - children: &'a [BlockNode], + children: &'a [PackedNode], /// Whether the grid should expand to fill the region. expand: Spec, /// The column tracks including gutter tracks. @@ -591,7 +591,7 @@ impl<'a> GridLayouter<'a> { /// /// Returns `None` if it's a gutter cell. #[track_caller] - fn cell(&self, x: usize, y: usize) -> Option<&'a BlockNode> { + fn cell(&self, x: usize, y: usize) -> Option<&'a PackedNode> { assert!(x < self.cols.len()); assert!(y < self.rows.len()); diff --git a/src/library/image.rs b/src/library/image.rs index b51e4e70a..f93d4b548 100644 --- a/src/library/image.rs +++ b/src/library/image.rs @@ -36,23 +36,31 @@ pub struct ImageNode { pub height: Option, } -impl InlineLevel for ImageNode { - fn layout(&self, ctx: &mut LayoutContext, space: Length, base: Size) -> Frame { +impl Layout for ImageNode { + fn layout( + &self, + ctx: &mut LayoutContext, + regions: &Regions, + ) -> Vec>> { let img = ctx.images.get(self.id); let pixel_size = Spec::new(img.width() as f64, img.height() as f64); let pixel_ratio = pixel_size.x / pixel_size.y; - let width = self.width.map(|w| w.resolve(base.w)); - let height = self.height.map(|w| w.resolve(base.h)); + let width = self.width.map(|w| w.resolve(regions.base.w)); + let height = self.height.map(|w| w.resolve(regions.base.h)); + + let mut cts = Constraints::new(regions.expand); + cts.set_base_if_linear(regions.base, Spec::new(self.width, self.height)); let size = match (width, height) { (Some(width), Some(height)) => Size::new(width, height), (Some(width), None) => Size::new(width, width / pixel_ratio), (None, Some(height)) => Size::new(height * pixel_ratio, height), (None, None) => { - if space.is_finite() { + cts.exact.x = Some(regions.current.w); + if regions.current.w.is_finite() { // Fit to width. - Size::new(space, space / pixel_ratio) + Size::new(regions.current.w, regions.current.w / pixel_ratio) } else { // Unbounded width, we have to make up something, // so it is 1pt per pixel. @@ -63,6 +71,7 @@ impl InlineLevel for ImageNode { let mut frame = Frame::new(size, size.h); frame.push(Point::zero(), Element::Image(self.id, size)); - frame + + vec![frame.constrain(cts)] } } diff --git a/src/library/mod.rs b/src/library/mod.rs index 775fb51f3..f11e05f39 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -6,6 +6,7 @@ mod align; mod container; mod deco; +mod document; mod flow; mod grid; mod image; @@ -36,6 +37,7 @@ pub use self::image::*; pub use align::*; pub use container::*; pub use deco::*; +pub use document::*; pub use flow::*; pub use grid::*; pub use pad::*; diff --git a/src/library/pad.rs b/src/library/pad.rs index d17a4ca23..88f8c5622 100644 --- a/src/library/pad.rs +++ b/src/library/pad.rs @@ -16,7 +16,7 @@ pub fn pad(_: &mut EvalContext, args: &mut Args) -> TypResult { bottom.or(all).unwrap_or_default(), ); - Ok(Value::Template(Template::from_block(move |style| { + Ok(Value::Template(Template::from_inline(move |style| { PadNode { padding, child: body.to_flow(style).pack(), @@ -30,10 +30,10 @@ pub struct PadNode { /// The amount of padding. pub padding: Sides, /// The child node whose sides to pad. - pub child: BlockNode, + pub child: PackedNode, } -impl BlockLevel for PadNode { +impl Layout for PadNode { fn layout( &self, ctx: &mut LayoutContext, diff --git a/src/library/page.rs b/src/library/page.rs index 16e6283de..a5935002b 100644 --- a/src/library/page.rs +++ b/src/library/page.rs @@ -73,3 +73,23 @@ pub fn pagebreak(_: &mut EvalContext, _: &mut Args) -> TypResult { template.pagebreak(true); Ok(Value::Template(template)) } + +/// Layouts its children onto one or multiple pages. +#[derive(Debug, Hash)] +pub struct PageNode { + /// The size of the page. + pub size: Size, + /// The node that produces the actual pages. + pub child: PackedNode, +} + +impl PageNode { + /// Layout the page run into a sequence of frames, one per page. + pub fn layout(&self, ctx: &mut LayoutContext) -> Vec> { + // When one of the lengths is infinite the page fits its content along + // that axis. + let expand = self.size.to_spec().map(Length::is_finite); + let regions = Regions::repeat(self.size, self.size, expand); + self.child.layout(ctx, ®ions).into_iter().map(|c| c.item).collect() + } +} diff --git a/src/library/par.rs b/src/library/par.rs index 216b7d410..c8befc2d0 100644 --- a/src/library/par.rs +++ b/src/library/par.rs @@ -8,7 +8,7 @@ use xi_unicode::LineBreakIterator; use super::prelude::*; use super::{shape, Decoration, ShapedText, Spacing}; use crate::style::TextStyle; -use crate::util::{EcoString, RangeExt, SliceExt}; +use crate::util::{EcoString, RangeExt, RcExt, SliceExt}; /// `par`: Configure paragraphs. pub fn par(ctx: &mut EvalContext, args: &mut Args) -> TypResult { @@ -63,7 +63,7 @@ pub struct ParNode { pub children: Vec, } -impl BlockLevel for ParNode { +impl Layout for ParNode { fn layout( &self, ctx: &mut LayoutContext, @@ -77,7 +77,7 @@ impl BlockLevel for ParNode { // Prepare paragraph layout by building a representation on which we can // do line breaking without layouting each and every line from scratch. - let layouter = ParLayouter::new(self, ctx, regions, bidi); + let layouter = ParLayouter::new(self, ctx, regions.clone(), bidi); // Find suitable linebreaks. layouter.layout(ctx, regions.clone()) @@ -126,7 +126,7 @@ pub enum ParChild { /// A run of text and how to align it in its line. Text(EcoString, Align, Rc), /// Any child node and how to align it in its line. - Node(InlineNode, Align), + Node(PackedNode, Align), /// A decoration that applies until a matching `Undecorate`. Decorate(Decoration), /// The end of a decoration. @@ -182,9 +182,12 @@ impl<'a> ParLayouter<'a> { fn new( par: &'a ParNode, ctx: &mut LayoutContext, - regions: &Regions, + mut regions: Regions, bidi: BidiInfo<'a>, ) -> Self { + // Disable expansion for children. + regions.expand = Spec::splat(false); + let mut items = vec![]; let mut ranges = vec![]; let mut starts = vec![]; @@ -216,8 +219,8 @@ impl<'a> ParLayouter<'a> { } } ParChild::Node(ref node, align) => { - let frame = node.layout(ctx, regions.current.w, regions.base); - items.push(ParItem::Frame(frame, align)); + let frame = node.layout(ctx, ®ions).remove(0); + items.push(ParItem::Frame(Rc::take(frame.item), align)); ranges.push(range); } ParChild::Decorate(ref deco) => { diff --git a/src/library/shape.rs b/src/library/shape.rs index 5be26aa49..c64dedb32 100644 --- a/src/library/shape.rs +++ b/src/library/shape.rs @@ -94,7 +94,7 @@ pub struct ShapeNode { /// How to fill the shape, if at all. pub fill: Option, /// The child node to place into the shape, if any. - pub child: Option, + pub child: Option, } /// The type of a shape. @@ -110,15 +110,36 @@ pub enum ShapeKind { Ellipse, } -impl InlineLevel for ShapeNode { - fn layout(&self, ctx: &mut LayoutContext, space: Length, base: Size) -> Frame { +impl Layout for ShapeNode { + fn layout( + &self, + ctx: &mut LayoutContext, + regions: &Regions, + ) -> Vec>> { // Resolve width and height relative to the region's base. - let width = self.width.map(|w| w.resolve(base.w)); - let height = self.height.map(|h| h.resolve(base.h)); + let width = self.width.map(|w| w.resolve(regions.base.w)); + let height = self.height.map(|h| h.resolve(regions.base.h)); + + // Generate constraints. + let mut cts = Constraints::new(regions.expand); + cts.set_base_if_linear(regions.base, Spec::new(self.width, self.height)); + + // Set tight exact and base constraints if the child is + // automatically sized since we don't know what the child might do. + if self.width.is_none() { + cts.exact.x = Some(regions.current.w); + cts.base.x = Some(regions.base.w); + } + + // Same here. + if self.height.is_none() { + cts.exact.y = Some(regions.current.h); + cts.base.y = Some(regions.base.h); + } // Layout. let mut frame = if let Some(child) = &self.child { - let mut node: &dyn BlockLevel = child; + let mut node: &dyn Layout = child; let padded; if matches!(self.shape, ShapeKind::Circle | ShapeKind::Ellipse) { @@ -133,11 +154,14 @@ impl InlineLevel for ShapeNode { // The "pod" is the region into which the child will be layouted. let mut pod = { - let size = Size::new(width.unwrap_or(space), height.unwrap_or(base.h)); + let size = Size::new( + width.unwrap_or(regions.current.w), + height.unwrap_or(regions.base.h), + ); let base = Size::new( - if width.is_some() { size.w } else { base.w }, - if height.is_some() { size.h } else { base.h }, + if width.is_some() { size.w } else { regions.base.w }, + if height.is_some() { size.h } else { regions.base.h }, ); let expand = Spec::new(width.is_some(), height.is_some()); @@ -180,6 +204,6 @@ impl InlineLevel for ShapeNode { frame.prepend(pos, Element::Geometry(geometry, fill)); } - frame + vec![frame.constrain(cts)] } } diff --git a/src/library/stack.rs b/src/library/stack.rs index fe1deda0f..46825939f 100644 --- a/src/library/stack.rs +++ b/src/library/stack.rs @@ -60,7 +60,7 @@ pub struct StackNode { pub children: Vec, } -impl BlockLevel for StackNode { +impl Layout for StackNode { fn layout( &self, ctx: &mut LayoutContext, @@ -76,7 +76,7 @@ pub enum StackChild { /// Spacing between other nodes. Spacing(Spacing), /// Any block node and how to align it in the stack. - Node(BlockNode), + Node(PackedNode), } impl Debug for StackChild { @@ -174,8 +174,8 @@ impl<'a> StackLayouter<'a> { self.items.push(StackItem::Absolute(resolved)); } - /// Layout a block node. - fn layout_node(&mut self, ctx: &mut LayoutContext, node: &BlockNode) { + /// Layout a node. + fn layout_node(&mut self, ctx: &mut LayoutContext, node: &PackedNode) { let frames = node.layout(ctx, &self.regions); let len = frames.len(); for (i, frame) in frames.into_iter().enumerate() { diff --git a/src/library/transform.rs b/src/library/transform.rs index b7e5e36c2..20d2bc1dc 100644 --- a/src/library/transform.rs +++ b/src/library/transform.rs @@ -1,5 +1,4 @@ use super::prelude::*; -use super::{ShapeKind, ShapeNode}; /// `move`: Move content without affecting layout. pub fn move_(_: &mut EvalContext, args: &mut Args) -> TypResult { @@ -10,13 +9,7 @@ pub fn move_(_: &mut EvalContext, args: &mut Args) -> TypResult { Ok(Value::Template(Template::from_inline(move |style| { MoveNode { offset: Spec::new(x, y), - child: ShapeNode { - shape: ShapeKind::Rect, - width: None, - height: None, - fill: None, - child: Some(body.to_flow(style).pack()), - }, + child: body.to_flow(style).pack(), } }))) } @@ -24,21 +17,30 @@ pub fn move_(_: &mut EvalContext, args: &mut Args) -> TypResult { #[derive(Debug, Hash)] struct MoveNode { offset: Spec>, - child: ShapeNode, + child: PackedNode, } -impl InlineLevel for MoveNode { - fn layout(&self, ctx: &mut LayoutContext, space: Length, base: Size) -> Frame { - let offset = Point::new( - self.offset.x.map(|x| x.resolve(base.w)).unwrap_or_default(), - self.offset.y.map(|y| y.resolve(base.h)).unwrap_or_default(), - ); +impl Layout for MoveNode { + fn layout( + &self, + ctx: &mut LayoutContext, + regions: &Regions, + ) -> Vec>> { + let mut frames = self.child.layout(ctx, regions); - let mut frame = self.child.layout(ctx, space, base); - for (point, _) in &mut frame.children { - *point += offset; + for (Constrained { item: frame, .. }, (_, base)) in + frames.iter_mut().zip(regions.iter()) + { + let offset = Point::new( + self.offset.x.map(|x| x.resolve(base.w)).unwrap_or_default(), + self.offset.y.map(|y| y.resolve(base.h)).unwrap_or_default(), + ); + + for (point, _) in &mut Rc::make_mut(frame).children { + *point += offset; + } } - frame + frames } } diff --git a/src/main.rs b/src/main.rs index b2ae1ea3a..fa8b61038 100644 --- a/src/main.rs +++ b/src/main.rs @@ -60,8 +60,8 @@ fn try_main() -> anyhow::Result<()> { // Typeset. match ctx.typeset(id) { // Export the PDF. - Ok(document) => { - let buffer = export::pdf(&ctx, &document); + Ok(frames) => { + let buffer = export::pdf(&ctx, &frames); fs::write(&args.output, buffer).context("failed to write PDF file")?; } diff --git a/src/source.rs b/src/source.rs index e692ac94d..74fa8d55b 100644 --- a/src/source.rs +++ b/src/source.rs @@ -1,4 +1,4 @@ -//! Source files. +//! Source file management. use std::collections::HashMap; use std::io; diff --git a/tests/ref/layout/pad.png b/tests/ref/layout/pad.png index 4d381ce24..6ce4016a8 100644 Binary files a/tests/ref/layout/pad.png and b/tests/ref/layout/pad.png differ diff --git a/tests/typ/layout/pad.typ b/tests/typ/layout/pad.typ index 3729761a8..2a91d1ca7 100644 --- a/tests/typ/layout/pad.typ +++ b/tests/typ/layout/pad.typ @@ -11,19 +11,11 @@ ) ) -Hi #box(pad(left: 10pt)[]) there +Hi #pad(left: 10pt)[A] there --- -#let pad(body) = pad(left: 10pt, right: 10pt, body) - -// Pad inherits expansion behaviour from stack .... -#pad[PL #align(right)[PR]] - -// ... block ... -#block(pad[PL #align(right)[PR]]) - -// ... and box. -#box(pad[PL #align(right)[PR]]) +// Pad can grow. +#pad(left: 10pt, right: 10pt)[PL #h(1fr) PR] --- // Test that the pad node doesn't consume the whole region. diff --git a/tests/typ/layout/stack-2.typ b/tests/typ/layout/stack-2.typ index afc9de394..2167f48f7 100644 --- a/tests/typ/layout/stack-2.typ +++ b/tests/typ/layout/stack-2.typ @@ -17,7 +17,7 @@ World! 🌍 --- #page(height: 2cm) #font(white) -#box(fill: forest)[ +#rect(fill: forest)[ #v(1fr) #h(1fr) Hi you! #h(5pt) #v(5pt) diff --git a/tests/typeset.rs b/tests/typeset.rs index 68e56343c..aa4d250bb 100644 --- a/tests/typeset.rs +++ b/tests/typeset.rs @@ -19,7 +19,7 @@ use typst::geom::{ use typst::image::Image; use typst::layout::layout; #[cfg(feature = "layout-cache")] -use typst::layout::PageNode; +use typst::library::DocumentNode; use typst::loading::FsLoader; use typst::parse::Scanner; use typst::source::SourceFile; @@ -231,11 +231,11 @@ fn test_part( let mut ok = true; let (frames, mut errors) = match ctx.execute(id) { - Ok(tree) => { - let mut frames = layout(ctx, &tree); + Ok(document) => { + let mut frames = layout(ctx, &document); #[cfg(feature = "layout-cache")] - (ok &= test_incremental(ctx, i, &tree, &frames)); + (ok &= test_incremental(ctx, i, &document, &frames)); if !compare_ref { frames.clear(); @@ -283,7 +283,7 @@ fn test_part( fn test_incremental( ctx: &mut Context, i: usize, - tree: &[PageNode], + document: &DocumentNode, frames: &[Rc], ) -> bool { let mut ok = true; @@ -298,7 +298,7 @@ fn test_incremental( ctx.layouts.turnaround(); - let cached = layout(ctx, tree); + let cached = layout(ctx, document); let misses = ctx .layouts .entries()