summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2021-12-28 00:29:15 +0100
committerGitHub <noreply@github.com>2021-12-28 00:29:15 +0100
commit9624ad635bd8adb0e421c37c63c7310ecc71a708 (patch)
tree0062b38db54c1fd785eedecbd9b77a12800bc340
parentf6c7a8292dc1ab0560408fca9d74505e9d7cf13a (diff)
parent7f7e14d95f7240727e0163451190ff63b230e393 (diff)
Merge pull request #52 from typst/basicc-cols
Introduce equal-width columns
-rw-r--r--src/eval/node.rs14
-rw-r--r--src/library/columns.rs145
-rw-r--r--src/library/flow.rs24
-rw-r--r--src/library/mod.rs14
-rw-r--r--src/library/page.rs22
-rw-r--r--tests/ref/layout/columns.pngbin0 -> 37999 bytes
-rw-r--r--tests/typ/layout/columns.typ104
7 files changed, 311 insertions, 12 deletions
diff --git a/src/eval/node.rs b/src/eval/node.rs
index 34a4f275..43cb906b 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 00000000..25d6da9d
--- /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<Value> {
+ 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<Value> {
+ 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<Constrained<Rc<Frame>>> {
+ // 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 cfa761b6..6bfa3ddd 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 b2dd0dbe..1c97f529 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_);
@@ -168,6 +173,15 @@ castable! {
}
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",
Value::Str(string) => string.into(),
diff --git a/src/library/page.rs b/src/library/page.rs
index 0e690770..100b4d0c 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<Value> {
@@ -40,6 +40,10 @@ impl PageNode {
pub const BOTTOM: Smart<Linear> = Smart::Auto;
/// The page's background color.
pub const FILL: Option<Paint> = 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 00000000..34eb1907
--- /dev/null
+++ b/tests/ref/layout/columns.png
Binary files differ
diff --git a/tests/typ/layout/columns.typ b/tests/typ/layout/columns.typ
new file mode 100644
index 00000000..bf954d93
--- /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)