Better expansion behaviour 🐪

This makes expansion behaviour inheritable by placing it into the area and passing it down during layouting instead of computing some approximation of what we want during execution.
This commit is contained in:
Laurenz 2021-03-11 10:48:29 +01:00
parent 4e5f85aa4a
commit c1b1dbcc09
14 changed files with 67 additions and 67 deletions

View File

@ -7,7 +7,7 @@ use super::*;
use crate::diag::{Diag, DiagSet};
use crate::geom::{ChildAlign, Dir, Gen, LayoutDirs, Length, Linear, Sides, Size};
use crate::layout::{
Expansion, Node, NodePad, NodePages, NodePar, NodeSpacing, NodeStack, NodeText, Tree,
Node, NodePad, NodePages, NodePar, NodeSpacing, NodeStack, NodeText, Tree,
};
use crate::parse::is_newline;
@ -105,18 +105,18 @@ impl<'a> ExecContext<'a> {
}
}
/// Execute the body of a function and return the result as a stack node.
pub fn exec_body(&mut self, body: &ValueTemplate, expand: Spec<Expansion>) -> Node {
/// Execute a template and return the result as a stack node.
pub fn exec(&mut self, template: &ValueTemplate) -> Node {
let dirs = self.state.dirs;
let align = self.state.align;
self.start_group(ContentGroup);
self.start_par_group();
body.exec(self);
template.exec(self);
self.end_par_group();
let children = self.end_group::<ContentGroup>().1;
NodeStack { dirs, align, expand, children }.into()
NodeStack { dirs, align, children }.into()
}
/// Start a page group based on the active page state.
@ -128,7 +128,6 @@ impl<'a> ExecContext<'a> {
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,
@ -158,7 +157,6 @@ impl<'a> ExecContext<'a> {
child: NodeStack {
dirs: group.dirs,
align: group.align,
expand: group.expand,
children,
}
.into(),
@ -186,13 +184,6 @@ impl<'a> ExecContext<'a> {
self.push(NodePar {
dirs: group.dirs,
align: group.align,
// FIXME: This is a hack and should be superseded by something
// better.
cross_expansion: if self.groups.len() <= 1 {
Expansion::Fill
} else {
Expansion::Fit
},
line_spacing: group.line_spacing,
children,
});
@ -306,7 +297,6 @@ pub enum Softness {
#[derive(Debug)]
struct PageGroup {
size: Size,
expand: Spec<Expansion>,
padding: Sides<Linear>,
dirs: LayoutDirs,
align: ChildAlign,

View File

@ -11,8 +11,7 @@ use std::rc::Rc;
use crate::diag::Pass;
use crate::env::Env;
use crate::eval::{ExprMap, TemplateFunc, TemplateNode, Value, ValueTemplate};
use crate::geom::Spec;
use crate::layout::{self, Expansion, NodeSpacing, NodeStack};
use crate::layout::{self, NodeFixed, NodeSpacing, NodeStack};
use crate::pretty::pretty;
use crate::syntax::*;
@ -120,11 +119,17 @@ impl Exec for NodeRaw {
ctx.apply_parbreak();
}
ctx.push(NodeStack {
dirs: ctx.state.dirs,
align: ctx.state.align,
expand: Spec::uniform(Expansion::Fit),
children,
// This is wrapped in a fixed node to make sure the stack fits to its
// content instead of filling the available area.
ctx.push(NodeFixed {
width: None,
height: None,
child: NodeStack {
dirs: ctx.state.dirs,
align: ctx.state.align,
children,
}
.into(),
});
if self.block {

View File

@ -3,9 +3,8 @@ use std::rc::Rc;
use fontdock::{fallback, FallbackTree, FontStretch, FontStyle, FontVariant, FontWeight};
use crate::geom::{
Align, ChildAlign, Dir, LayoutDirs, Length, Linear, Relative, Sides, Size, Spec,
Align, ChildAlign, Dir, LayoutDirs, Length, Linear, Relative, Sides, Size,
};
use crate::layout::Expansion;
use crate::paper::{Paper, PaperClass, PAPER_A4};
/// The evaluation state.
@ -42,8 +41,6 @@ pub struct PageState {
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 on each side of the page. If a side is set to
/// `None`, the default for the paper class is used.
pub margins: Sides<Option<Linear>>,
@ -55,7 +52,6 @@ impl PageState {
Self {
class: paper.class,
size: paper.size(),
expand: Spec::uniform(Expansion::Fill),
margins: Sides::uniform(None),
}
}

View File

@ -20,7 +20,13 @@ impl Layout for NodeFixed {
self.height.map(|h| h.resolve(full.height)).unwrap_or(current.height),
);
let areas = Areas::once(size);
let fill_if = |cond| if cond { Expand::Fill } else { Expand::Fit };
let expand = Spec::new(
fill_if(self.width.is_some()),
fill_if(self.height.is_some()),
);
let areas = Areas::once(size, expand);
self.child.layout(ctx, &areas)
}
}

View File

@ -55,7 +55,7 @@ pub struct NodePages {
impl NodePages {
/// Layout the page run.
pub fn layout(&self, ctx: &mut LayoutContext) -> Vec<Frame> {
let areas = Areas::repeat(self.size);
let areas = Areas::repeat(self.size, Spec::uniform(Expand::Fill));
let layouted = self.child.layout(ctx, &areas);
layouted.into_frames()
}
@ -85,26 +85,31 @@ pub struct Areas {
pub backlog: Vec<Size>,
/// The final area that is repeated when the backlog is empty.
pub last: Option<Size>,
/// Whether the frames resulting from layouting into this areas should be
/// shrunk to fit their content or expanded to fill the area.
pub expand: Spec<Expand>,
}
impl Areas {
/// Create a new length-1 sequence of areas with just one `area`.
pub fn once(size: Size) -> Self {
pub fn once(size: Size, expand: Spec<Expand>) -> Self {
Self {
current: size,
full: size,
backlog: vec![],
last: None,
expand,
}
}
/// Create a new sequence of areas that repeats `area` indefinitely.
pub fn repeat(size: Size) -> Self {
pub fn repeat(size: Size, expand: Spec<Expand>) -> Self {
Self {
current: size,
full: size,
backlog: vec![],
last: Some(size),
expand,
}
}
@ -129,14 +134,14 @@ impl Areas {
/// Whether to expand or shrink a node along an axis.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum Expansion {
pub enum Expand {
/// Fit the content.
Fit,
/// Fill the available space.
Fill,
}
impl Expansion {
impl Expand {
/// Resolve the expansion to either the `fit` or `fill` length.
///
/// Prefers `fit` if `fill` is infinite.

View File

@ -37,6 +37,7 @@ fn shrink(areas: &Areas, padding: Sides<Linear>) -> Areas {
full: shrink(areas.full),
backlog: areas.backlog.iter().copied().map(shrink).collect(),
last: areas.last.map(shrink),
expand: areas.expand,
}
}

View File

@ -8,8 +8,6 @@ pub struct NodePar {
/// The children are placed in lines along the `cross` direction. The lines
/// are stacked along the `main` direction.
pub dirs: LayoutDirs,
/// Whether to expand the cross axis to fill the area or to fit the content.
pub cross_expansion: Expansion,
/// The spacing to insert after each line.
pub line_spacing: Length,
/// The nodes to be arranged in a paragraph.
@ -42,11 +40,11 @@ impl From<NodePar> for NodeAny {
}
}
struct ParLayouter<'a> {
par: &'a NodePar,
struct ParLayouter {
main: SpecAxis,
cross: SpecAxis,
dirs: LayoutDirs,
line_spacing: Length,
areas: Areas,
finished: Vec<Frame>,
lines: Vec<(Length, Frame, Align)>,
@ -56,13 +54,13 @@ struct ParLayouter<'a> {
line_ruler: Align,
}
impl<'a> ParLayouter<'a> {
fn new(par: &'a NodePar, areas: Areas) -> Self {
impl ParLayouter {
fn new(par: &NodePar, areas: Areas) -> Self {
Self {
par,
main: par.dirs.main.axis(),
cross: par.dirs.cross.axis(),
dirs: par.dirs,
line_spacing: par.line_spacing,
areas,
finished: vec![],
lines: vec![],
@ -134,13 +132,12 @@ impl<'a> ParLayouter<'a> {
}
fn finish_line(&mut self) {
let expand = self.areas.expand.switch(self.dirs);
let full_size = {
let full = self.areas.full.switch(self.dirs);
Gen::new(
self.line_size.main,
self.par
.cross_expansion
.resolve(self.line_size.cross.min(full.cross), full.cross),
expand.cross.resolve(self.line_size.cross.min(full.cross), full.cross),
)
};
@ -165,8 +162,8 @@ impl<'a> ParLayouter<'a> {
// Add line spacing, but only between lines.
if !self.lines.is_empty() {
self.lines_size.main += self.par.line_spacing;
*self.areas.current.get_mut(self.main) -= self.par.line_spacing;
self.lines_size.main += self.line_spacing;
*self.areas.current.get_mut(self.main) -= self.line_spacing;
}
// Update metrics of the whole paragraph.

View File

@ -10,8 +10,6 @@ pub struct NodeStack {
pub dirs: LayoutDirs,
/// 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 expand: Spec<Expansion>,
/// The nodes to be stacked.
pub children: Vec<Node>,
}
@ -40,8 +38,7 @@ impl From<NodeStack> for NodeAny {
}
}
struct StackLayouter<'a> {
stack: &'a NodeStack,
struct StackLayouter {
main: SpecAxis,
dirs: LayoutDirs,
areas: Areas,
@ -51,10 +48,9 @@ struct StackLayouter<'a> {
ruler: Align,
}
impl<'a> StackLayouter<'a> {
fn new(stack: &'a NodeStack, areas: Areas) -> Self {
impl StackLayouter {
fn new(stack: &NodeStack, areas: Areas) -> Self {
Self {
stack,
main: stack.dirs.main.axis(),
dirs: stack.dirs,
areas,
@ -97,7 +93,7 @@ impl<'a> StackLayouter<'a> {
fn finish_area(&mut self) {
let full_size = {
let expand = self.stack.expand.switch(self.dirs);
let expand = self.areas.expand.switch(self.dirs);
let full = self.areas.full.switch(self.dirs);
Gen::new(
expand.main.resolve(self.used.main.min(full.main), full.main),

View File

@ -29,8 +29,7 @@ pub fn pad(ctx: &mut EvalContext, args: &mut ValueArgs) -> Value {
Value::template("pad", move |ctx| {
let snapshot = ctx.state.clone();
let expand = Spec::uniform(Expansion::Fit);
let child = ctx.exec_body(&body, expand);
let child = ctx.exec(&body);
ctx.push(NodePad { padding, child });
ctx.state = snapshot;

View File

@ -45,19 +45,16 @@ pub fn page(ctx: &mut EvalContext, args: &mut ValueArgs) -> Value {
if let Some(paper) = paper {
ctx.state.page.class = paper.class;
ctx.state.page.size = paper.size();
ctx.state.page.expand = Spec::uniform(Expansion::Fill);
}
if let Some(width) = width {
ctx.state.page.class = PaperClass::Custom;
ctx.state.page.size.width = width;
ctx.state.page.expand.horizontal = Expansion::Fill;
}
if let Some(height) = height {
ctx.state.page.class = PaperClass::Custom;
ctx.state.page.size.height = height;
ctx.state.page.expand.vertical = Expansion::Fill;
}
if let Some(margins) = margins {
@ -83,7 +80,6 @@ pub fn page(ctx: &mut EvalContext, args: &mut ValueArgs) -> Value {
if flip.unwrap_or(false) {
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);
}
ctx.set_dirs(Gen::new(main, cross));

View File

@ -26,17 +26,13 @@ pub fn box_(ctx: &mut EvalContext, args: &mut ValueArgs) -> Value {
let color = args.get(ctx, "color");
let body = args.find::<ValueTemplate>(ctx).unwrap_or_default();
let fill_if = |c| if c { Expansion::Fill } else { Expansion::Fit };
let expand = Spec::new(fill_if(width.is_some()), fill_if(height.is_some()));
Value::template("box", move |ctx| {
let snapshot = ctx.state.clone();
ctx.set_dirs(Gen::new(main, cross));
let child = ctx.exec_body(&body, expand);
let child = ctx.exec(&body);
let fixed = NodeFixed { width, height, child };
if let Some(color) = color {
ctx.push(NodeBackground {
fill: Fill::Color(color),

BIN
tests/ref/expand.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

14
tests/typ/expand.typ Normal file
View File

@ -0,0 +1,14 @@
// Test fit/fill expansion.
---
#let right(body) = align(right, body)
#let pad(body) = pad(left: 10pt, right: 10pt, body)
// Top-level paragraph fills page, boxed paragraph only when width is fixed.
L #right[R] \
#box(width: 50pt)[L #right[R]] \
#box[L #right[R]] \
// Pad inherits expansion behaviour.
#pad[PL #right[PR]] \
#box(pad[PL #right[PR]])

View File

@ -20,8 +20,8 @@ use typst::eval::{EvalContext, Scope, Value, ValueArgs, ValueFunc};
use typst::exec::State;
use typst::export::pdf;
use typst::font::FsIndexExt;
use typst::geom::{Length, Point, Sides, Size, Spec};
use typst::layout::{Element, Expansion, Fill, Frame, Geometry, Image, Shape};
use typst::geom::{Length, Point, Sides, Size};
use typst::layout::{Element, Fill, Frame, Geometry, Image, Shape};
use typst::library;
use typst::parse::{LineMap, Scanner};
use typst::shaping::Shaped;
@ -202,7 +202,6 @@ fn test_part(
// large and fit them to match their content.
let mut state = State::default();
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()));
let Pass { output: mut frames, diags } = typeset(env, &src, &scope, state);