diff --git a/src/layout/mod.rs b/src/layout/mod.rs index cb6f1348e..d563dafba 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -17,10 +17,10 @@ use std::rc::Rc; use crate::eval::{StyleChain, Styled}; use crate::font::FontStore; -use crate::frame::Frame; -use crate::geom::{Align, Linear, Point, Sides, Size, Spec}; +use crate::frame::{Element, Frame, Geometry, Shape, Stroke}; +use crate::geom::{Align, Linear, Paint, Point, Sides, Size, Spec}; use crate::image::ImageStore; -use crate::library::{AlignNode, Move, PadNode, PageNode, SizedNode, TransformNode}; +use crate::library::{AlignNode, Move, PadNode, PageNode, TransformNode}; use crate::Context; /// The root layout node, a document consisting of top-level page runs. @@ -153,6 +153,16 @@ impl PackedNode { } } + /// Fill the frames resulting from a node. + pub fn filled(self, fill: Paint) -> Self { + FillNode { fill, child: self }.pack() + } + + /// Stroke the frames resulting from a node. + pub fn stroked(self, stroke: Stroke) -> Self { + StrokeNode { stroke, child: self }.pack() + } + /// Set alignments for this node. pub fn aligned(self, aligns: Spec>) -> Self { if aligns.any(Option::is_some) { @@ -294,3 +304,107 @@ where state.finish() } } + +/// A node that sizes its child. +#[derive(Debug, Hash)] +pub struct SizedNode { + /// How to size the node horizontally and vertically. + pub sizing: Spec>, + /// The node to be sized. + pub child: PackedNode, +} + +impl Layout for SizedNode { + fn layout( + &self, + ctx: &mut LayoutContext, + regions: &Regions, + styles: StyleChain, + ) -> Vec>> { + let is_auto = self.sizing.map_is_none(); + let is_rel = self.sizing.map(|s| s.map_or(false, Linear::is_relative)); + + // The "pod" is the region into which the child will be layouted. + let pod = { + // Resolve the sizing to a concrete size. + let size = self + .sizing + .zip(regions.base) + .map(|(s, b)| s.map(|v| v.resolve(b))) + .unwrap_or(regions.current); + + // Select the appropriate base and expansion for the child depending + // on whether it is automatically or linearly sized. + let base = is_auto.select(regions.base, size); + let expand = regions.expand | !is_auto; + + Regions::one(size, base, expand) + }; + + let mut frames = self.child.layout(ctx, &pod, styles); + let Constrained { item: frame, cts } = &mut frames[0]; + + // Ensure frame size matches regions size if expansion is on. + let target = regions.expand.select(regions.current, frame.size); + Rc::make_mut(frame).resize(target, Align::LEFT_TOP); + + // Set base & exact constraints if the child is automatically sized + // since we don't know what the child might have done. Also set base if + // our sizing is relative. + *cts = Constraints::new(regions.expand); + cts.exact = regions.current.filter(regions.expand | is_auto); + cts.base = regions.base.filter(is_rel | is_auto); + + frames + } +} + +/// Fill the frames resulting from a node. +#[derive(Debug, Hash)] +pub struct FillNode { + /// How to fill the frames resulting from the `child`. + pub fill: Paint, + /// The node to fill. + pub child: PackedNode, +} + +impl Layout for FillNode { + fn layout( + &self, + ctx: &mut LayoutContext, + regions: &Regions, + styles: StyleChain, + ) -> Vec>> { + let mut frames = self.child.layout(ctx, regions, styles); + for Constrained { item: frame, .. } in &mut frames { + let shape = Shape::filled(Geometry::Rect(frame.size), self.fill); + Rc::make_mut(frame).prepend(Point::zero(), Element::Shape(shape)); + } + frames + } +} + +/// Stroke the frames resulting from a node. +#[derive(Debug, Hash)] +pub struct StrokeNode { + /// How to stroke the frames resulting from the `child`. + pub stroke: Stroke, + /// The node to stroke. + pub child: PackedNode, +} + +impl Layout for StrokeNode { + fn layout( + &self, + ctx: &mut LayoutContext, + regions: &Regions, + styles: StyleChain, + ) -> Vec>> { + let mut frames = self.child.layout(ctx, regions, styles); + for Constrained { item: frame, .. } in &mut frames { + let shape = Shape::stroked(Geometry::Rect(frame.size), self.stroke); + Rc::make_mut(frame).prepend(Point::zero(), Element::Shape(shape)); + } + frames + } +} diff --git a/src/library/container.rs b/src/library/container.rs new file mode 100644 index 000000000..ae097a467 --- /dev/null +++ b/src/library/container.rs @@ -0,0 +1,26 @@ +//! Inline- and block-level containers. + +use super::prelude::*; + +/// Size content and place it into a paragraph. +pub struct BoxNode; + +#[class] +impl BoxNode { + fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult { + let width = args.named("width")?; + let height = args.named("height")?; + let body: PackedNode = args.find().unwrap_or_default(); + Ok(Node::inline(body.sized(Spec::new(width, height)))) + } +} + +/// Place content into a separate flow. +pub struct BlockNode; + +#[class] +impl BlockNode { + fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult { + Ok(Node::Block(args.find().unwrap_or_default())) + } +} diff --git a/src/library/flow.rs b/src/library/flow.rs index cfcd65619..3405b04a0 100644 --- a/src/library/flow.rs +++ b/src/library/flow.rs @@ -138,6 +138,12 @@ impl<'a> FlowLayouter<'a> { } } + if self.expand.y { + while self.regions.backlog.len() > 0 { + self.finish_region(); + } + } + self.finish_region(); self.finished } diff --git a/src/library/grid.rs b/src/library/grid.rs index ee9aafe17..59f524273 100644 --- a/src/library/grid.rs +++ b/src/library/grid.rs @@ -69,7 +69,7 @@ castable! { Value::Relative(v) => vec![TrackSizing::Linear(v.into())], Value::Linear(v) => vec![TrackSizing::Linear(v)], Value::Fractional(v) => vec![TrackSizing::Fractional(v)], - Value::Int(v) => vec![TrackSizing::Auto; Value::Int(v).cast()?], + Value::Int(v) => vec![TrackSizing::Auto; Value::Int(v).cast::()?.get()], Value::Array(values) => values .into_iter() .filter_map(|v| v.cast().ok()) diff --git a/src/library/mod.rs b/src/library/mod.rs index c4ec29880..51d57c3ef 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -5,6 +5,7 @@ pub mod align; pub mod columns; +pub mod container; pub mod deco; pub mod flow; pub mod grid; @@ -17,9 +18,9 @@ pub mod page; pub mod par; pub mod placed; pub mod shape; -pub mod sized; pub mod spacing; pub mod stack; +pub mod table; pub mod text; pub mod transform; pub mod utility; @@ -27,6 +28,7 @@ pub mod utility; pub use self::image::*; pub use align::*; pub use columns::*; +pub use container::*; pub use deco::*; pub use flow::*; pub use grid::*; @@ -38,9 +40,9 @@ pub use page::*; pub use par::*; pub use placed::*; pub use shape::*; -pub use sized::*; pub use spacing::*; pub use stack::*; +pub use table::*; pub use text::*; pub use transform::*; pub use utility::*; @@ -96,6 +98,7 @@ pub fn new() -> Scope { std.def_class::("heading"); std.def_class::>("list"); std.def_class::>("enum"); + std.def_class::("table"); std.def_class::("image"); std.def_class::>("rect"); std.def_class::>("square"); diff --git a/src/library/page.rs b/src/library/page.rs index e2c27a360..f3a287dc9 100644 --- a/src/library/page.rs +++ b/src/library/page.rs @@ -98,27 +98,22 @@ impl PageNode { child = ColumnsNode { columns, child: self.0.clone() }.pack(); } - // Realize margins with padding node. + // Realize margins. child = child.padded(padding); + // Realize background fill. + if let Some(fill) = styles.get(Self::FILL) { + child = child.filled(fill); + } + // Layout the child. let expand = size.map(Length::is_finite); let regions = Regions::repeat(size, size, expand); - let mut frames: Vec<_> = child + child .layout(ctx, ®ions, styles) .into_iter() .map(|c| c.item) - .collect(); - - // Add background fill if requested. - if let Some(fill) = styles.get(Self::FILL) { - for frame in &mut frames { - let shape = Shape::filled(Geometry::Rect(frame.size), fill); - Rc::make_mut(frame).prepend(Point::zero(), Element::Shape(shape)); - } - } - - frames + .collect() } } diff --git a/src/library/sized.rs b/src/library/sized.rs deleted file mode 100644 index 575787173..000000000 --- a/src/library/sized.rs +++ /dev/null @@ -1,80 +0,0 @@ -//! Horizontal and vertical sizing of nodes. - -use super::prelude::*; - -/// Size content and place it into a paragraph. -pub struct BoxNode; - -#[class] -impl BoxNode { - fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult { - let width = args.named("width")?; - let height = args.named("height")?; - let body: PackedNode = args.find().unwrap_or_default(); - Ok(Node::inline(body.sized(Spec::new(width, height)))) - } -} - -/// Place content into a separate flow. -pub struct BlockNode; - -#[class] -impl BlockNode { - fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult { - Ok(Node::Block(args.find().unwrap_or_default())) - } -} - -/// A node that sizes its child. -#[derive(Debug, Hash)] -pub struct SizedNode { - /// How to size the node horizontally and vertically. - pub sizing: Spec>, - /// The node to be sized. - pub child: PackedNode, -} - -impl Layout for SizedNode { - fn layout( - &self, - ctx: &mut LayoutContext, - regions: &Regions, - styles: StyleChain, - ) -> Vec>> { - let is_auto = self.sizing.map_is_none(); - let is_rel = self.sizing.map(|s| s.map_or(false, Linear::is_relative)); - - // The "pod" is the region into which the child will be layouted. - let pod = { - // Resolve the sizing to a concrete size. - let size = self - .sizing - .zip(regions.base) - .map(|(s, b)| s.map(|v| v.resolve(b))) - .unwrap_or(regions.current); - - // Select the appropriate base and expansion for the child depending - // on whether it is automatically or linearly sized. - let base = is_auto.select(regions.base, size); - let expand = regions.expand | !is_auto; - - Regions::one(size, base, expand) - }; - - let mut frames = self.child.layout(ctx, &pod, styles); - let Constrained { item: frame, cts } = &mut frames[0]; - - // Ensure frame size matches regions size if expansion is on. - let target = regions.expand.select(regions.current, frame.size); - Rc::make_mut(frame).resize(target, Align::LEFT_TOP); - - // Set base & exact constraints if the child is automatically sized - // since we don't know what the child might have done. Also set base if - // our sizing is relative. - *cts = Constraints::new(regions.expand); - cts.exact = regions.current.filter(regions.expand | is_auto); - cts.base = regions.base.filter(is_rel | is_auto); - - frames - } -} diff --git a/src/library/table.rs b/src/library/table.rs new file mode 100644 index 000000000..c8e0e17b0 --- /dev/null +++ b/src/library/table.rs @@ -0,0 +1,101 @@ +//! Tabular container. + +use super::prelude::*; +use super::{GridNode, TrackSizing}; + +/// A table of items. +#[derive(Debug, Hash)] +pub struct TableNode { + /// Defines sizing for content rows and columns. + pub tracks: Spec>, + /// Defines sizing of gutter rows and columns between content. + pub gutter: Spec>, + /// The nodes to be arranged in the table. + pub children: Vec, +} + +#[class] +impl TableNode { + /// The primary cell fill color. + pub const PRIMARY: Option = None; + /// The secondary cell fill color. + pub const SECONDARY: Option = None; + /// How the stroke the cells. + pub const STROKE: Option = Some(RgbaColor::BLACK.into()); + /// The stroke's thickness. + pub const THICKNESS: Length = Length::pt(1.0); + /// How much to pad the cells's content. + pub const PADDING: Linear = Length::pt(5.0).into(); + + fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult { + let columns = args.named("columns")?.unwrap_or_default(); + let rows = args.named("rows")?.unwrap_or_default(); + let base_gutter: Vec = args.named("gutter")?.unwrap_or_default(); + let column_gutter = args.named("column-gutter")?; + let row_gutter = args.named("row-gutter")?; + Ok(Node::block(Self { + tracks: Spec::new(columns, rows), + gutter: Spec::new( + column_gutter.unwrap_or_else(|| base_gutter.clone()), + row_gutter.unwrap_or(base_gutter), + ), + children: args.all().collect(), + })) + } + + fn set(args: &mut Args, styles: &mut StyleMap) -> TypResult<()> { + let fill = args.named("fill")?; + styles.set_opt(Self::PRIMARY, args.named("primary")?.or(fill)); + styles.set_opt(Self::SECONDARY, args.named("secondary")?.or(fill)); + styles.set_opt(Self::STROKE, args.named("stroke")?); + styles.set_opt(Self::THICKNESS, args.named("thickness")?); + styles.set_opt(Self::PADDING, args.named("padding")?); + Ok(()) + } +} + +impl Layout for TableNode { + fn layout( + &self, + ctx: &mut LayoutContext, + regions: &Regions, + styles: StyleChain, + ) -> Vec>> { + let primary = styles.get(Self::PRIMARY); + let secondary = styles.get(Self::SECONDARY); + let thickness = styles.get(Self::THICKNESS); + let stroke = styles.get(Self::STROKE).map(|paint| Stroke { paint, thickness }); + let padding = styles.get(Self::PADDING); + + let cols = self.tracks.x.len(); + let children = self + .children + .iter() + .cloned() + .enumerate() + .map(|(i, mut child)| { + child = child.padded(Sides::splat(padding)); + + if let Some(stroke) = stroke { + child = child.stroked(stroke); + } + + let x = i % cols; + let y = i / cols; + if let Some(fill) = [primary, secondary][(x + y) % 2] { + child = child.filled(fill); + } + + child + }) + .collect(); + + let grid = GridNode { + tracks: self.tracks.clone(), + gutter: self.gutter.clone(), + children, + }; + + grid.layout(ctx, regions, styles) + } +} diff --git a/tests/ref/layout/table.png b/tests/ref/layout/table.png new file mode 100644 index 000000000..1a576c350 Binary files /dev/null and b/tests/ref/layout/table.png differ diff --git a/tests/typ/layout/table.typ b/tests/typ/layout/table.typ new file mode 100644 index 000000000..52b6f70f1 --- /dev/null +++ b/tests/typ/layout/table.typ @@ -0,0 +1,9 @@ +#set page(height: 70pt) +#set table(primary: rgb("aaa"), secondary: none) + +#table( + columns: (1fr,) * 3, + stroke: rgb("333"), + thickness: 2pt, + [A], [B], [C], [], [], [D \ E \ F \ \ \ G], [H], +)