Merge pull request #52 from typst/basicc-cols
Introduce equal-width columns
This commit is contained in:
commit
9624ad635b
@ -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();
|
||||
|
145
src/library/columns.rs
Normal file
145
src/library/columns.rs
Normal file
@ -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
|
||||
}
|
||||
}
|
@ -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) => {
|
||||
|
@ -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",
|
||||
|
@ -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);
|
||||
|
BIN
tests/ref/layout/columns.png
Normal file
BIN
tests/ref/layout/columns.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 37 KiB |
104
tests/typ/layout/columns.typ
Normal file
104
tests/typ/layout/columns.typ
Normal file
@ -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)
|
Loading…
x
Reference in New Issue
Block a user