Introduce equal-width columns

This commit is contained in:
Martin Haug 2021-12-18 18:04:26 +01:00
parent f6c7a8292d
commit b22ce6f8b8
7 changed files with 305 additions and 11 deletions

View File

@ -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
@ -345,7 +355,7 @@ 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);
child.styles_mut().map(|s| s.erase(&styles));
}
let flow = FlowNode(children).pack();

157
src/library/columns.rs Normal file
View File

@ -0,0 +1,157 @@
use super::prelude::*;
use super::ParNode;
/// `columns`: Stack children along an axis.
pub fn columns(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
let count = 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: count,
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: usize,
/// 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.
if regions.current.x.is_infinite() {
return self.child.layout(ctx, regions);
}
// All gutters in the document. (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` and
// `region.backlog`.
let mut sizes = vec![];
// Assure there is at least one column.
let columns = self.columns.max(1);
for (current, base) in std::iter::once((regions.current, regions.base))
.chain(regions.backlog.clone().into_iter().map(|s| (s, s)))
{
let gutter = self.gutter.resolve(base.x);
gutters.push(gutter);
let size = Spec::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 col_regions = Regions::one(first, first, regions.expand);
col_regions.backlog = sizes.clone().into_iter();
// We have to treat the last region separately.
let last_column_gutter = regions.last.map(|last| {
let gutter = self.gutter.resolve(last.x);
let size = Spec::new(
(last.x - gutter * (columns - 1) as f64) / columns as f64,
last.y,
);
col_regions.last = Some(size);
(size, gutter)
});
let frames = self.child.layout(ctx, &col_regions);
let dir = ctx.styles.get(ParNode::DIR);
// Dealing with infinite height areas here.
let height = if regions.current.y.is_infinite() {
frames
.iter()
.map(|frame| frame.item.size.y)
.max()
.unwrap_or(Length::zero())
} else {
regions.current.y
};
let mut regions = regions.clone();
let to = |cursor: Length, width: Length, regions: &Regions| {
if dir.is_positive() {
cursor
} else {
regions.current.x - cursor - width
}
};
let mut cursor = Length::zero();
let mut res = vec![];
let mut frame = Frame::new(Spec::new(regions.current.x, height));
for (i, child_frame) in frames.into_iter().enumerate() {
let region = i / columns;
let size = std::iter::once(&first)
.chain(sizes.iter())
.nth(i)
.copied()
.unwrap_or_else(|| last_column_gutter.unwrap().0);
frame.push_frame(
Point::new(to(cursor, size.x, &regions), Length::zero()),
child_frame.item,
);
cursor += size.x;
if i % columns == columns - 1 {
// Refresh column height for non-infinite regions here.
let height = if regions.current.y.is_infinite() {
height
} else {
regions.current.y
};
regions.next();
let old_frame = std::mem::replace(
&mut frame,
Frame::new(Spec::new(regions.current.x, height)),
);
res.push(old_frame.constrain(Constraints::tight(&regions)));
cursor = Length::zero();
} else {
cursor += gutters
.get(region)
.copied()
.unwrap_or_else(|| last_column_gutter.unwrap().1);
}
}
if !frame.elements.is_empty() {
res.push(frame.constrain(Constraints::tight(&regions)));
}
res
}
}

View File

@ -32,6 +32,8 @@ impl Debug for FlowNode {
pub enum FlowChild {
/// A paragraph/block break.
Break(Styles),
/// Skip the rest of the region and move to the next.
Skip,
/// Vertical spacing between other children.
Spacing(SpacingNode),
/// An arbitrary node.
@ -40,20 +42,22 @@ pub enum FlowChild {
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 => write!(f, "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) => {

View File

@ -4,6 +4,7 @@
//! definitions.
mod align;
mod columns;
mod flow;
mod grid;
mod heading;
@ -42,6 +43,7 @@ mod prelude {
pub use self::image::*;
pub use align::*;
pub use columns::*;
pub use flow::*;
pub use grid::*;
pub use heading::*;
@ -83,6 +85,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 +99,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_);

View File

@ -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: usize = 1;
/// 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 ctx.styles.get(Self::COLUMNS) > 1 {
ColumnsNode {
child: self.child.clone(),
columns,
gutter: ctx.styles.get(Self::COLUMN_GUTTER),
}
.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);

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

View File

@ -0,0 +1,97 @@
// Test the column layouter.
---
// Test columns for a sized page.
#set page(height: 4.3cm, 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 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 `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 more than two columns.
#set page(height: 2cm, width: 7.05cm, columns: 3)
#set par(align: center)
#circle(fill: eastern)
#circle(fill: conifer)
#circle(fill: eastern)
---
// Test setting a column gutter.
#set page(height: 3.25cm, width: 7.05cm, columns: 2, column-gutter: 30pt)
#rect(width: 100%, height: 2.5cm, fill: conifer)
#rect(width: 100%, height: 2cm, fill: eastern)
---
// Test RTL columns.
#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 `colbreak` function.
#set page(height: 1cm, width: 7.05cm, columns: 2)
A
#colbreak()
#colbreak()
B
#colbreak()
C
---
// 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)
The page can grow as much as it wants horizontally.
---
// Test columns in an infinitely wide 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.
#set page(height: auto, width: 7.05cm, columns: 0)
This makes less sense but will still
produce a normal page.