Unbounded pages 🌌

This commit is contained in:
Laurenz 2021-01-13 23:19:44 +01:00
parent 1b53e27f27
commit 272a4c2289
31 changed files with 155 additions and 103 deletions

View File

@ -95,6 +95,7 @@ impl EvalContext {
pub fn start_page_group(&mut self, softness: Softness) {
self.start_group(PageGroup {
size: self.state.page.size,
expand: self.state.page.expand,
padding: self.state.page.margins(),
dirs: self.state.dirs,
align: self.state.align,
@ -124,7 +125,7 @@ impl EvalContext {
child: NodeStack {
dirs: group.dirs,
align: group.align,
expansion: Gen::uniform(Expansion::Fill),
expand: group.expand,
children,
}
.into(),
@ -281,6 +282,7 @@ pub enum Softness {
#[derive(Debug)]
struct PageGroup {
size: Size,
expand: Spec<Expansion>,
padding: Sides<Linear>,
dirs: LayoutDirs,
align: ChildAlign,

View File

@ -19,7 +19,7 @@ use std::rc::Rc;
use crate::color::Color;
use crate::diag::Pass;
use crate::env::SharedEnv;
use crate::geom::{Angle, Gen, Length, Relative};
use crate::geom::{Angle, Length, Relative, Spec};
use crate::layout::{self, Expansion, NodeSpacing, NodeStack};
use crate::syntax::*;
@ -137,7 +137,7 @@ impl Eval for Spanned<&NodeRaw> {
ctx.push(NodeStack {
dirs: ctx.state.dirs,
align: ctx.state.align,
expansion: Gen::uniform(Expansion::Fit),
expand: Spec::uniform(Expansion::Fit),
children,
});

View File

@ -4,8 +4,9 @@ use fontdock::{fallback, FallbackTree, FontStretch, FontStyle, FontVariant, Font
use super::Scope;
use crate::geom::{
Align, ChildAlign, Dir, LayoutDirs, Length, Linear, Relative, Sides, Size,
Align, ChildAlign, Dir, LayoutDirs, Length, Linear, Relative, Sides, Size, Spec,
};
use crate::layout::Expansion;
use crate::paper::{Paper, PaperClass, PAPER_A4};
/// The evaluation state.
@ -45,6 +46,8 @@ pub struct StatePage {
pub class: PaperClass,
/// The width and height of the page.
pub size: Size,
/// Whether the expand the pages to the `size` or to fit the content.
pub expand: Spec<Expansion>,
/// The amount of white space in the order [left, top, right, bottom]. If a
/// side is set to `None`, the default for the paper class is used.
pub margins: Sides<Option<Linear>>,
@ -56,6 +59,7 @@ impl StatePage {
Self {
class: paper.class,
size: paper.size(),
expand: Spec::uniform(Expansion::Fill),
margins: Sides::uniform(None),
}
}

View File

@ -81,6 +81,16 @@ impl Length {
Self { raw: self.raw.max(other.raw) }
}
/// Whether the length is finite.
pub fn is_finite(self) -> bool {
self.raw.is_finite()
}
/// Whether the length is infinite.
pub fn is_infinite(self) -> bool {
self.raw.is_infinite()
}
/// Whether the length is `NaN`.
pub fn is_nan(self) -> bool {
self.raw.is_nan()

View File

@ -26,7 +26,12 @@ impl Relative {
/// Resolve this relative to the given `length`.
pub fn resolve(self, length: Length) -> Length {
self.get() * length
// Zero wins over infinity.
if self.0 == 0.0 {
Length::ZERO
} else {
self.get() * length
}
}
}

View File

@ -31,6 +31,16 @@ impl Size {
self.width >= other.width && self.height >= other.height
}
/// Whether both components are finite.
pub fn is_finite(self) -> bool {
self.width.is_finite() && self.height.is_finite()
}
/// Whether any of the two components is infinite.
pub fn is_infinite(self) -> bool {
self.width.is_infinite() || self.height.is_infinite()
}
/// Whether any of the two components is `NaN`.
pub fn is_nan(self) -> bool {
self.width.is_nan() || self.height.is_nan()

View File

@ -14,10 +14,10 @@ pub struct NodeFixed {
impl Layout for NodeFixed {
fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Layouted {
let Area { rem, full } = areas.current;
let Areas { current, full, .. } = areas;
let size = Size::new(
self.width.map(|w| w.resolve(full.width)).unwrap_or(rem.width),
self.height.map(|h| h.resolve(full.height)).unwrap_or(rem.height),
self.width.map(|w| w.resolve(full.width)).unwrap_or(current.width),
self.height.map(|h| h.resolve(full.height)).unwrap_or(current.height),
);
let areas = Areas::once(size);

View File

@ -71,27 +71,13 @@ pub struct LayoutContext {
pub env: SharedEnv,
}
/// An area into which content can be laid out.
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct Area {
/// The remaining size of this area.
pub rem: Size,
/// The full size this area once had (used for relative sizing).
pub full: Size,
}
impl Area {
/// Create a new area.
pub fn new(size: Size) -> Self {
Self { rem: size, full: size }
}
}
/// A collection of areas to layout into.
#[derive(Debug, Clone, PartialEq)]
pub struct Areas {
/// The current area.
pub current: Area,
/// The remaining size of the current area.
pub current: Size,
/// The full size the current area once had (used for relative sizing).
pub full: Size,
/// A stack of followup areas (the next area is the last element).
pub backlog: Vec<Size>,
/// The final area that is repeated when the backlog is empty.
@ -102,7 +88,8 @@ impl Areas {
/// Create a new length-1 sequence of areas with just one `area`.
pub fn once(size: Size) -> Self {
Self {
current: Area::new(size),
current: size,
full: size,
backlog: vec![],
last: None,
}
@ -111,7 +98,8 @@ impl Areas {
/// Create a new sequence of areas that repeats `area` indefinitely.
pub fn repeat(size: Size) -> Self {
Self {
current: Area::new(size),
current: size,
full: size,
backlog: vec![],
last: Some(size),
}
@ -120,7 +108,8 @@ impl Areas {
/// Advance to the next area if there is any.
pub fn next(&mut self) {
if let Some(size) = self.backlog.pop().or(self.last) {
self.current = Area::new(size);
self.current = size;
self.full = size;
}
}
@ -130,11 +119,32 @@ impl Areas {
pub fn in_full_last(&self) -> bool {
self.backlog.is_empty()
&& self.last.map_or(true, |size| {
self.current.rem.is_nan() || size.is_nan() || self.current.rem == size
self.current.is_nan() || size.is_nan() || self.current == size
})
}
}
/// Whether to expand or shrink a node along an axis.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum Expansion {
/// Fit the content.
Fit,
/// Fill the available space.
Fill,
}
impl Expansion {
/// Resolve the expansion to either the `fit` or `fill` length.
///
/// Prefers `fit` if `fill` is infinite.
pub fn resolve(self, fit: Length, fill: Length) -> Length {
match self {
Self::Fill if fill.is_finite() => fill,
_ => fit,
}
}
}
/// The result of layouting a node.
#[derive(Debug, Clone, PartialEq)]
pub enum Layouted {
@ -158,15 +168,6 @@ impl Layouted {
}
}
/// Whether to expand or shrink a node along an axis.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum Expansion {
/// Fit the content.
Fit,
/// Fill the available space.
Fill,
}
/// A finished layout with elements at fixed positions.
#[derive(Debug, Clone, PartialEq)]
pub struct Frame {

View File

@ -39,10 +39,8 @@ impl From<NodePad> for NodeAny {
fn shrink(areas: &Areas, padding: Sides<Linear>) -> Areas {
let shrink = |size| size - padding.resolve(size).size();
Areas {
current: Area {
rem: shrink(areas.current.rem),
full: shrink(areas.current.full),
},
current: shrink(areas.current),
full: shrink(areas.full),
backlog: areas.backlog.iter().copied().map(shrink).collect(),
last: areas.last.map(shrink),
}

View File

@ -74,7 +74,7 @@ impl<'a> ParLayouter<'a> {
}
fn push_spacing(&mut self, amount: Length) {
let cross_max = self.areas.current.rem.get(self.cross);
let cross_max = self.areas.current.get(self.cross);
self.run_size.cross = (self.run_size.cross + amount).min(cross_max);
}
@ -84,7 +84,7 @@ impl<'a> ParLayouter<'a> {
}
let fits = {
let mut usable = self.areas.current.rem;
let mut usable = self.areas.current;
*usable.get_mut(self.cross) -= self.run_size.cross;
usable.fits(frame.size)
};
@ -92,7 +92,7 @@ impl<'a> ParLayouter<'a> {
if !fits {
self.finish_run();
while !self.areas.current.rem.fits(frame.size) {
while !self.areas.current.fits(frame.size) {
if self.areas.in_full_last() {
// TODO: Diagnose once the necessary spans exist.
let _ = warning!("cannot fit frame into any area");
@ -112,10 +112,15 @@ impl<'a> ParLayouter<'a> {
}
fn finish_run(&mut self) {
let full_size = Gen::new(self.run_size.main, match self.par.cross_expansion {
Expansion::Fill => self.areas.current.full.get(self.cross),
Expansion::Fit => self.run_size.cross,
});
let full_size = {
let full = self.areas.full.switch(self.dirs);
Gen::new(
self.run_size.main,
self.par
.cross_expansion
.resolve(self.run_size.cross.min(full.cross), full.cross),
)
};
let mut output = Frame::new(full_size.switch(self.dirs).to_size());
@ -139,7 +144,7 @@ impl<'a> ParLayouter<'a> {
self.lines.push((self.lines_size.main, output, self.run_ruler));
let main_offset = full_size.main + self.par.line_spacing;
*self.areas.current.rem.get_mut(self.main) -= main_offset;
*self.areas.current.get_mut(self.main) -= main_offset;
self.lines_size.main += main_offset;
self.lines_size.cross = self.lines_size.cross.max(full_size.cross);

View File

@ -11,7 +11,7 @@ pub struct NodeStack {
/// How to align this stack in _its_ parent.
pub align: ChildAlign,
/// Whether to expand the axes to fill the area or to fit the content.
pub expansion: Gen<Expansion>,
pub expand: Spec<Expansion>,
/// The nodes to be stacked.
pub children: Vec<Node>,
}
@ -66,7 +66,7 @@ impl<'a> StackLayouter<'a> {
}
fn push_spacing(&mut self, amount: Length) {
let main_rest = self.areas.current.rem.get_mut(self.main);
let main_rest = self.areas.current.get_mut(self.main);
let capped = amount.min(*main_rest);
*main_rest -= capped;
self.used.main += capped;
@ -77,7 +77,7 @@ impl<'a> StackLayouter<'a> {
self.finish_area();
}
while !self.areas.current.rem.fits(frame.size) {
while !self.areas.current.fits(frame.size) {
if self.areas.in_full_last() {
// TODO: Diagnose once the necessary spans exist.
let _ = warning!("cannot fit frame into any area");
@ -90,7 +90,7 @@ impl<'a> StackLayouter<'a> {
let size = frame.size.switch(self.dirs);
self.frames.push((self.used.main, frame, align));
*self.areas.current.rem.get_mut(self.main) -= size.main;
*self.areas.current.get_mut(self.main) -= size.main;
self.used.main += size.main;
self.used.cross = self.used.cross.max(size.cross);
self.ruler = align.main;
@ -98,16 +98,11 @@ impl<'a> StackLayouter<'a> {
fn finish_area(&mut self) {
let full_size = {
let full = self.areas.current.full.switch(self.dirs);
let expand = self.stack.expand.switch(self.dirs);
let full = self.areas.full.switch(self.dirs);
Gen::new(
match self.stack.expansion.main {
Expansion::Fill => full.main,
Expansion::Fit => self.used.main.min(full.main),
},
match self.stack.expansion.cross {
Expansion::Fill => full.cross,
Expansion::Fit => self.used.cross.min(full.cross),
},
expand.main.resolve(self.used.main.min(full.main), full.main),
expand.cross.resolve(self.used.cross.min(full.cross), full.cross),
)
};

View File

@ -55,8 +55,11 @@ struct NodeImage {
impl Layout for NodeImage {
fn layout(&self, _: &mut LayoutContext, areas: &Areas) -> Layouted {
let Area { rem, full } = areas.current;
let pixel_ratio = (self.dimensions.0 as f64) / (self.dimensions.1 as f64);
let Areas { current, full, .. } = areas;
let pixel_width = self.dimensions.0 as f64;
let pixel_height = self.dimensions.1 as f64;
let pixel_ratio = pixel_width / pixel_height;
let width = self.width.map(|w| w.resolve(full.width));
let height = self.height.map(|w| w.resolve(full.height));
@ -66,12 +69,15 @@ impl Layout for NodeImage {
(Some(width), None) => Size::new(width, width / pixel_ratio),
(None, Some(height)) => Size::new(height * pixel_ratio, height),
(None, None) => {
let ratio = rem.width / rem.height;
if ratio < pixel_ratio {
Size::new(rem.width, rem.width / pixel_ratio)
} else {
let ratio = current.width / current.height;
if ratio < pixel_ratio && current.width.is_finite() {
Size::new(current.width, current.width / pixel_ratio)
} else if current.height.is_finite() {
// TODO: Fix issue with line spacing.
Size::new(rem.height * pixel_ratio, rem.height)
Size::new(current.height * pixel_ratio, current.height)
} else {
// Totally unbounded area, we have to make up something.
Size::new(Length::pt(pixel_width), Length::pt(pixel_height))
}
}
};

View File

@ -197,13 +197,12 @@ pub fn box_(ctx: &mut EvalContext, args: &mut Args) -> Value {
let children = ctx.end_content_group();
let fill_if = |c| if c { Expansion::Fill } else { Expansion::Fit };
let expansion =
Spec::new(fill_if(width.is_some()), fill_if(height.is_some())).switch(dirs);
let expand = Spec::new(fill_if(width.is_some()), fill_if(height.is_some()));
ctx.push(NodeFixed {
width,
height,
child: NodeStack { dirs, align, expansion, children }.into(),
child: NodeStack { dirs, align, expand, children }.into(),
});
ctx.state = snapshot;
@ -271,6 +270,7 @@ pub fn page(ctx: &mut EvalContext, args: &mut Args) -> Value {
if let Some(paper) = Paper::from_name(&name.v) {
ctx.state.page.class = paper.class;
ctx.state.page.size = paper.size();
ctx.state.page.expand = Spec::uniform(Expansion::Fill);
} else {
ctx.diag(error!(name.span, "invalid paper name"));
}
@ -279,11 +279,13 @@ pub fn page(ctx: &mut EvalContext, args: &mut Args) -> Value {
if let Some(width) = args.get(ctx, "width") {
ctx.state.page.class = PaperClass::Custom;
ctx.state.page.size.width = width;
ctx.state.page.expand.horizontal = Expansion::Fill;
}
if let Some(height) = args.get(ctx, "height") {
ctx.state.page.class = PaperClass::Custom;
ctx.state.page.size.height = height;
ctx.state.page.expand.vertical = Expansion::Fill;
}
if let Some(margins) = args.get(ctx, "margins") {
@ -307,8 +309,9 @@ pub fn page(ctx: &mut EvalContext, args: &mut Args) -> Value {
}
if args.get(ctx, "flip").unwrap_or(false) {
let size = &mut ctx.state.page.size;
std::mem::swap(&mut size.width, &mut size.height);
let page = &mut ctx.state.page;
std::mem::swap(&mut page.size.width, &mut page.size.height);
std::mem::swap(&mut page.expand.horizontal, &mut page.expand.vertical);
}
let main = args.get(ctx, "main-dir");

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 B

After

Width:  |  Height:  |  Size: 94 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 KiB

After

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 821 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

View File

@ -12,13 +12,13 @@ Add [h 10pt] [h 10pt] up
// Relative to font size.
Relative [h 100%] spacing
// Missing spacing.
// Error: 1:11-1:11 missing argument: spacing
Totally [h] ignored
// Swapped axes.
[page main-dir: rtl, cross-dir: ttb][
[page main-dir: rtl, cross-dir: ttb, height: 80pt][
1 [h 1cm] 2
3 [v 1cm] 4 [v -1cm] 5
]
// Missing spacing.
// Error: 1:11-1:11 missing argument: spacing
Totally [h] ignored

View File

@ -14,7 +14,7 @@
[image "res/rhino.png"]
// Fit to height of page.
[page width: 270pt][
[page height: 40pt][
[image "res/rhino.png"]
]
@ -29,7 +29,7 @@
// Make sure the bounding-box of the image is correct.
[align bottom, right][
[image "res/tiger.jpg"]
[image "res/tiger.jpg", width: 60pt]
]
---

View File

@ -1,28 +1,36 @@
// Test configuring page sizes and margins.
// Set width.
[page width: 50pt][High]
// Set height.
[page height: 50pt][Wide]
// Set width and height.
[page width: 120pt, height: 120pt]
[page width: 40pt][High]
[page height: 40pt][Wide]
// Set all margins at once.
[page margins: 40pt][
[page margins: 30pt][
[align top, left][TL]
[align bottom, right][BR]
]
// Set individual margins.
[page height: 40pt]
[page left: 0pt | align left][Left]
[page right: 0pt | align right][Right]
[page top: 0pt | align top][Top]
[page bottom: 0pt | align bottom][Bottom]
// Ensure that specific margins override general margins.
[page margins: 0pt, left: 40pt][Overriden]
[page margins: 0pt, left: 20pt][Overriden]
// Flip the page.
[page "a10", flip: true][Flipped]
---
// Test flipping.
// Flipped predefined paper.
[page "a11", flip: true][Flipped A11]
// Flipped custom page size.
[page width: 40pt, height: 120pt]
[page flip: true]
Wide
---
// Test a combination of pages with bodies and normal content.
@ -40,7 +48,7 @@ Sixth
---
// Test changing the layouting directions of pages.
[page main-dir: btt, cross-dir: rtl]
[page height: 50pt, main-dir: btt, cross-dir: rtl]
Right to left!

View File

@ -8,15 +8,12 @@
####### Seven
---
// Is a heading.
// Heading vs. no heading.
/**/ # Heading
{[## Heading]}
[box][### Heading]
---
// Is no heading.
\# No heading
Text with # hashtag

View File

@ -18,8 +18,8 @@ use typst::env::{Env, ImageResource, ResourceLoader, SharedEnv};
use typst::eval::{Args, EvalContext, State, Value, ValueFunc};
use typst::export::pdf;
use typst::font::FontLoader;
use typst::geom::{Length, Point, Sides, Size};
use typst::layout::{Element, Frame, Image};
use typst::geom::{Length, Point, Sides, Size, Spec};
use typst::layout::{Element, Expansion, Frame, Image};
use typst::parse::{LineMap, Scanner};
use typst::shaping::Shaped;
use typst::syntax::{Location, Pos, SpanVec, Spanned, WithSpan};
@ -153,12 +153,12 @@ fn test(
let env = env.borrow();
if !frames.is_empty() {
let canvas = draw(&frames, &env, 2.0);
canvas.pixmap.save_png(png_path).unwrap();
let pdf_data = pdf::export(&frames, &env);
fs::write(pdf_path, pdf_data).unwrap();
let canvas = draw(&frames, &env, 2.0);
canvas.pixmap.save_png(png_path).unwrap();
if let Some(ref_path) = ref_path {
if let Ok(ref_pixmap) = Pixmap::load_png(ref_path) {
if canvas.pixmap != ref_pixmap {
@ -184,7 +184,11 @@ fn test_part(i: usize, src: &str, env: &SharedEnv) -> (bool, Vec<Frame>) {
let (compare_ref, ref_diags) = parse_metadata(src, &map);
let mut state = State::default();
state.page.size = Size::uniform(Length::pt(120.0));
// We want to have "unbounded" pages, so we allow them to be infinitely
// large and fit them to match their content.
state.page.size = Size::new(Length::pt(120.0), Length::raw(f64::INFINITY));
state.page.expand = Spec::new(Expansion::Fill, Expansion::Fit);
state.page.margins = Sides::uniform(Some(Length::pt(10.0).into()));
pub fn dump(_: &mut EvalContext, args: &mut Args) -> Value {
@ -283,6 +287,10 @@ fn draw(frames: &[Frame], env: &Env, pixel_per_pt: f32) -> Canvas {
let pixel_width = (pixel_per_pt * width.to_pt() as f32) as u32;
let pixel_height = (pixel_per_pt * height.to_pt() as f32) as u32;
if pixel_width > 4000 || pixel_height > 4000 {
panic!("overlarge image: {} by {}", pixel_width, pixel_height);
}
let mut canvas = Canvas::new(pixel_width, pixel_height).unwrap();
canvas.scale(pixel_per_pt, pixel_per_pt);
canvas.pixmap.fill(Color::BLACK);