diff --git a/src/exec/context.rs b/src/exec/context.rs index d491a251..4d2047a6 100644 --- a/src/exec/context.rs +++ b/src/exec/context.rs @@ -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) -> 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::().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, padding: Sides, dirs: LayoutDirs, align: ChildAlign, diff --git a/src/exec/mod.rs b/src/exec/mod.rs index ea2c90f4..79ad81e7 100644 --- a/src/exec/mod.rs +++ b/src/exec/mod.rs @@ -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 { diff --git a/src/exec/state.rs b/src/exec/state.rs index 416b5d08..3293662a 100644 --- a/src/exec/state.rs +++ b/src/exec/state.rs @@ -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, /// 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>, @@ -55,7 +52,6 @@ impl PageState { Self { class: paper.class, size: paper.size(), - expand: Spec::uniform(Expansion::Fill), margins: Sides::uniform(None), } } diff --git a/src/layout/fixed.rs b/src/layout/fixed.rs index 60dbe4d6..a9554a07 100644 --- a/src/layout/fixed.rs +++ b/src/layout/fixed.rs @@ -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) } } diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 622b0363..2e6a39ff 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -55,7 +55,7 @@ pub struct NodePages { impl NodePages { /// Layout the page run. pub fn layout(&self, ctx: &mut LayoutContext) -> Vec { - 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, /// The final area that is repeated when the backlog is empty. pub last: Option, + /// 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, } 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) -> 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) -> 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. diff --git a/src/layout/pad.rs b/src/layout/pad.rs index 425fa41b..02adebeb 100644 --- a/src/layout/pad.rs +++ b/src/layout/pad.rs @@ -37,6 +37,7 @@ fn shrink(areas: &Areas, padding: Sides) -> Areas { full: shrink(areas.full), backlog: areas.backlog.iter().copied().map(shrink).collect(), last: areas.last.map(shrink), + expand: areas.expand, } } diff --git a/src/layout/par.rs b/src/layout/par.rs index 45494dec..9a5d26dd 100644 --- a/src/layout/par.rs +++ b/src/layout/par.rs @@ -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 for NodeAny { } } -struct ParLayouter<'a> { - par: &'a NodePar, +struct ParLayouter { main: SpecAxis, cross: SpecAxis, dirs: LayoutDirs, + line_spacing: Length, areas: Areas, finished: Vec, 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. diff --git a/src/layout/stack.rs b/src/layout/stack.rs index 7d1d7a12..73bd49c1 100644 --- a/src/layout/stack.rs +++ b/src/layout/stack.rs @@ -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, /// The nodes to be stacked. pub children: Vec, } @@ -40,8 +38,7 @@ impl From 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), diff --git a/src/library/pad.rs b/src/library/pad.rs index f9e4386d..0bf93093 100644 --- a/src/library/pad.rs +++ b/src/library/pad.rs @@ -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; diff --git a/src/library/page.rs b/src/library/page.rs index 1fdf4d3f..4ab92b31 100644 --- a/src/library/page.rs +++ b/src/library/page.rs @@ -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)); diff --git a/src/library/shapes.rs b/src/library/shapes.rs index 5e638c01..9c466318 100644 --- a/src/library/shapes.rs +++ b/src/library/shapes.rs @@ -26,17 +26,13 @@ pub fn box_(ctx: &mut EvalContext, args: &mut ValueArgs) -> Value { let color = args.get(ctx, "color"); let body = args.find::(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), diff --git a/tests/ref/expand.png b/tests/ref/expand.png new file mode 100644 index 00000000..fc1f5de9 Binary files /dev/null and b/tests/ref/expand.png differ diff --git a/tests/typ/expand.typ b/tests/typ/expand.typ new file mode 100644 index 00000000..3b242928 --- /dev/null +++ b/tests/typ/expand.typ @@ -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]]) diff --git a/tests/typeset.rs b/tests/typeset.rs index 232dfa5c..ff0e44bc 100644 --- a/tests/typeset.rs +++ b/tests/typeset.rs @@ -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);