diff --git a/src/eval/args.rs b/src/eval/args.rs index 04f83b506..dda6fedf7 100644 --- a/src/eval/args.rs +++ b/src/eval/args.rs @@ -1,7 +1,5 @@ //! Simplifies argument parsing. -use std::mem; - use super::{Convert, EvalContext, RefKey, ValueDict}; use crate::syntax::{SpanWith, Spanned}; @@ -67,7 +65,7 @@ impl Args { { for (&key, entry) in self.0.v.nums_mut() { let span = entry.value.span; - match T::convert(mem::take(&mut entry.value)).0 { + match T::convert(std::mem::take(&mut entry.value)).0 { Ok(t) => { self.0.v.remove(key); return Some(t); @@ -87,7 +85,7 @@ impl Args { std::iter::from_fn(move || { for (&key, entry) in self.0.v.nums_mut().skip(skip) { let span = entry.value.span; - match T::convert(mem::take(&mut entry.value)).0 { + match T::convert(std::mem::take(&mut entry.value)).0 { Ok(t) => { self.0.v.remove(key); return Some(t); @@ -109,7 +107,7 @@ impl Args { std::iter::from_fn(move || { for (key, entry) in self.0.v.strs_mut().skip(skip) { let span = entry.value.span; - match T::convert(mem::take(&mut entry.value)).0 { + match T::convert(std::mem::take(&mut entry.value)).0 { Ok(t) => { let key = key.clone(); self.0.v.remove(&key); diff --git a/src/eval/mod.rs b/src/eval/mod.rs index 313dddc49..6f8b3f5b7 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -16,16 +16,15 @@ pub use state::*; pub use value::*; use std::any::Any; -use std::mem; use std::rc::Rc; use fontdock::FontStyle; use crate::diag::Diag; use crate::diag::{Deco, Feedback, Pass}; -use crate::geom::{Gen, Length, Relative, Spec, Switch}; +use crate::geom::{Gen, Length, Relative}; use crate::layout::{ - Document, LayoutNode, Pad, Pages, Par, Softness, Spacing, Stack, Text, + Document, Expansion, LayoutNode, Pad, Pages, Par, Softness, Spacing, Stack, Text, }; use crate::syntax::*; @@ -35,11 +34,9 @@ use crate::syntax::*; /// evaluation. pub fn eval(tree: &SynTree, state: State) -> Pass { let mut ctx = EvalContext::new(state); - ctx.start_page_group(false); tree.eval(&mut ctx); ctx.end_page_group(); - ctx.finish() } @@ -125,7 +122,7 @@ impl EvalContext { /// [`push`]: #method.push /// [`end_group`]: #method.end_group pub fn start_group(&mut self, meta: T) { - self.groups.push((Box::new(meta), mem::take(&mut self.inner))); + self.groups.push((Box::new(meta), std::mem::take(&mut self.inner))); } /// End a layouting group started with [`start_group`]. @@ -134,9 +131,15 @@ impl EvalContext { /// /// [`start_group`]: #method.start_group pub fn end_group(&mut self) -> (T, Vec) { + if let Some(&LayoutNode::Spacing(spacing)) = self.inner.last() { + if spacing.softness == Softness::Soft { + self.inner.pop(); + } + } + let (any, outer) = self.groups.pop().expect("no pushed group"); let group = *any.downcast::().expect("bad group type"); - (group, mem::replace(&mut self.inner, outer)) + (group, std::mem::replace(&mut self.inner, outer)) } /// Start a page run group based on the active page state. @@ -167,9 +170,9 @@ impl EvalContext { padding, child: LayoutNode::dynamic(Stack { dirs, - children, aligns, - expand: Spec::new(true, true), + expansion: Gen::new(Expansion::Fill, Expansion::Fill), + children, }), }), }) @@ -191,13 +194,13 @@ impl EvalContext { if !children.is_empty() { // FIXME: This is a hack and should be superseded by constraints // having min and max size. - let expand_cross = self.groups.len() <= 1; + let cross_expansion = Expansion::fill_if(self.groups.len() <= 1); self.push(Par { dirs, + aligns, + cross_expansion, line_spacing, children, - aligns, - expand: Gen::new(false, expand_cross).switch(dirs), }); } } @@ -222,7 +225,7 @@ impl EvalContext { Text { text, dir: self.state.dirs.cross, - size: self.state.font.font_size(), + font_size: self.state.font.font_size(), families: Rc::clone(&self.state.font.families), variant, aligns: self.state.aligns, @@ -284,26 +287,11 @@ impl Eval for SynNode { ctx.start_par_group(); } - SynNode::Emph => { - ctx.state.font.emph ^= true; - } - - SynNode::Strong => { - ctx.state.font.strong ^= true; - } - - SynNode::Heading(heading) => { - heading.eval(ctx); - } - - SynNode::Raw(raw) => { - raw.eval(ctx); - } - - SynNode::Expr(expr) => { - let value = expr.eval(ctx); - value.eval(ctx); - } + SynNode::Emph => ctx.state.font.emph ^= true, + SynNode::Strong => ctx.state.font.strong ^= true, + SynNode::Heading(heading) => heading.eval(ctx), + SynNode::Raw(raw) => raw.eval(ctx), + SynNode::Expr(expr) => expr.eval(ctx).eval(ctx), } } } @@ -339,9 +327,9 @@ impl Eval for NodeRaw { ctx.push(Stack { dirs: ctx.state.dirs, - children, aligns: ctx.state.aligns, - expand: Spec::new(false, false), + expansion: Gen::new(Expansion::Fit, Expansion::Fit), + children, }); ctx.state.font.families = prev; diff --git a/src/export/pdf.rs b/src/export/pdf.rs index 722728ff4..905ef9840 100644 --- a/src/export/pdf.rs +++ b/src/export/pdf.rs @@ -142,9 +142,9 @@ impl<'a, W: Write> PdfExporter<'a, W> { match element { LayoutElement::Text(shaped) => { // Check if we need to issue a font switching action. - if shaped.face != face || shaped.size != size { + if shaped.face != face || shaped.font_size != size { face = shaped.face; - size = shaped.size; + size = shaped.font_size; text.tf( self.to_pdf[&shaped.face] as u32 + 1, size.to_pt() as f32, diff --git a/src/layout/document.rs b/src/layout/document.rs index 69ac3d9de..a91dbbe9f 100644 --- a/src/layout/document.rs +++ b/src/layout/document.rs @@ -3,6 +3,7 @@ use super::*; /// The top-level layout node. #[derive(Debug, Clone, PartialEq)] pub struct Document { + /// The runs of pages with same properties. pub runs: Vec, } @@ -22,26 +23,17 @@ impl Document { pub struct Pages { /// The size of the pages. pub size: Size, - /// The layout node that produces the actual pages. + /// The layout node that produces the actual pages (typically a [stack]). + /// + /// [stack]: struct.Stack.html pub child: LayoutNode, } impl Pages { /// Layout the page run. pub async fn layout(&self, ctx: &mut LayoutContext) -> Vec { - let constraints = LayoutConstraints { - spaces: vec![LayoutSpace { base: self.size, size: self.size }], - repeat: true, - }; - - self.child - .layout(ctx, constraints) - .await - .into_iter() - .filter_map(|item| match item { - Layouted::Spacing(_) => None, - Layouted::Box(layout, _) => Some(layout), - }) - .collect() + let areas = Areas::repeat(self.size); + let layouted = self.child.layout(ctx, &areas).await; + layouted.into_iter().filter_map(Layouted::into_boxed).collect() } } diff --git a/src/layout/fixed.rs b/src/layout/fixed.rs index 939473056..78a512e69 100644 --- a/src/layout/fixed.rs +++ b/src/layout/fixed.rs @@ -4,34 +4,25 @@ use crate::geom::Linear; /// A node that can fix its child's width and height. #[derive(Debug, Clone, PartialEq)] pub struct Fixed { + /// The fixed width, if any. pub width: Option, + /// The fixed height, if any. pub height: Option, + /// The child node whose size to fix. pub child: LayoutNode, } #[async_trait(?Send)] impl Layout for Fixed { - async fn layout( - &self, - ctx: &mut LayoutContext, - constraints: LayoutConstraints, - ) -> Vec { - let space = constraints.spaces[0]; + async fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Vec { + let Area { rem, full } = areas.current; let size = Size::new( - self.width - .map(|w| w.eval(space.base.width)) - .unwrap_or(space.size.width), - self.height - .map(|h| h.eval(space.base.height)) - .unwrap_or(space.size.height), + self.width.map(|w| w.eval(full.width)).unwrap_or(rem.width), + self.height.map(|h| h.eval(full.height)).unwrap_or(rem.height), ); - self.child - .layout(ctx, LayoutConstraints { - spaces: vec![LayoutSpace { base: size, size }], - repeat: false, - }) - .await + let areas = Areas::once(size); + self.child.layout(ctx, &areas).await } } diff --git a/src/layout/mod.rs b/src/layout/mod.rs index d5ab24e73..a6ef4300a 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -51,11 +51,86 @@ pub trait Layout { /// constraints: LayoutConstraints, /// ) -> Vec; /// ``` - async fn layout( - &self, - ctx: &mut LayoutContext, - constraints: LayoutConstraints, - ) -> Vec; + async fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Vec; +} + +/// A sequence of areas to layout into. +#[derive(Debug, Clone, PartialEq)] +pub struct Areas { + /// The current area. + pub current: Area, + /// The backlog of followup areas. + /// + /// _Note_: This works stack-like and not queue-like! + pub backlog: Vec, + /// The last area that is repeated when the backlog is empty. + pub last: Option, +} + +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), + backlog: vec![], + last: None, + } + } + + /// Create a new sequence of areas that repeats `area` indefinitely. + pub fn repeat(size: Size) -> Self { + Self { + current: Area::new(size), + backlog: vec![], + last: Some(size), + } + } + + /// 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); + } + } + + /// Whether `current` is a fully sized (untouched) copy of the last area. + /// + /// If this is false calling `next()` will have no effect. + pub fn in_full_last(&self) -> bool { + self.backlog.is_empty() && self.last.map_or(true, |size| self.current.rem == size) + } +} + +/// The 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 } + } +} + +/// How to determine a container's size along an axis. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum Expansion { + /// Fit the content. + Fit, + /// Fill the available space. + Fill, +} + +impl Expansion { + /// Returns `Fill` if the condition is true and `Fit` otherwise. + pub fn fill_if(condition: bool) -> Self { + if condition { Self::Fill } else { Self::Fit } + } } /// An item that is produced by [layouting] a node. @@ -65,27 +140,18 @@ pub trait Layout { pub enum Layouted { /// Spacing that should be added to the parent. Spacing(Length), - /// A box that should be aligned in the parent. - Box(BoxLayout, Gen), + /// A box that should be added to and aligned in the parent. + Boxed(BoxLayout, Gen), } -/// The constraints for layouting a single node. -#[derive(Debug, Clone)] -pub struct LayoutConstraints { - /// The spaces to layout into. - pub spaces: Vec, - /// Whether to spill over into copies of the last space or finish layouting - /// when the last space is used up. - pub repeat: bool, -} - -/// The space into which content is laid out. -#[derive(Debug, Copy, Clone, PartialEq)] -pub struct LayoutSpace { - /// The full size of this container (the base for relative sizes). - pub base: Size, - /// The maximum size of the rectangle to layout into. - pub size: Size, +impl Layouted { + /// Return the box if this if its a box variant. + pub fn into_boxed(self) -> Option { + match self { + Self::Spacing(_) => None, + Self::Boxed(boxed, _) => Some(boxed), + } + } } /// A finished box with content at fixed positions. diff --git a/src/layout/node.rs b/src/layout/node.rs index 0adbb1457..31213b9d5 100644 --- a/src/layout/node.rs +++ b/src/layout/node.rs @@ -18,12 +18,23 @@ pub enum LayoutNode { } impl LayoutNode { - /// Create a new model node form a type implementing `DynNode`. + /// Create a new dynamic node. pub fn dynamic(inner: T) -> Self { Self::Dyn(Dynamic::new(inner)) } } +#[async_trait(?Send)] +impl Layout for LayoutNode { + async fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Vec { + match self { + Self::Spacing(spacing) => spacing.layout(ctx, areas).await, + Self::Text(text) => text.layout(ctx, areas).await, + Self::Dyn(boxed) => boxed.layout(ctx, areas).await, + } + } +} + impl Debug for LayoutNode { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self { @@ -34,21 +45,6 @@ impl Debug for LayoutNode { } } -#[async_trait(?Send)] -impl Layout for LayoutNode { - async fn layout( - &self, - ctx: &mut LayoutContext, - constraints: LayoutConstraints, - ) -> Vec { - match self { - Self::Spacing(spacing) => spacing.layout(ctx, constraints).await, - Self::Text(text) => text.layout(ctx, constraints).await, - Self::Dyn(boxed) => boxed.layout(ctx, constraints).await, - } - } -} - /// A wrapper around a boxed dynamic node. /// /// _Note_: This is needed because the compiler can't `derive(PartialEq)` for @@ -80,6 +76,12 @@ impl Debug for Dynamic { } } +impl From for LayoutNode { + fn from(dynamic: Dynamic) -> Self { + Self::Dyn(dynamic) + } +} + impl Clone for Dynamic { fn clone(&self) -> Self { Self(self.0.dyn_clone()) @@ -92,12 +94,6 @@ impl PartialEq for Dynamic { } } -impl From for LayoutNode { - fn from(dynamic: Dynamic) -> Self { - Self::Dyn(dynamic) - } -} - /// A dynamic node, which can implement custom layouting behaviour. /// /// This trait just combines the requirements for types to qualify as dynamic diff --git a/src/layout/pad.rs b/src/layout/pad.rs index e7584dc8c..2574fa165 100644 --- a/src/layout/pad.rs +++ b/src/layout/pad.rs @@ -4,45 +4,40 @@ use crate::geom::Linear; /// A node that pads its child at the sides. #[derive(Debug, Clone, PartialEq)] pub struct Pad { + /// The amount of padding. pub padding: Sides, + /// The child node whose sides to pad. pub child: LayoutNode, } #[async_trait(?Send)] impl Layout for Pad { - async fn layout( - &self, - ctx: &mut LayoutContext, - constraints: LayoutConstraints, - ) -> Vec { - self.child - .layout(ctx, LayoutConstraints { - spaces: constraints - .spaces - .into_iter() - .map(|space| LayoutSpace { - base: space.base - self.padding.eval(space.base).size(), - size: space.size - self.padding.eval(space.size).size(), - }) - .collect(), - repeat: constraints.repeat, - }) - .await - .into_iter() - .map(|item| match item { - Layouted::Box(boxed, align) => { - let padding = self.padding.eval(boxed.size); - let padded = boxed.size + padding.size(); + async fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Vec { + let shrink = |size| size - self.padding.eval(size).size(); + let areas = Areas { + current: Area { + rem: shrink(areas.current.rem), + full: shrink(areas.current.full), + }, + backlog: areas.backlog.iter().copied().map(shrink).collect(), + last: areas.last.map(shrink), + }; - let mut outer = BoxLayout::new(padded); - let start = Point::new(padding.left, padding.top); - outer.push_layout(start, boxed); + let mut layouted = self.child.layout(ctx, &areas).await; - Layouted::Box(outer, align) + for item in &mut layouted { + if let Layouted::Boxed(boxed, _) = item { + let padding = self.padding.eval(boxed.size); + let origin = Point::new(padding.left, padding.top); + + boxed.size += padding.size(); + for (point, _) in &mut boxed.elements { + *point += origin; } - item => item, - }) - .collect() + } + } + + layouted } } diff --git a/src/layout/spacing.rs b/src/layout/spacing.rs index 766ff4d23..427cb7b06 100644 --- a/src/layout/spacing.rs +++ b/src/layout/spacing.rs @@ -2,16 +2,21 @@ use std::fmt::{self, Debug, Formatter}; use super::*; -/// A node that inserts spacing. +/// A spacing node. #[derive(Copy, Clone, PartialEq)] pub struct Spacing { + /// The amount of spacing to insert. pub amount: Length, + /// Spacing interaction, see [Softness's] documentation for more + /// information. + /// + /// [Softness's]: enum.Softness.html pub softness: Softness, } #[async_trait(?Send)] impl Layout for Spacing { - async fn layout(&self, _: &mut LayoutContext, _: LayoutConstraints) -> Vec { + async fn layout(&self, _: &mut LayoutContext, _: &Areas) -> Vec { vec![Layouted::Spacing(self.amount)] } } diff --git a/src/layout/text.rs b/src/layout/text.rs index 1c09b40a8..7d8386a16 100644 --- a/src/layout/text.rs +++ b/src/layout/text.rs @@ -9,32 +9,36 @@ use crate::shaping; /// A text node. #[derive(Clone, PartialEq)] pub struct Text { + /// The text. pub text: String, - pub size: Length, + /// The font size. + pub font_size: Length, + /// The text direction. pub dir: Dir, + /// The families used for font fallback. pub families: Rc, + /// The font variant, pub variant: FontVariant, + /// How to align this text node in its parent. pub aligns: Gen, } #[async_trait(?Send)] impl Layout for Text { - async fn layout( - &self, - ctx: &mut LayoutContext, - _constraints: LayoutConstraints, - ) -> Vec { + async fn layout(&self, ctx: &mut LayoutContext, _: &Areas) -> Vec { let mut loader = ctx.loader.borrow_mut(); - let boxed = shaping::shape( - &self.text, - self.size, - self.dir, - &mut loader, - &self.families, - self.variant, - ) - .await; - vec![Layouted::Box(boxed, self.aligns)] + vec![Layouted::Boxed( + shaping::shape( + &mut loader, + &self.text, + self.font_size, + self.dir, + &self.families, + self.variant, + ) + .await, + self.aligns, + )] } } diff --git a/src/library/boxed.rs b/src/library/boxed.rs index 0c1ed30a7..0045b0bd4 100644 --- a/src/library/boxed.rs +++ b/src/library/boxed.rs @@ -1,5 +1,5 @@ use crate::geom::Linear; -use crate::layout::{Fixed, Stack}; +use crate::layout::{Expansion, Fixed, Stack}; use crate::prelude::*; /// `box`: Layouts its contents into a box. @@ -20,9 +20,7 @@ pub fn boxed(mut args: Args, ctx: &mut EvalContext) -> Value { ctx.start_group(()); ctx.start_par_group(); - body.eval(ctx); - ctx.end_par_group(); let ((), children) = ctx.end_group(); @@ -33,7 +31,11 @@ pub fn boxed(mut args: Args, ctx: &mut EvalContext) -> Value { dirs, children, aligns, - expand: Spec::new(width.is_some(), height.is_some()), + expansion: Spec::new( + Expansion::fill_if(width.is_some()), + Expansion::fill_if(height.is_some()), + ) + .switch(dirs), }), }); diff --git a/src/library/page.rs b/src/library/page.rs index 570dbb106..efb40f100 100644 --- a/src/library/page.rs +++ b/src/library/page.rs @@ -1,5 +1,3 @@ -use std::mem; - use crate::geom::{Length, Linear}; use crate::paper::{Paper, PaperClass}; use crate::prelude::*; @@ -56,7 +54,7 @@ pub fn page(mut args: Args, ctx: &mut EvalContext) -> Value { if args.get::<_, bool>(ctx, "flip").unwrap_or(false) { let size = &mut ctx.state.page.size; - mem::swap(&mut size.width, &mut size.height); + std::mem::swap(&mut size.width, &mut size.height); } args.done(ctx); diff --git a/src/shaping.rs b/src/shaping.rs index bc6fb6b74..5c718acb8 100644 --- a/src/shaping.rs +++ b/src/shaping.rs @@ -22,11 +22,11 @@ pub struct Shaped { pub face: FaceId, /// The shaped glyphs. pub glyphs: Vec, - /// The horizontal offsets of the glyphs. This is indexed parallel to `glyphs`. - /// Vertical offets are not yet supported. + /// The horizontal offsets of the glyphs. This is indexed parallel to + /// `glyphs`. Vertical offets are not yet supported. pub offsets: Vec, /// The font size. - pub size: Length, + pub font_size: Length, } impl Shaped { @@ -37,7 +37,7 @@ impl Shaped { face, glyphs: vec![], offsets: vec![], - size, + font_size: size, } } @@ -62,15 +62,15 @@ impl Debug for Shaped { /// /// [`Shaped`]: struct.Shaped.html pub async fn shape( - text: &str, - size: Length, - dir: Dir, loader: &mut FontLoader, + text: &str, + font_size: Length, + dir: Dir, fallback: &FallbackTree, variant: FontVariant, ) -> BoxLayout { - let mut layout = BoxLayout::new(Size::new(Length::ZERO, size)); - let mut shaped = Shaped::new(FaceId::MAX, size); + let mut layout = BoxLayout::new(Size::new(Length::ZERO, font_size)); + let mut shaped = Shaped::new(FaceId::MAX, font_size); let mut offset = Length::ZERO; // Create an iterator with conditional direction. @@ -86,7 +86,7 @@ pub async fn shape( let query = FaceQuery { fallback: fallback.iter(), variant, c }; if let Some((id, owned_face)) = loader.query(query).await { let face = owned_face.get(); - let (glyph, width) = match lookup_glyph(face, c, size) { + let (glyph, width) = match lookup_glyph(face, c, font_size) { Some(v) => v, None => continue, }; @@ -96,7 +96,7 @@ pub async fn shape( let pos = Point::new(layout.size.width, Length::ZERO); layout.push(pos, LayoutElement::Text(shaped)); layout.size.width += offset; - shaped = Shaped::new(FaceId::MAX, size); + shaped = Shaped::new(FaceId::MAX, font_size); offset = Length::ZERO; } diff --git a/tests/test_typeset.rs b/tests/test_typeset.rs index 3789d9785..e70447459 100644 --- a/tests/test_typeset.rs +++ b/tests/test_typeset.rs @@ -198,9 +198,9 @@ fn render_shaped( let path = builder.0.finish(); let units_per_em = face.units_per_em().unwrap_or(1000); - let s = scale * (shaped.size / units_per_em as f64); + let s = scale * (shaped.font_size / units_per_em as f64); let x = pos.x + scale * offset; - let y = pos.y + scale * shaped.size; + let y = pos.y + scale * shaped.font_size; let t = Transform::create_scale(s.to_pt() as f32, -s.to_pt() as f32) .post_translate(Vector::new(x.to_pt() as f32, y.to_pt() as f32));