diff --git a/library/src/graphics/hide.rs b/library/src/graphics/hide.rs index 3a21c2c70..64cbee64d 100644 --- a/library/src/graphics/hide.rs +++ b/library/src/graphics/hide.rs @@ -4,22 +4,26 @@ use crate::prelude::*; #[derive(Debug, Hash)] pub struct HideNode(pub Content); -#[node(LayoutInline)] +#[node(Layout, Inline)] impl HideNode { fn construct(_: &Vm, args: &mut Args) -> SourceResult { Ok(Self(args.expect("body")?).pack()) } } -impl LayoutInline for HideNode { - fn layout_inline( +impl Layout for HideNode { + fn layout( &self, world: Tracked, styles: StyleChain, regions: &Regions, - ) -> SourceResult { - let mut frame = self.0.layout_inline(world, styles, regions)?; - frame.clear(); - Ok(frame) + ) -> SourceResult { + let mut fragment = self.0.layout(world, styles, regions)?; + for frame in &mut fragment { + frame.clear(); + } + Ok(fragment) } } + +impl Inline for HideNode {} diff --git a/library/src/graphics/image.rs b/library/src/graphics/image.rs index de3384df1..2c58496cc 100644 --- a/library/src/graphics/image.rs +++ b/library/src/graphics/image.rs @@ -9,7 +9,7 @@ use crate::text::LinkNode; #[derive(Debug, Hash)] pub struct ImageNode(pub Image); -#[node(LayoutInline)] +#[node(Layout, Inline)] impl ImageNode { /// How the image should adjust itself to a given area. pub const FIT: ImageFit = ImageFit::Cover; @@ -37,13 +37,13 @@ impl ImageNode { } } -impl LayoutInline for ImageNode { - fn layout_inline( +impl Layout for ImageNode { + fn layout( &self, _: Tracked, styles: StyleChain, regions: &Regions, - ) -> SourceResult { + ) -> SourceResult { let pxw = self.0.width() as f64; let pxh = self.0.height() as f64; let px_ratio = pxw / pxh; @@ -94,10 +94,12 @@ impl LayoutInline for ImageNode { frame.link(url.clone()); } - Ok(frame) + Ok(Fragment::frame(frame)) } } +impl Inline for ImageNode {} + /// How an image should adjust itself to a given area. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub enum ImageFit { diff --git a/library/src/graphics/line.rs b/library/src/graphics/line.rs index 11f0be32c..8acf5bb6e 100644 --- a/library/src/graphics/line.rs +++ b/library/src/graphics/line.rs @@ -9,7 +9,7 @@ pub struct LineNode { delta: Axes>, } -#[node(LayoutInline)] +#[node(Layout, Inline)] impl LineNode { /// How to stroke the line. #[property(resolve, fold)] @@ -36,13 +36,13 @@ impl LineNode { } } -impl LayoutInline for LineNode { - fn layout_inline( +impl Layout for LineNode { + fn layout( &self, _: Tracked, styles: StyleChain, regions: &Regions, - ) -> SourceResult { + ) -> SourceResult { let stroke = styles.get(Self::STROKE).unwrap_or_default(); let origin = self @@ -58,11 +58,13 @@ impl LayoutInline for LineNode { .map(|(l, b)| l.relative_to(b)); let target = regions.expand.select(regions.first, Size::zero()); - let mut frame = Frame::new(target); + let mut frame = Frame::new(target); let shape = Geometry::Line(delta.to_point()).stroked(stroke); frame.push(origin.to_point(), Element::Shape(shape)); - Ok(frame) + Ok(Fragment::frame(frame)) } } + +impl Inline for LineNode {} diff --git a/library/src/graphics/shape.rs b/library/src/graphics/shape.rs index 4c9fec07d..114182e5f 100644 --- a/library/src/graphics/shape.rs +++ b/library/src/graphics/shape.rs @@ -19,7 +19,7 @@ pub type CircleNode = ShapeNode; /// A ellipse with optional content. pub type EllipseNode = ShapeNode; -#[node(LayoutInline)] +#[node(Layout, Inline)] impl ShapeNode { /// How to fill the shape. pub const FILL: Option = None; @@ -72,13 +72,13 @@ impl ShapeNode { } } -impl LayoutInline for ShapeNode { - fn layout_inline( +impl Layout for ShapeNode { + fn layout( &self, world: Tracked, styles: StyleChain, regions: &Regions, - ) -> SourceResult { + ) -> SourceResult { let mut frame; if let Some(child) = &self.0 { let mut inset = styles.get(Self::INSET); @@ -90,7 +90,7 @@ impl LayoutInline for ShapeNode { let child = child.clone().padded(inset.map(|side| side.map(Length::from))); let mut pod = Regions::one(regions.first, regions.base, regions.expand); - frame = child.layout_inline(world, styles, &pod)?; + frame = child.layout(world, styles, &pod)?.into_frame(); // Relayout with full expansion into square region to make sure // the result is really a square or circle. @@ -106,7 +106,7 @@ impl LayoutInline for ShapeNode { pod.first = Size::splat(length); pod.expand = Axes::splat(true); - frame = child.layout_inline(world, styles, &pod)?; + frame = child.layout(world, styles, &pod)?.into_frame(); } } else { // The default size that a shape takes on if it has no child and @@ -165,10 +165,12 @@ impl LayoutInline for ShapeNode { frame.link(url.clone()); } - Ok(frame) + Ok(Fragment::frame(frame)) } } +impl Inline for ShapeNode {} + /// A category of shape. pub type ShapeKind = usize; diff --git a/library/src/layout/align.rs b/library/src/layout/align.rs index 10a4a2ed3..d8b6d92e8 100644 --- a/library/src/layout/align.rs +++ b/library/src/layout/align.rs @@ -10,14 +10,14 @@ pub struct AlignNode { pub child: Content, } -#[node(LayoutBlock)] +#[node(Layout)] impl AlignNode { fn construct(_: &Vm, args: &mut Args) -> SourceResult { let aligns: Axes> = args.find()?.unwrap_or_default(); let body: Content = args.expect("body")?; if let Axes { x: Some(x), y: None } = aligns { - if !body.has::() { + if !body.has::() || body.has::() { return Ok(body.styled(ParNode::ALIGN, HorizontalAlign(x))); } } @@ -26,13 +26,13 @@ impl AlignNode { } } -impl LayoutBlock for AlignNode { - fn layout_block( +impl Layout for AlignNode { + fn layout( &self, world: Tracked, styles: StyleChain, regions: &Regions, - ) -> SourceResult> { + ) -> SourceResult { // The child only needs to expand along an axis if there's no alignment. let mut pod = regions.clone(); pod.expand &= self.aligns.as_ref().map(Option::is_none); @@ -44,8 +44,8 @@ impl LayoutBlock for AlignNode { } // Layout the child. - let mut frames = self.child.layout_block(world, styles.chain(&map), &pod)?; - for (region, frame) in regions.iter().zip(&mut frames) { + let mut fragment = self.child.layout(world, styles.chain(&map), &pod)?; + for (region, frame) in regions.iter().zip(&mut fragment) { // Align in the target size. The target size depends on whether we // should expand. let target = regions.expand.select(region, frame.size()); @@ -57,6 +57,6 @@ impl LayoutBlock for AlignNode { frame.resize(target, aligns); } - Ok(frames) + Ok(fragment) } } diff --git a/library/src/layout/columns.rs b/library/src/layout/columns.rs index b18ba49fe..257cc62f6 100644 --- a/library/src/layout/columns.rs +++ b/library/src/layout/columns.rs @@ -11,7 +11,7 @@ pub struct ColumnsNode { pub child: Content, } -#[node(LayoutBlock)] +#[node(Layout)] impl ColumnsNode { /// The size of the gutter space between each column. #[property(resolve)] @@ -26,17 +26,17 @@ impl ColumnsNode { } } -impl LayoutBlock for ColumnsNode { - fn layout_block( +impl Layout for ColumnsNode { + fn layout( &self, world: Tracked, styles: StyleChain, regions: &Regions, - ) -> SourceResult> { + ) -> SourceResult { // Separating the infinite space into infinite columns does not make // much sense. if !regions.first.x.is_finite() { - return self.child.layout_block(world, styles, regions); + return self.child.layout(world, styles, regions); } // Determine the width of the gutter and each column. @@ -58,7 +58,7 @@ impl LayoutBlock for ColumnsNode { }; // Layout the children. - let mut frames = self.child.layout_block(world, styles, &pod)?.into_iter(); + let mut frames = self.child.layout(world, styles, &pod)?.into_iter(); let mut finished = vec![]; let dir = styles.get(TextNode::DIR); @@ -94,7 +94,7 @@ impl LayoutBlock for ColumnsNode { finished.push(output); } - Ok(finished) + Ok(Fragment::frames(finished)) } } diff --git a/library/src/layout/container.rs b/library/src/layout/container.rs index b299c0fcc..1c1f87629 100644 --- a/library/src/layout/container.rs +++ b/library/src/layout/container.rs @@ -10,7 +10,7 @@ pub struct BoxNode { pub child: Content, } -#[node(LayoutInline)] +#[node(Layout, Inline)] impl BoxNode { fn construct(_: &Vm, args: &mut Args) -> SourceResult { let width = args.named("width")?; @@ -20,13 +20,13 @@ impl BoxNode { } } -impl LayoutInline for BoxNode { - fn layout_inline( +impl Layout for BoxNode { + fn layout( &self, world: Tracked, styles: StyleChain, regions: &Regions, - ) -> SourceResult { + ) -> SourceResult { // The "pod" is the region into which the child will be layouted. let pod = { // Resolve the sizing to a concrete size. @@ -47,21 +47,23 @@ impl LayoutInline for BoxNode { }; // Layout the child. - let mut frame = self.child.layout_inline(world, styles, &pod)?; + let mut frame = self.child.layout(world, styles, &pod)?.into_frame(); // Ensure frame size matches regions size if expansion is on. let target = regions.expand.select(regions.first, frame.size()); frame.resize(target, Align::LEFT_TOP); - Ok(frame) + Ok(Fragment::frame(frame)) } } +impl Inline for BoxNode {} + /// A block-level container that places content into a separate flow. #[derive(Debug, Hash)] pub struct BlockNode(pub Content); -#[node(LayoutBlock)] +#[node(Layout)] impl BlockNode { /// The spacing between the previous and this block. #[property(skip)] @@ -87,13 +89,13 @@ impl BlockNode { } } -impl LayoutBlock for BlockNode { - fn layout_block( +impl Layout for BlockNode { + fn layout( &self, world: Tracked, styles: StyleChain, regions: &Regions, - ) -> SourceResult> { - self.0.layout_block(world, styles, regions) + ) -> SourceResult { + self.0.layout(world, styles, regions) } } diff --git a/library/src/layout/flow.rs b/library/src/layout/flow.rs index 3338da097..fd3e5fc70 100644 --- a/library/src/layout/flow.rs +++ b/library/src/layout/flow.rs @@ -11,23 +11,23 @@ use crate::text::ParNode; #[derive(Hash)] pub struct FlowNode(pub StyleVec); -#[node(LayoutBlock)] +#[node(Layout)] impl FlowNode {} -impl LayoutBlock for FlowNode { - fn layout_block( +impl Layout for FlowNode { + fn layout( &self, world: Tracked, styles: StyleChain, regions: &Regions, - ) -> SourceResult> { + ) -> SourceResult { let mut layouter = FlowLayouter::new(regions); for (child, map) in self.0.iter() { let styles = styles.chain(&map); if let Some(&node) = child.to::() { layouter.layout_spacing(node.amount, styles); - } else if child.has::() { + } else if child.has::() { layouter.layout_block(world, child, styles)?; } else if child.is::() { layouter.finish_region(); @@ -136,7 +136,7 @@ impl FlowLayouter { // aligned later. if let Some(placed) = block.to::() { if placed.out_of_flow() { - let frame = block.layout_block(world, styles, &self.regions)?.remove(0); + let frame = block.layout(world, styles, &self.regions)?.into_frame(); self.items.push(FlowItem::Placed(frame)); return Ok(()); } @@ -166,9 +166,9 @@ impl FlowLayouter { } // Layout the block itself. - let frames = block.layout_block(world, chained, &self.regions)?; - let len = frames.len(); - for (i, frame) in frames.into_iter().enumerate() { + let fragment = block.layout(world, chained, &self.regions)?; + let len = fragment.len(); + for (i, frame) in fragment.into_iter().enumerate() { // Grow our size, shrink the region and save the frame for later. let size = frame.size(); self.used.y += size.y; @@ -234,8 +234,8 @@ impl FlowLayouter { self.finished.push(output); } - /// Finish layouting and return the resulting frames. - fn finish(mut self) -> Vec { + /// Finish layouting and return the resulting fragment. + fn finish(mut self) -> Fragment { if self.expand.y { while !self.regions.backlog.is_empty() { self.finish_region(); @@ -243,6 +243,6 @@ impl FlowLayouter { } self.finish_region(); - self.finished + Fragment::frames(self.finished) } } diff --git a/library/src/layout/grid.rs b/library/src/layout/grid.rs index 4cbef4214..470b1f3bb 100644 --- a/library/src/layout/grid.rs +++ b/library/src/layout/grid.rs @@ -13,7 +13,7 @@ pub struct GridNode { pub cells: Vec, } -#[node(LayoutBlock)] +#[node(Layout)] impl GridNode { fn construct(_: &Vm, args: &mut Args) -> SourceResult { let TrackSizings(columns) = args.named("columns")?.unwrap_or_default(); @@ -33,13 +33,13 @@ impl GridNode { } } -impl LayoutBlock for GridNode { - fn layout_block( +impl Layout for GridNode { + fn layout( &self, world: Tracked, styles: StyleChain, regions: &Regions, - ) -> SourceResult> { + ) -> SourceResult { // Prepare grid layout by unifying content and gutter tracks. let layouter = GridLayouter::new( world, @@ -222,7 +222,7 @@ impl<'a> GridLayouter<'a> { } /// Determines the columns sizes and then layouts the grid row-by-row. - fn layout(mut self) -> SourceResult> { + fn layout(mut self) -> SourceResult { self.measure_columns()?; for y in 0..self.rows.len() { @@ -243,7 +243,7 @@ impl<'a> GridLayouter<'a> { } self.finish_region()?; - Ok(self.finished) + Ok(Fragment::frames(self.finished)) } /// Determine all column sizes. @@ -320,8 +320,7 @@ impl<'a> GridLayouter<'a> { v.resolve(self.styles).relative_to(self.regions.base.y); } - let frame = - cell.layout_block(self.world, self.styles, &pod)?.remove(0); + let frame = cell.layout(self.world, self.styles, &pod)?.into_frame(); resolved.set_max(frame.width()); } } @@ -391,7 +390,7 @@ impl<'a> GridLayouter<'a> { } let mut sizes = cell - .layout_block(self.world, self.styles, &pod)? + .layout(self.world, self.styles, &pod)? .into_iter() .map(|frame| frame.height()); @@ -429,9 +428,9 @@ impl<'a> GridLayouter<'a> { } // Layout into multiple regions. - let frames = self.layout_multi_row(&resolved, y)?; - let len = frames.len(); - for (i, frame) in frames.into_iter().enumerate() { + let fragment = self.layout_multi_row(&resolved, y)?; + let len = fragment.len(); + for (i, frame) in fragment.into_iter().enumerate() { self.push_row(frame); if i + 1 < len { self.finish_region()?; @@ -480,7 +479,7 @@ impl<'a> GridLayouter<'a> { .select(self.regions.base, size); let pod = Regions::one(size, base, Axes::splat(true)); - let frame = cell.layout_block(self.world, self.styles, &pod)?.remove(0); + let frame = cell.layout(self.world, self.styles, &pod)?.into_frame(); output.push_frame(pos, frame); } @@ -491,11 +490,7 @@ impl<'a> GridLayouter<'a> { } /// Layout a row spanning multiple regions. - fn layout_multi_row( - &mut self, - heights: &[Abs], - y: usize, - ) -> SourceResult> { + fn layout_multi_row(&mut self, heights: &[Abs], y: usize) -> SourceResult { // Prepare frames. let mut outputs: Vec<_> = heights .iter() @@ -520,8 +515,8 @@ impl<'a> GridLayouter<'a> { } // Push the layouted frames into the individual output frames. - let frames = cell.layout_block(self.world, self.styles, &pod)?; - for (output, frame) in outputs.iter_mut().zip(frames) { + let fragment = cell.layout(self.world, self.styles, &pod)?; + for (output, frame) in outputs.iter_mut().zip(fragment) { output.push_frame(pos, frame); } } @@ -529,7 +524,7 @@ impl<'a> GridLayouter<'a> { pos.x += rcol; } - Ok(outputs) + Ok(Fragment::frames(outputs)) } /// Push a row frame into the current region. diff --git a/library/src/layout/mod.rs b/library/src/layout/mod.rs index e63a072e6..3481a6bdf 100644 --- a/library/src/layout/mod.rs +++ b/library/src/layout/mod.rs @@ -29,7 +29,6 @@ use std::mem; use comemo::Tracked; use typed_arena::Arena; use typst::diag::SourceResult; -use typst::doc::Frame; use typst::geom::*; use typst::model::{ applicable, capability, realize, Content, Node, SequenceNode, Style, StyleChain, @@ -70,72 +69,37 @@ impl LayoutRoot for Content { } } -/// Block-level layout. +/// Layout into regions. #[capability] -pub trait LayoutBlock { +pub trait Layout { /// Layout into one frame per region. - fn layout_block( + fn layout( &self, world: Tracked, styles: StyleChain, regions: &Regions, - ) -> SourceResult>; + ) -> SourceResult; } -impl LayoutBlock for Content { +impl Layout for Content { #[comemo::memoize] - fn layout_block( + fn layout( &self, world: Tracked, styles: StyleChain, regions: &Regions, - ) -> SourceResult> { + ) -> SourceResult { let scratch = Scratch::default(); let (realized, styles) = realize_block(world, &scratch, self, styles)?; let barrier = Style::Barrier(realized.id()); let styles = styles.chain_one(&barrier); - realized - .with::() - .unwrap() - .layout_block(world, styles, regions) + realized.with::().unwrap().layout(world, styles, regions) } } /// Inline-level layout. #[capability] -pub trait LayoutInline { - /// Layout into a single frame. - fn layout_inline( - &self, - world: Tracked, - styles: StyleChain, - regions: &Regions, - ) -> SourceResult; -} - -impl LayoutInline for Content { - #[comemo::memoize] - fn layout_inline( - &self, - world: Tracked, - styles: StyleChain, - regions: &Regions, - ) -> SourceResult { - assert!(regions.backlog.is_empty()); - assert!(regions.last.is_none()); - - if self.has::() && !applicable(self, styles) { - let barrier = Style::Barrier(self.id()); - let styles = styles.chain_one(&barrier); - return self - .with::() - .unwrap() - .layout_inline(world, styles, regions); - } - - Ok(self.layout_block(world, styles, regions)?.remove(0)) - } -} +pub trait Inline: Layout {} /// A sequence of regions to layout into. #[derive(Debug, Clone, Hash)] @@ -255,7 +219,7 @@ fn realize_block<'a>( content: &'a Content, styles: StyleChain<'a>, ) -> SourceResult<(Content, StyleChain<'a>)> { - if content.has::() && !applicable(content, styles) { + if content.has::() && !applicable(content, styles) { return Ok((content.clone(), styles)); } @@ -497,7 +461,7 @@ impl<'a> FlowBuilder<'a> { return true; } - if content.has::() { + if content.has::() { let is_tight_list = if let Some(node) = content.to::() { node.tight } else if let Some(node) = content.to::() { @@ -542,7 +506,7 @@ impl<'a> ParBuilder<'a> { || content.is::() || content.is::() || content.is::() - || content.has::() + || content.has::() { self.0.push(content.clone(), styles); return true; diff --git a/library/src/layout/pad.rs b/library/src/layout/pad.rs index 4389d990f..c688dd478 100644 --- a/library/src/layout/pad.rs +++ b/library/src/layout/pad.rs @@ -9,7 +9,7 @@ pub struct PadNode { pub child: Content, } -#[node(LayoutBlock)] +#[node(Layout)] impl PadNode { fn construct(_: &Vm, args: &mut Args) -> SourceResult { let all = args.named("rest")?.or(args.find()?); @@ -25,19 +25,19 @@ impl PadNode { } } -impl LayoutBlock for PadNode { - fn layout_block( +impl Layout for PadNode { + fn layout( &self, world: Tracked, styles: StyleChain, regions: &Regions, - ) -> SourceResult> { + ) -> SourceResult { // Layout child into padded regions. let padding = self.padding.resolve(styles); let pod = regions.map(|size| shrink(size, padding)); - let mut frames = self.child.layout_block(world, styles, &pod)?; + let mut fragment = self.child.layout(world, styles, &pod)?; - for frame in &mut frames { + for frame in &mut fragment { // Apply the padding inversely such that the grown size padded // yields the frame's size. let padded = grow(frame.size(), padding); @@ -49,7 +49,7 @@ impl LayoutBlock for PadNode { frame.translate(offset); } - Ok(frames) + Ok(fragment) } } diff --git a/library/src/layout/page.rs b/library/src/layout/page.rs index 42db02c39..9fe608ee9 100644 --- a/library/src/layout/page.rs +++ b/library/src/layout/page.rs @@ -60,7 +60,7 @@ impl PageNode { world: Tracked, mut page: usize, styles: StyleChain, - ) -> SourceResult> { + ) -> SourceResult { // When one of the lengths is infinite the page fits its content along // that axis. let width = styles.get(Self::WIDTH).unwrap_or(Abs::inf()); @@ -97,7 +97,7 @@ impl PageNode { // Layout the child. let regions = Regions::repeat(size, size, size.map(Abs::is_finite)); - let mut frames = child.layout_block(world, styles, ®ions)?; + let mut fragment = child.layout(world, styles, ®ions)?; let header = styles.get(Self::HEADER); let footer = styles.get(Self::FOOTER); @@ -105,7 +105,7 @@ impl PageNode { let background = styles.get(Self::BACKGROUND); // Realize overlays. - for frame in &mut frames { + for frame in &mut fragment { let size = frame.size(); let pad = padding.resolve(styles).relative_to(size); let pw = size.x - pad.left - pad.right; @@ -118,7 +118,7 @@ impl PageNode { ] { if let Some(content) = marginal.resolve(world, page)? { let pod = Regions::one(area, area, Axes::splat(true)); - let sub = content.layout_block(world, styles, &pod)?.remove(0); + let sub = content.layout(world, styles, &pod)?.into_frame(); if std::ptr::eq(marginal, background) { frame.prepend_frame(pos, sub); } else { @@ -130,7 +130,7 @@ impl PageNode { page += 1; } - Ok(frames) + Ok(fragment) } } diff --git a/library/src/layout/place.rs b/library/src/layout/place.rs index af3130738..215b5b9f2 100644 --- a/library/src/layout/place.rs +++ b/library/src/layout/place.rs @@ -5,7 +5,7 @@ use crate::prelude::*; #[derive(Debug, Hash)] pub struct PlaceNode(pub Content); -#[node(LayoutBlock, Behave)] +#[node(Layout, Behave)] impl PlaceNode { fn construct(_: &Vm, args: &mut Args) -> SourceResult { let aligns = args.find()?.unwrap_or(Axes::with_x(Some(GenAlign::Start))); @@ -16,13 +16,13 @@ impl PlaceNode { } } -impl LayoutBlock for PlaceNode { - fn layout_block( +impl Layout for PlaceNode { + fn layout( &self, world: Tracked, styles: StyleChain, regions: &Regions, - ) -> SourceResult> { + ) -> SourceResult { let out_of_flow = self.out_of_flow(); // The pod is the base area of the region because for absolute @@ -33,14 +33,14 @@ impl LayoutBlock for PlaceNode { Regions::one(regions.base, regions.base, expand) }; - let mut frames = self.0.layout_block(world, styles, &pod)?; + let mut frame = self.0.layout(world, styles, &pod)?.into_frame(); // If expansion is off, zero all sizes so that we don't take up any // space in our parent. Otherwise, respect the expand settings. let target = regions.expand.select(regions.first, Size::zero()); - frames[0].resize(target, Align::LEFT_TOP); + frame.resize(target, Align::LEFT_TOP); - Ok(frames) + Ok(Fragment::frame(frame)) } } diff --git a/library/src/layout/stack.rs b/library/src/layout/stack.rs index c935d9715..7de1d34a6 100644 --- a/library/src/layout/stack.rs +++ b/library/src/layout/stack.rs @@ -15,7 +15,7 @@ pub struct StackNode { pub children: Vec, } -#[node(LayoutBlock)] +#[node(Layout)] impl StackNode { fn construct(_: &Vm, args: &mut Args) -> SourceResult { Ok(Self { @@ -27,13 +27,13 @@ impl StackNode { } } -impl LayoutBlock for StackNode { - fn layout_block( +impl Layout for StackNode { + fn layout( &self, world: Tracked, styles: StyleChain, regions: &Regions, - ) -> SourceResult> { + ) -> SourceResult { let mut layouter = StackLayouter::new(self.dir, regions, styles); // Spacing to insert before the next block. @@ -196,9 +196,9 @@ impl<'a> StackLayouter<'a> { self.dir.start().into() }); - let frames = block.layout_block(world, styles, &self.regions)?; - let len = frames.len(); - for (i, frame) in frames.into_iter().enumerate() { + let fragment = block.layout(world, styles, &self.regions)?; + let len = fragment.len(); + for (i, frame) in fragment.into_iter().enumerate() { // Grow our size, shrink the region and save the frame for later. let size = frame.size(); let size = match self.axis { @@ -276,9 +276,9 @@ impl<'a> StackLayouter<'a> { } /// Finish layouting and return the resulting frames. - fn finish(mut self) -> Vec { + fn finish(mut self) -> Fragment { self.finish_region(); - self.finished + Fragment::frames(self.finished) } } diff --git a/library/src/layout/transform.rs b/library/src/layout/transform.rs index f09b4e65d..cfc4ac83e 100644 --- a/library/src/layout/transform.rs +++ b/library/src/layout/transform.rs @@ -11,7 +11,7 @@ pub struct MoveNode { pub child: Content, } -#[node(LayoutInline)] +#[node(Layout, Inline)] impl MoveNode { fn construct(_: &Vm, args: &mut Args) -> SourceResult { let dx = args.named("dx")?.unwrap_or_default(); @@ -24,21 +24,25 @@ impl MoveNode { } } -impl LayoutInline for MoveNode { - fn layout_inline( +impl Layout for MoveNode { + fn layout( &self, world: Tracked, styles: StyleChain, regions: &Regions, - ) -> SourceResult { - let mut frame = self.child.layout_inline(world, styles, regions)?; - let delta = self.delta.resolve(styles); - let delta = delta.zip(frame.size()).map(|(d, s)| d.relative_to(s)); - frame.translate(delta.to_point()); - Ok(frame) + ) -> SourceResult { + let mut fragment = self.child.layout(world, styles, regions)?; + for frame in &mut fragment { + let delta = self.delta.resolve(styles); + let delta = delta.zip(frame.size()).map(|(d, s)| d.relative_to(s)); + frame.translate(delta.to_point()); + } + Ok(fragment) } } +impl Inline for MoveNode {} + /// Transform content without affecting layout. #[derive(Debug, Hash)] pub struct TransformNode { @@ -54,7 +58,7 @@ pub type RotateNode = TransformNode; /// Scale content without affecting layout. pub type ScaleNode = TransformNode; -#[node(LayoutInline)] +#[node(Layout, Inline)] impl TransformNode { /// The origin of the transformation. #[property(resolve)] @@ -78,26 +82,28 @@ impl TransformNode { } } -impl LayoutInline for TransformNode { - fn layout_inline( +impl Layout for TransformNode { + fn layout( &self, world: Tracked, styles: StyleChain, regions: &Regions, - ) -> SourceResult { - let mut frame = self.child.layout_inline(world, styles, regions)?; - - let origin = styles.get(Self::ORIGIN).unwrap_or(Align::CENTER_HORIZON); - let Axes { x, y } = origin.zip(frame.size()).map(|(o, s)| o.position(s)); - let transform = Transform::translate(x, y) - .pre_concat(self.transform) - .pre_concat(Transform::translate(-x, -y)); - frame.transform(transform); - - Ok(frame) + ) -> SourceResult { + let mut fragment = self.child.layout(world, styles, regions)?; + for frame in &mut fragment { + let origin = styles.get(Self::ORIGIN).unwrap_or(Align::CENTER_HORIZON); + let Axes { x, y } = origin.zip(frame.size()).map(|(o, s)| o.position(s)); + let transform = Transform::translate(x, y) + .pre_concat(self.transform) + .pre_concat(Transform::translate(-x, -y)); + frame.transform(transform); + } + Ok(fragment) } } +impl Inline for TransformNode {} + /// Kinds of transformations. /// /// The move transformation is handled separately. diff --git a/library/src/math/mod.rs b/library/src/math/mod.rs index 3b1d66e9a..7136c8b94 100644 --- a/library/src/math/mod.rs +++ b/library/src/math/mod.rs @@ -19,7 +19,7 @@ pub struct MathNode { pub display: bool, } -#[node(Show, LayoutInline, Texify)] +#[node(Show, Layout, Inline, Texify)] impl MathNode { fn field(&self, name: &str) -> Option { match name { @@ -48,17 +48,19 @@ impl Show for MathNode { } } -impl LayoutInline for MathNode { - fn layout_inline( +impl Layout for MathNode { + fn layout( &self, world: Tracked, styles: StyleChain, _: &Regions, - ) -> SourceResult { + ) -> SourceResult { layout_tex(world, &self.texify(), self.display, styles) } } +impl Inline for MathNode {} + impl Texify for MathNode { fn texify(&self) -> EcoString { self.children.iter().map(Texify::texify).collect() diff --git a/library/src/math/tex.rs b/library/src/math/tex.rs index a85bab189..5f332f3ce 100644 --- a/library/src/math/tex.rs +++ b/library/src/math/tex.rs @@ -39,7 +39,7 @@ pub fn layout_tex( tex: &str, display: bool, styles: StyleChain, -) -> SourceResult { +) -> SourceResult { // Load the font. let variant = variant(styles); let mut font = None; @@ -98,7 +98,8 @@ pub fn layout_tex( // Render into the frame. renderer.render(&layout, &mut backend); - Ok(backend.frame) + + Ok(Fragment::frame(backend.frame)) } /// A ReX rendering backend that renders into a frame. diff --git a/library/src/prelude.rs b/library/src/prelude.rs index bc0ec31d4..87fc8e0dc 100644 --- a/library/src/prelude.rs +++ b/library/src/prelude.rs @@ -27,6 +27,6 @@ pub use typst::util::{format_eco, EcoString}; pub use typst::World; #[doc(no_inline)] -pub use crate::layout::{LayoutBlock, LayoutInline, Regions}; +pub use crate::layout::{Inline, Layout, Regions}; #[doc(no_inline)] pub use crate::shared::{Behave, Behaviour, ContentExt, StyleMapExt}; diff --git a/library/src/shared/ext.rs b/library/src/shared/ext.rs index f90260ad4..54a1598a2 100644 --- a/library/src/shared/ext.rs +++ b/library/src/shared/ext.rs @@ -99,22 +99,22 @@ struct FillNode { child: Content, } -#[node(LayoutBlock)] +#[node(Layout)] impl FillNode {} -impl LayoutBlock for FillNode { - fn layout_block( +impl Layout for FillNode { + fn layout( &self, world: Tracked, styles: StyleChain, regions: &Regions, - ) -> SourceResult> { - let mut frames = self.child.layout_block(world, styles, regions)?; - for frame in &mut frames { + ) -> SourceResult { + let mut fragment = self.child.layout(world, styles, regions)?; + for frame in &mut fragment { let shape = Geometry::Rect(frame.size()).filled(self.fill); frame.prepend(Point::zero(), Element::Shape(shape)); } - Ok(frames) + Ok(fragment) } } @@ -127,21 +127,21 @@ struct StrokeNode { child: Content, } -#[node(LayoutBlock)] +#[node(Layout)] impl StrokeNode {} -impl LayoutBlock for StrokeNode { - fn layout_block( +impl Layout for StrokeNode { + fn layout( &self, world: Tracked, styles: StyleChain, regions: &Regions, - ) -> SourceResult> { - let mut frames = self.child.layout_block(world, styles, regions)?; - for frame in &mut frames { + ) -> SourceResult { + let mut fragment = self.child.layout(world, styles, regions)?; + for frame in &mut fragment { let shape = Geometry::Rect(frame.size()).stroked(self.stroke); frame.prepend(Point::zero(), Element::Shape(shape)); } - Ok(frames) + Ok(fragment) } } diff --git a/library/src/structure/document.rs b/library/src/structure/document.rs index 2e5761e04..e52c92ad2 100644 --- a/library/src/structure/document.rs +++ b/library/src/structure/document.rs @@ -26,7 +26,8 @@ impl LayoutRoot for DocumentNode { let mut pages = vec![]; for (page, map) in self.0.iter() { let number = 1 + pages.len(); - pages.extend(page.layout(world, number, styles.chain(map))?); + let fragment = page.layout(world, number, styles.chain(map))?; + pages.extend(fragment); } Ok(Document { diff --git a/library/src/structure/list.rs b/library/src/structure/list.rs index 6bfddd2e8..b51284a8f 100644 --- a/library/src/structure/list.rs +++ b/library/src/structure/list.rs @@ -18,7 +18,7 @@ pub type EnumNode = ListNode; /// A description list. pub type DescNode = ListNode; -#[node(LayoutBlock)] +#[node(Layout)] impl ListNode { /// How the list is labelled. #[property(referenced)] @@ -75,13 +75,13 @@ impl ListNode { } } -impl LayoutBlock for ListNode { - fn layout_block( +impl Layout for ListNode { + fn layout( &self, world: Tracked, styles: StyleChain, regions: &Regions, - ) -> SourceResult> { + ) -> SourceResult { let mut cells = vec![]; let mut number = 1; @@ -137,7 +137,7 @@ impl LayoutBlock for ListNode { gutter: Axes::with_y(vec![gutter.into()]), cells, } - .layout_block(world, styles, regions) + .layout(world, styles, regions) } } diff --git a/library/src/structure/table.rs b/library/src/structure/table.rs index 4dd14cddd..bb900f3dd 100644 --- a/library/src/structure/table.rs +++ b/library/src/structure/table.rs @@ -12,7 +12,7 @@ pub struct TableNode { pub cells: Vec, } -#[node(LayoutBlock)] +#[node(Layout)] impl TableNode { /// How to fill the cells. #[property(referenced)] @@ -50,13 +50,13 @@ impl TableNode { } } -impl LayoutBlock for TableNode { - fn layout_block( +impl Layout for TableNode { + fn layout( &self, world: Tracked, styles: StyleChain, regions: &Regions, - ) -> SourceResult> { + ) -> SourceResult { let fill = styles.get(Self::FILL); let stroke = styles.get(Self::STROKE).map(PartialStroke::unwrap_or_default); let padding = styles.get(Self::PADDING); @@ -89,7 +89,7 @@ impl LayoutBlock for TableNode { gutter: self.gutter.clone(), cells, } - .layout_block(world, styles, regions) + .layout(world, styles, regions) } } diff --git a/library/src/text/par.rs b/library/src/text/par.rs index 4c22c0348..9dc878739 100644 --- a/library/src/text/par.rs +++ b/library/src/text/par.rs @@ -15,7 +15,7 @@ use crate::prelude::*; #[derive(Hash)] pub struct ParNode(pub StyleVec); -#[node(LayoutBlock)] +#[node(Layout)] impl ParNode { /// The indent the first line of a consecutive paragraph should have. #[property(resolve)] @@ -43,13 +43,13 @@ impl ParNode { } } -impl LayoutBlock for ParNode { - fn layout_block( +impl Layout for ParNode { + fn layout( &self, world: Tracked, styles: StyleChain, regions: &Regions, - ) -> SourceResult> { + ) -> SourceResult { // Collect all text into one string for BiDi analysis. let (text, segments) = collect(self, &styles); @@ -130,24 +130,26 @@ impl Unlabellable for ParbreakNode {} #[derive(Debug, Hash)] pub struct RepeatNode(pub Content); -#[node(LayoutInline)] +#[node(Layout, Inline)] impl RepeatNode { fn construct(_: &Vm, args: &mut Args) -> SourceResult { Ok(Self(args.expect("body")?).pack()) } } -impl LayoutInline for RepeatNode { - fn layout_inline( +impl Layout for RepeatNode { + fn layout( &self, world: Tracked, styles: StyleChain, regions: &Regions, - ) -> SourceResult { - self.0.layout_inline(world, styles, regions) + ) -> SourceResult { + self.0.layout(world, styles, regions) } } +impl Inline for RepeatNode {} + /// Range of a substring of text. type Range = std::ops::Range; @@ -405,7 +407,7 @@ fn collect<'a>( .find_map(|child| { if child.is::() || child.is::() { Some(true) - } else if child.has::() { + } else if child.has::() { Some(false) } else { None @@ -460,7 +462,7 @@ fn collect<'a>( } else if let Some(&node) = child.to::() { full.push(SPACING_REPLACE); Segment::Spacing(node.amount) - } else if child.has::() { + } else if child.has::() { full.push(NODE_REPLACE); Segment::Inline(child) } else { @@ -530,7 +532,7 @@ fn prepare<'a>( } else { let size = Size::new(regions.first.x, regions.base.y); let pod = Regions::one(size, regions.base, Axes::splat(false)); - let mut frame = inline.layout_inline(world, styles, &pod)?; + let mut frame = inline.layout(world, styles, &pod)?.into_frame(); frame.translate(Point::with_y(styles.get(TextNode::BASELINE))); items.push(Item::Frame(frame)); } @@ -1011,7 +1013,7 @@ fn line<'a>( } /// Combine layouted lines into one frame per region. -fn stack(p: &Preparation, lines: &[Line], regions: &Regions) -> SourceResult> { +fn stack(p: &Preparation, lines: &[Line], regions: &Regions) -> SourceResult { // Determine the paragraph's width: Full width of the region if we // should expand or there's fractional spacing, fit-to-width otherwise. let mut width = regions.first.x; @@ -1050,7 +1052,7 @@ fn stack(p: &Preparation, lines: &[Line], regions: &Regions) -> SourceResult, } +/// A partial layout result. +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Fragment(Vec); + +impl Fragment { + /// Create a fragment from a single frame. + pub fn frame(frame: Frame) -> Self { + Self(vec![frame]) + } + + /// Create a fragment from multiple frames. + pub fn frames(frames: Vec) -> Self { + Self(frames) + } + + /// The number of frames in the fragment. + pub fn len(&self) -> usize { + self.0.len() + } + + /// Extract the first and only frame. + /// + /// Panics if there are multiple frames. + #[track_caller] + pub fn into_frame(self) -> Frame { + assert_eq!(self.0.len(), 1, "expected exactly one frame"); + self.0.into_iter().next().unwrap() + } + + /// Iterate over the contained frames. + pub fn iter(&self) -> std::slice::Iter { + self.0.iter() + } + + /// Iterate over the contained frames. + pub fn iter_mut(&mut self) -> std::slice::IterMut { + self.0.iter_mut() + } +} + +impl IntoIterator for Fragment { + type Item = Frame; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +impl<'a> IntoIterator for &'a Fragment { + type Item = &'a Frame; + type IntoIter = std::slice::Iter<'a, Frame>; + + fn into_iter(self) -> Self::IntoIter { + self.0.iter() + } +} + +impl<'a> IntoIterator for &'a mut Fragment { + type Item = &'a mut Frame; + type IntoIter = std::slice::IterMut<'a, Frame>; + + fn into_iter(self) -> Self::IntoIter { + self.0.iter_mut() + } +} + /// A finished layout with elements at fixed positions. #[derive(Default, Clone, Eq, PartialEq)] pub struct Frame { @@ -39,8 +106,6 @@ pub struct Frame { /// The baseline of the frame measured from the top. If this is `None`, the /// frame's implicit baseline is at the bottom. baseline: Option, - /// The semantic role of the frame. - role: Option, /// The elements composing this layout. elements: Arc>, } @@ -53,12 +118,7 @@ impl Frame { #[track_caller] pub fn new(size: Size) -> Self { assert!(size.is_finite()); - Self { - size, - baseline: None, - role: None, - elements: Arc::new(vec![]), - } + Self { size, baseline: None, elements: Arc::new(vec![]) } } /// The size of the frame. @@ -96,11 +156,6 @@ impl Frame { self.baseline = Some(baseline); } - /// The role of the frame. - pub fn role(&self) -> Option { - self.role - } - /// An iterator over the elements inside this frame alongside their /// positions relative to the top-left of the frame. pub fn elements(&self) -> std::slice::Iter<'_, (Point, Element)> { @@ -125,7 +180,7 @@ impl Frame { } } -/// Inserting elements and subframes. +/// Insert elements and subframes. impl Frame { /// The layer the next item will be added on. This corresponds to the number /// of elements in the frame. @@ -141,7 +196,7 @@ impl Frame { /// Add a frame at a position in the foreground. /// /// Automatically decides whether to inline the frame or to include it as a - /// group based on the number of elements in and the role of the frame. + /// group based on the number of elements in it. pub fn push_frame(&mut self, pos: Point, frame: Frame) { if self.should_inline(&frame) { self.inline(self.layer(), pos, frame); @@ -185,8 +240,7 @@ impl Frame { /// Whether the given frame should be inlined. fn should_inline(&self, frame: &Frame) -> bool { - (self.elements.is_empty() || frame.elements.len() <= 5) - && frame.role().map_or(true, |role| role.is_weak()) + self.elements.is_empty() || frame.elements.len() <= 5 } /// Inline a frame at the given layer. @@ -294,10 +348,6 @@ impl Frame { impl Debug for Frame { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - if let Some(role) = self.role { - write!(f, "{role:?} ")?; - } - f.debug_list() .entries(self.elements.iter().map(|(_, element)| element)) .finish() @@ -503,8 +553,8 @@ impl Location { } } -/// A semantic role of a frame. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +/// Standard semantic roles. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum Role { /// A paragraph. Paragraph, @@ -542,13 +592,3 @@ pub enum Role { /// A page foreground. Foreground, } - -impl Role { - /// Whether the role describes a generic element and is not very - /// descriptive. - pub fn is_weak(self) -> bool { - // In Typst, all text is in a paragraph, so paragraph isn't very - // descriptive. - matches!(self, Self::Paragraph | Self::GenericBlock | Self::GenericInline) - } -} diff --git a/src/export/pdf/mod.rs b/src/export/pdf/mod.rs index 7e5a3c064..8f9d96374 100644 --- a/src/export/pdf/mod.rs +++ b/src/export/pdf/mod.rs @@ -12,7 +12,7 @@ use std::hash::Hash; use pdf_writer::types::Direction; use pdf_writer::{Finish, Name, PdfWriter, Ref, TextStr}; -use self::outline::{Heading, HeadingNode}; +use self::outline::HeadingNode; use self::page::Page; use crate::doc::{Document, Lang, Metadata}; use crate::font::Font; diff --git a/src/export/pdf/outline.rs b/src/export/pdf/outline.rs index add167b42..e7a356c13 100644 --- a/src/export/pdf/outline.rs +++ b/src/export/pdf/outline.rs @@ -4,43 +4,34 @@ use super::{AbsExt, PdfContext, RefExt}; use crate::geom::{Abs, Point}; use crate::util::EcoString; -/// A heading that can later be linked in the outline panel. +/// A heading in the outline panel. #[derive(Debug, Clone)] -pub struct Heading { +pub struct HeadingNode { pub content: EcoString, pub level: usize, pub position: Point, pub page: Ref, -} - -/// A node in the outline tree. -#[derive(Debug, Clone)] -pub struct HeadingNode { - pub heading: Heading, pub children: Vec, } impl HeadingNode { - pub fn leaf(heading: Heading) -> Self { - HeadingNode { heading, children: Vec::new() } - } - pub fn len(&self) -> usize { 1 + self.children.iter().map(Self::len).sum::() } - pub fn insert(&mut self, other: Heading, level: usize) -> bool { - if level >= other.level { + #[allow(unused)] + pub fn try_insert(&mut self, child: Self, level: usize) -> bool { + if level >= child.level { return false; } - if let Some(child) = self.children.last_mut() { - if child.insert(other.clone(), level + 1) { + if let Some(last) = self.children.last_mut() { + if last.try_insert(child.clone(), level + 1) { return true; } } - self.children.push(Self::leaf(other)); + self.children.push(child); true } } @@ -74,10 +65,10 @@ pub fn write_outline_item( outline.count(-(node.children.len() as i32)); } - outline.title(TextStr(&node.heading.content)); - outline.dest_direct().page(node.heading.page).xyz( - node.heading.position.x.to_f32(), - (node.heading.position.y + Abs::pt(3.0)).to_f32(), + outline.title(TextStr(&node.content)); + outline.dest_direct().page(node.page).xyz( + node.position.x.to_f32(), + (node.position.y + Abs::pt(3.0)).to_f32(), None, ); diff --git a/src/export/pdf/page.rs b/src/export/pdf/page.rs index 7c4794253..fc714e7ac 100644 --- a/src/export/pdf/page.rs +++ b/src/export/pdf/page.rs @@ -2,10 +2,8 @@ use pdf_writer::types::{ActionType, AnnotationType, ColorSpaceOperand}; use pdf_writer::writers::ColorSpace; use pdf_writer::{Content, Filter, Finish, Name, Rect, Ref, Str}; -use super::{ - deflate, AbsExt, EmExt, Heading, HeadingNode, PdfContext, RefExt, D65_GRAY, SRGB, -}; -use crate::doc::{Destination, Element, Frame, Group, Role, Text}; +use super::{deflate, AbsExt, EmExt, PdfContext, RefExt, D65_GRAY, SRGB}; +use crate::doc::{Destination, Element, Frame, Group, Text}; use crate::font::Font; use crate::geom::{ self, Abs, Color, Em, Geometry, Numeric, Paint, Point, Ratio, Shape, Size, Stroke, @@ -281,23 +279,6 @@ impl PageContext<'_, '_> { /// Encode a frame into the content stream. fn write_frame(ctx: &mut PageContext, frame: &Frame) { - if let Some(Role::Heading { level, outlined: true }) = frame.role() { - let heading = Heading { - position: Point::new(ctx.state.transform.tx, ctx.state.transform.ty), - content: frame.text(), - page: ctx.page_ref, - level: level.get(), - }; - - if let Some(last) = ctx.parent.heading_tree.last_mut() { - if !last.insert(heading.clone(), 1) { - ctx.parent.heading_tree.push(HeadingNode::leaf(heading)) - } - } else { - ctx.parent.heading_tree.push(HeadingNode::leaf(heading)) - } - } - for &(pos, ref element) in frame.elements() { let x = pos.x.to_f32(); let y = pos.y.to_f32(); diff --git a/tests/ref/graphics/line.png b/tests/ref/graphics/line.png index 7858b5c9a..4a73ccd73 100644 Binary files a/tests/ref/graphics/line.png and b/tests/ref/graphics/line.png differ diff --git a/tests/typ/graphics/line.typ b/tests/typ/graphics/line.typ index 97dcb5cf7..2cb2fc9c8 100644 --- a/tests/typ/graphics/line.typ +++ b/tests/typ/graphics/line.typ @@ -22,22 +22,24 @@ #let star(width, ..args) = box(width: width, height: width)[ #set text(spacing: 0%) #set line(..args) - - #align(left)[ - #line(length: +30%, origin: (09.0%, 02%)) - #line(length: +30%, origin: (38.7%, 02%), angle: -72deg) - #line(length: +30%, origin: (57.5%, 02%), angle: 252deg) - #line(length: +30%, origin: (57.3%, 02%)) - #line(length: -30%, origin: (88.0%, 02%), angle: -36deg) - #line(length: +30%, origin: (73.3%, 48%), angle: 252deg) - #line(length: -30%, origin: (73.5%, 48%), angle: 36deg) - #line(length: +30%, origin: (25.4%, 48%), angle: -36deg) - #line(length: +30%, origin: (25.6%, 48%), angle: -72deg) - #line(length: +32%, origin: (8.50%, 02%), angle: 34deg) - ] + #set par(align: left) + #line(length: +30%, origin: (09.0%, 02%)) + #line(length: +30%, origin: (38.7%, 02%), angle: -72deg) + #line(length: +30%, origin: (57.5%, 02%), angle: 252deg) + #line(length: +30%, origin: (57.3%, 02%)) + #line(length: -30%, origin: (88.0%, 02%), angle: -36deg) + #line(length: +30%, origin: (73.3%, 48%), angle: 252deg) + #line(length: -30%, origin: (73.5%, 48%), angle: 36deg) + #line(length: +30%, origin: (25.4%, 48%), angle: -36deg) + #line(length: +30%, origin: (25.6%, 48%), angle: -72deg) + #line(length: +32%, origin: (8.50%, 02%), angle: 34deg) ] -#align(center, grid(columns: (1fr,) * 3, ..((star(20pt, stroke: 0.5pt),) * 9))) +#align(center, grid( + columns: 3, + column-gutter: 10pt, + ..((star(20pt, stroke: 0.5pt),) * 9) +)) --- // Test errors.