diff --git a/src/eval/node.rs b/src/eval/node.rs index 34a4f275a..43cb906b7 100644 --- a/src/eval/node.rs +++ b/src/eval/node.rs @@ -32,6 +32,8 @@ pub enum Node { Linebreak, /// A paragraph break. Parbreak, + /// A column break. + Colbreak, /// A page break. Pagebreak, /// Plain text. @@ -212,6 +214,14 @@ impl Packer { // paragraph. self.parbreak(Some(styles)); } + Node::Colbreak => { + // Explicit column breaks end the current paragraph and then + // discards the paragraph break. + self.parbreak(None); + self.make_flow_compatible(&styles); + self.flow.children.push(FlowChild::Skip); + self.flow.last.hard(); + } Node::Pagebreak => { // We must set the flow styles after the page break such that an // empty page created by two page breaks in a row has styles at @@ -344,8 +354,8 @@ impl Packer { // Take the flow and erase any styles that will be inherited anyway. let Builder { mut children, styles, .. } = mem::take(&mut self.flow); - for child in &mut children { - child.styles_mut().erase(&styles); + for local in children.iter_mut().filter_map(FlowChild::styles_mut) { + local.erase(&styles); } let flow = FlowNode(children).pack(); diff --git a/src/library/columns.rs b/src/library/columns.rs new file mode 100644 index 000000000..25d6da9d7 --- /dev/null +++ b/src/library/columns.rs @@ -0,0 +1,145 @@ +use super::prelude::*; +use super::ParNode; + +/// `columns`: Stack children along an axis. +pub fn columns(_: &mut EvalContext, args: &mut Args) -> TypResult { + let columns = args.expect("column count")?; + let gutter = args.named("gutter")?.unwrap_or(Relative::new(0.04).into()); + let body: Node = args.expect("body")?; + Ok(Value::block(ColumnsNode { + columns, + gutter, + child: body.into_block(), + })) +} + +/// `colbreak`: Start a new column. +pub fn colbreak(_: &mut EvalContext, _: &mut Args) -> TypResult { + Ok(Value::Node(Node::Colbreak)) +} + +/// A node that separates a region into multiple equally sized columns. +#[derive(Debug, Hash)] +pub struct ColumnsNode { + /// How many columns there should be. + pub columns: NonZeroUsize, + /// The size of the gutter space between each column. + pub gutter: Linear, + /// The child to be layouted into the columns. Most likely, this should be a + /// flow or stack node. + pub child: PackedNode, +} + +impl Layout for ColumnsNode { + fn layout( + &self, + ctx: &mut LayoutContext, + regions: &Regions, + ) -> Vec>> { + // Separating the infinite space into infinite columns does not make + // much sense. Note that this line assumes that no infinitely wide + // region will follow if the first region's width is finite. + if regions.current.x.is_infinite() { + return self.child.layout(ctx, regions); + } + + // Gutter width for each region. (Can be different because the relative + // component is calculated seperately for each region.) + let mut gutters = vec![]; + + // Sizes of all columns resulting from `region.current`, + // `region.backlog` and `regions.last`. + let mut sizes = vec![]; + + let columns = self.columns.get(); + + for (current, base) in regions + .iter() + .take(1 + regions.backlog.len() + if regions.last.is_some() { 1 } else { 0 }) + { + let gutter = self.gutter.resolve(base.x); + gutters.push(gutter); + let size = Size::new( + (current.x - gutter * (columns - 1) as f64) / columns as f64, + current.y, + ); + for _ in 0 .. columns { + sizes.push(size); + } + } + + let first = sizes.remove(0); + let mut pod = Regions::one( + first, + Size::new(first.x, regions.base.y), + Spec::new(true, regions.expand.y), + ); + + // Retrieve elements for the last region from the vectors. + let last_gutter = if regions.last.is_some() { + let gutter = gutters.pop().unwrap(); + let size = sizes.drain(sizes.len() - columns ..).next().unwrap(); + pod.last = Some(size); + Some(gutter) + } else { + None + }; + + pod.backlog = sizes.into_iter(); + + let mut frames = self.child.layout(ctx, &pod).into_iter(); + + let dir = ctx.styles.get(ParNode::DIR); + + let mut finished = vec![]; + let total_regions = (frames.len() as f32 / columns as f32).ceil() as usize; + + for ((current, base), gutter) in regions + .iter() + .take(total_regions) + .zip(gutters.into_iter().chain(last_gutter.into_iter().cycle())) + { + // The height should be the parent height if the node shall expand. + // Otherwise its the maximum column height for the frame. In that + // case, the frame is first created with zero height and then + // resized. + let mut height = if regions.expand.y { current.y } else { Length::zero() }; + let mut frame = Frame::new(Spec::new(regions.current.x, height)); + + let mut cursor = Length::zero(); + + for _ in 0 .. columns { + let child_frame = match frames.next() { + Some(frame) => frame.item, + None => break, + }; + + let width = child_frame.size.x; + + if !regions.expand.y { + height.set_max(child_frame.size.y); + } + + frame.push_frame( + Point::with_x(if dir.is_positive() { + cursor + } else { + regions.current.x - cursor - width + }), + child_frame, + ); + + cursor += width + gutter; + } + + frame.size.y = height; + + let mut cts = Constraints::new(regions.expand); + cts.base = base.map(Some); + cts.exact = current.map(Some); + finished.push(frame.constrain(cts)); + } + + finished + } +} diff --git a/src/library/flow.rs b/src/library/flow.rs index cfa761b6d..6bfa3ddd9 100644 --- a/src/library/flow.rs +++ b/src/library/flow.rs @@ -36,24 +36,28 @@ pub enum FlowChild { Spacing(SpacingNode), /// An arbitrary node. Node(PackedNode), + /// Skip the rest of the region and move to the next. + Skip, } impl FlowChild { /// A reference to the child's styles. - pub fn styles(&self) -> &Styles { + pub fn styles(&self) -> Option<&Styles> { match self { - Self::Break(styles) => styles, - Self::Spacing(node) => &node.styles, - Self::Node(node) => &node.styles, + Self::Break(styles) => Some(styles), + Self::Spacing(node) => Some(&node.styles), + Self::Node(node) => Some(&node.styles), + Self::Skip => None, } } /// A mutable reference to the child's styles. - pub fn styles_mut(&mut self) -> &mut Styles { + pub fn styles_mut(&mut self) -> Option<&mut Styles> { match self { - Self::Break(styles) => styles, - Self::Spacing(node) => &mut node.styles, - Self::Node(node) => &mut node.styles, + Self::Break(styles) => Some(styles), + Self::Spacing(node) => Some(&mut node.styles), + Self::Node(node) => Some(&mut node.styles), + Self::Skip => None, } } } @@ -69,6 +73,7 @@ impl Debug for FlowChild { } Self::Spacing(node) => node.fmt(f), Self::Node(node) => node.fmt(f), + Self::Skip => f.pad("Skip"), } } } @@ -138,6 +143,9 @@ impl<'a> FlowLayouter<'a> { let amount = chain.get(ParNode::SPACING).resolve(em); self.layout_absolute(amount.into()); } + FlowChild::Skip => { + self.finish_region(); + } FlowChild::Spacing(node) => match node.kind { SpacingKind::Linear(v) => self.layout_absolute(v), SpacingKind::Fractional(v) => { diff --git a/src/library/mod.rs b/src/library/mod.rs index b2dd0dbe5..1c97f5297 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -4,6 +4,7 @@ //! definitions. mod align; +mod columns; mod flow; mod grid; mod heading; @@ -25,6 +26,7 @@ mod utility; /// Helpful imports for creating library functionality. mod prelude { pub use std::fmt::{self, Debug, Formatter}; + pub use std::num::NonZeroUsize; pub use std::rc::Rc; pub use typst_macros::properties; @@ -42,6 +44,7 @@ mod prelude { pub use self::image::*; pub use align::*; +pub use columns::*; pub use flow::*; pub use grid::*; pub use heading::*; @@ -83,6 +86,7 @@ pub fn new() -> Scope { // Break and spacing functions. std.def_func("pagebreak", pagebreak); + std.def_func("colbreak", colbreak); std.def_func("parbreak", parbreak); std.def_func("linebreak", linebreak); std.def_func("h", h); @@ -96,6 +100,7 @@ pub fn new() -> Scope { std.def_func("stack", stack); std.def_func("grid", grid); std.def_func("pad", pad); + std.def_func("columns", columns); std.def_func("align", align); std.def_func("place", place); std.def_func("move", move_); @@ -167,6 +172,15 @@ castable! { Value::Int(int) => int.try_into().map_err(|_| "must be at least zero")?, } +castable! { + prelude::NonZeroUsize, + Expected: "positive integer", + Value::Int(int) => int + .try_into() + .and_then(|n: usize| n.try_into()) + .map_err(|_| "must be positive")?, +} + castable! { String, Expected: "string", diff --git a/src/library/page.rs b/src/library/page.rs index 0e6907707..100b4d0c5 100644 --- a/src/library/page.rs +++ b/src/library/page.rs @@ -4,7 +4,7 @@ use std::fmt::{self, Display, Formatter}; use std::str::FromStr; use super::prelude::*; -use super::PadNode; +use super::{ColumnsNode, PadNode}; /// `pagebreak`: Start a new page. pub fn pagebreak(_: &mut EvalContext, _: &mut Args) -> TypResult { @@ -40,6 +40,10 @@ impl PageNode { pub const BOTTOM: Smart = Smart::Auto; /// The page's background color. pub const FILL: Option = None; + /// How many columns the page has. + pub const COLUMNS: NonZeroUsize = NonZeroUsize::new(1).unwrap(); + /// How many columns the page has. + pub const COLUMN_GUTTER: Linear = Relative::new(0.04).into(); } impl Construct for PageNode { @@ -76,6 +80,8 @@ impl Set for PageNode { styles.set_opt(Self::RIGHT, args.named("right")?.or(margins)); styles.set_opt(Self::BOTTOM, args.named("bottom")?.or(margins)); styles.set_opt(Self::FILL, args.named("fill")?); + styles.set_opt(Self::COLUMNS, args.named("columns")?); + styles.set_opt(Self::COLUMN_GUTTER, args.named("column-gutter")?); Ok(()) } @@ -112,8 +118,20 @@ impl PageNode { bottom: ctx.styles.get(Self::BOTTOM).unwrap_or(default.bottom), }; + let columns = ctx.styles.get(Self::COLUMNS); + let child = if columns.get() > 1 { + ColumnsNode { + columns, + gutter: ctx.styles.get(Self::COLUMN_GUTTER), + child: self.child.clone(), + } + .pack() + } else { + self.child.clone() + }; + // Pad the child. - let padded = PadNode { child: self.child.clone(), padding }.pack(); + let padded = PadNode { child, padding }.pack(); // Layout the child. let expand = size.map(Length::is_finite); diff --git a/tests/ref/layout/columns.png b/tests/ref/layout/columns.png new file mode 100644 index 000000000..34eb19076 Binary files /dev/null and b/tests/ref/layout/columns.png differ diff --git a/tests/typ/layout/columns.typ b/tests/typ/layout/columns.typ new file mode 100644 index 000000000..bf954d930 --- /dev/null +++ b/tests/typ/layout/columns.typ @@ -0,0 +1,104 @@ +// Test the column layouter. + +--- +// Test normal operation and RTL directions. +#set page(height: 3.25cm, width: 7.05cm, columns: 2, column-gutter: 30pt) +#set text("Noto Sans Arabic", serif) +#set par(lang: "ar") + +#rect(fill: conifer, height: 8pt, width: 6pt) وتحفيز +العديد من التفاعلات الكيميائية. (DNA) من أهم الأحماض النووية التي تُشكِّل +إلى جانب كل من البروتينات والليبيدات والسكريات المتعددة +#rect(fill: eastern, height: 8pt, width: 6pt) +الجزيئات الضخمة الأربعة الضرورية للحياة. + +--- +// Test the `columns` function. +#set page(width: auto) + +#rect(width: 180pt, height: 100pt, padding: 8pt, columns(2, [ + A special plight has befallen our document. + Columns in text boxes reigned down unto the soil + to waste a year's crop of rich layouts. + The columns at least were graciously balanced. +])) + +--- +// Test columns for a sized page. +#set page(height: 5cm, width: 7.05cm, columns: 2) + +Lorem ipsum dolor sit amet is a common blind text +and I again am in need of filling up this page +#align(bottom, rect(fill: eastern, width: 100%, height: 12pt)) +#colbreak() + +so I'm returning to this trusty tool of tangible terror. +Sure, it is not the most creative way of filling up +a page for a test but it does get the job done. + +--- +// Test the expansion behavior. +#set page(height: 2.5cm, width: 7.05cm) + +#rect(padding: 6pt, columns(2, [ + ABC \ + BCD + #colbreak() + DEF +])) + +--- +// Test setting a column gutter and more than two columns. +#set page(height: 3.25cm, width: 7.05cm, columns: 3, column-gutter: 30pt) + +#rect(width: 100%, height: 2.5cm, fill: conifer) +#rect(width: 100%, height: 2cm, fill: eastern) +#circle(fill: eastern) + +--- +// Test the `colbreak` and `pagebreak` functions. +#set page(height: 1cm, width: 7.05cm, columns: 2) + +A +#colbreak() +#colbreak() +B +#pagebreak() +C +#colbreak() +D + +--- +// Test an empty second column. +#set page(width: 7.05cm, columns: 2) + +#rect(width: 100%, padding: 3pt)[So there isn't anything in the second column?] + +--- +// Test columns when one of them is empty. +#set page(width: auto, columns: 3) + +Arbitrary horizontal growth. + +--- +// Test columns in an infinitely high frame. +#set page(width: 7.05cm, columns: 2) + +There can be as much content as you want in the left column +and the document will grow with it. + +#rect(fill: conifer, width: 100%, height: 30pt) + +Only an explicit #colbreak() `#colbreak()` can put content in the +second column. + +--- +// Test a page with a single column. +#set page(height: auto, width: 7.05cm, columns: 1) + +This is a normal page. Very normal. + +--- +// Test a page with zero columns. +// Error: 49-50 must be positive +#set page(height: auto, width: 7.05cm, columns: 0)