diff --git a/src/layout/document.rs b/src/layout/document.rs deleted file mode 100644 index 112457d67..000000000 --- a/src/layout/document.rs +++ /dev/null @@ -1,37 +0,0 @@ -use super::*; - -/// The top-level layout node. -#[derive(Debug, Clone, PartialEq)] -pub struct Document { - /// The runs of pages with same properties. - pub runs: Vec, -} - -impl Document { - /// Layout the document. - pub fn layout(&self, ctx: &mut LayoutContext) -> Vec { - let mut layouts = vec![]; - for run in &self.runs { - layouts.extend(run.layout(ctx)); - } - layouts - } -} - -/// A variable-length run of pages that all have the same properties. -#[derive(Debug, Clone, PartialEq)] -pub struct Pages { - /// The size of the pages. - pub size: Size, - /// The layout node that produces the actual pages (typically a [`Stack`]). - pub child: LayoutNode, -} - -impl Pages { - /// Layout the page run. - pub fn layout(&self, ctx: &mut LayoutContext) -> Vec { - let areas = Areas::repeat(self.size); - let layouted = self.child.layout(ctx, &areas); - layouted.into_layouts() - } -} diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 2d4553b43..57add0442 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -1,8 +1,6 @@ //! Layouting of documents. -mod document; mod fixed; -mod graphics; mod node; mod pad; mod par; @@ -16,9 +14,7 @@ use crate::font::SharedFontLoader; use crate::geom::*; use crate::shaping::Shaped; -pub use document::*; pub use fixed::*; -pub use graphics::*; pub use node::*; pub use pad::*; pub use par::*; @@ -193,3 +189,35 @@ pub struct ImageElement { /// The document size of the image. pub size: Size, } + +/// The top-level layout node. +#[derive(Debug, Clone, PartialEq)] +pub struct Document { + /// The runs of pages with same properties. + pub runs: Vec, +} + +impl Document { + /// Layout the document. + pub fn layout(&self, ctx: &mut LayoutContext) -> Vec { + self.runs.iter().flat_map(|run| run.layout(ctx)).collect() + } +} + +/// A variable-length run of pages that all have the same properties. +#[derive(Debug, Clone, PartialEq)] +pub struct Pages { + /// The size of each page. + pub size: Size, + /// The layout node that produces the actual pages (typically a [`Stack`]). + pub child: LayoutNode, +} + +impl Pages { + /// Layout the page run. + pub fn layout(&self, ctx: &mut LayoutContext) -> Vec { + let areas = Areas::repeat(self.size); + let layouted = self.child.layout(ctx, &areas); + layouted.into_layouts() + } +} diff --git a/src/layout/node.rs b/src/layout/node.rs index 35e742a5c..f01ded561 100644 --- a/src/layout/node.rs +++ b/src/layout/node.rs @@ -74,12 +74,6 @@ 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,6 +86,12 @@ 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 09cf016b0..00830a070 100644 --- a/src/layout/pad.rs +++ b/src/layout/pad.rs @@ -29,6 +29,12 @@ impl Layout for Pad { } } +impl From for LayoutNode { + fn from(pad: Pad) -> Self { + Self::dynamic(pad) + } +} + /// Shrink all areas by the padding. fn shrink_areas(areas: &Areas, padding: Sides) -> Areas { let shrink = |size| size - padding.resolve(size).size(); @@ -52,9 +58,3 @@ fn pad_layout(layout: &mut BoxLayout, padding: Sides) { *point += origin; } } - -impl From for LayoutNode { - fn from(pad: Pad) -> Self { - Self::dynamic(pad) - } -} diff --git a/src/library/align.rs b/src/library/align.rs deleted file mode 100644 index 4f4a17500..000000000 --- a/src/library/align.rs +++ /dev/null @@ -1,185 +0,0 @@ -use crate::prelude::*; -use std::fmt::{self, Display, Formatter}; - -/// `align`: Align content along the layouting axes. -/// -/// # Positional arguments -/// - At most two of `left`, `right`, `top`, `bottom`, `center`. -/// -/// When `center` is used as a positional argument, it is automatically inferred -/// which axis it should apply to depending on further arguments, defaulting -/// to the axis, text is set along. -/// -/// # Keyword arguments -/// - `horizontal`: Any of `left`, `right` or `center`. -/// - `vertical`: Any of `top`, `bottom` or `center`. -/// -/// There may not be two alignment specifications for the same axis. -pub fn align(mut args: Args, ctx: &mut EvalContext) -> Value { - let snapshot = ctx.state.clone(); - let body = args.find::(); - let first = args.get::<_, Spanned>(ctx, 0); - let second = args.get::<_, Spanned>(ctx, 1); - let hor = args.get::<_, Spanned>(ctx, "horizontal"); - let ver = args.get::<_, Spanned>(ctx, "vertical"); - args.done(ctx); - - let iter = first - .into_iter() - .chain(second.into_iter()) - .map(|align| (align.v.axis(), align)) - .chain(hor.into_iter().map(|align| (Some(SpecAxis::Horizontal), align))) - .chain(ver.into_iter().map(|align| (Some(SpecAxis::Vertical), align))); - - let align = dedup_aligns(ctx, iter); - let ends_par = align.main != ctx.state.align.main; - ctx.state.align = align; - - if ends_par { - ctx.end_par_group(); - ctx.start_par_group(); - } - - if let Some(body) = body { - body.eval(ctx); - ctx.state = snapshot; - } - - Value::None -} - -/// Deduplicate alignments and deduce to which axes they apply. -fn dedup_aligns( - ctx: &mut EvalContext, - iter: impl Iterator, Spanned)>, -) -> BoxAlign { - let mut alignments = ctx.state.align; - let mut had = Gen::uniform(false); - let mut had_center = false; - - for (axis, Spanned { v: align, span }) in iter { - // Check whether we know which axis this alignment belongs to. - if let Some(axis) = axis { - // We know the axis. - let gen_axis = axis.switch(ctx.state.flow); - let gen_align = align.switch(ctx.state.flow); - - if align.axis().map_or(false, |a| a != axis) { - ctx.diag(error!( - span, - "invalid alignment `{}` for {} axis", align, axis, - )); - } else if had.get(gen_axis) { - ctx.diag(error!(span, "duplicate alignment for {} axis", axis)); - } else { - *alignments.get_mut(gen_axis) = gen_align; - *had.get_mut(gen_axis) = true; - } - } else { - // We don't know the axis: This has to be a `center` alignment for a - // positional argument. - debug_assert_eq!(align, AlignArg::Center); - - if had.main && had.cross { - ctx.diag(error!(span, "duplicate alignment")); - } else if had_center { - // Both this and the previous one are unspecified `center` - // alignments. Both axes should be centered. - alignments = BoxAlign::new(Align::Center, Align::Center); - had = Gen::uniform(true); - } else { - had_center = true; - } - } - - // If we we know one alignment, we can handle the unspecified `center` - // alignment. - if had_center && (had.main || had.cross) { - if had.main { - alignments.cross = Align::Center; - had.cross = true; - } else { - alignments.main = Align::Center; - had.main = true; - } - had_center = false; - } - } - - // If center has not been flushed by now, it is the only argument and then - // we default to applying it to the cross axis. - if had_center { - alignments.cross = Align::Center; - } - - alignments -} - -/// An alignment argument. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] -enum AlignArg { - Left, - Right, - Top, - Bottom, - Center, -} - -impl AlignArg { - /// The specific axis this alignment refers to. - /// - /// Returns `None` if this is `Center` since the axis is unknown. - pub fn axis(self) -> Option { - match self { - Self::Left => Some(SpecAxis::Horizontal), - Self::Right => Some(SpecAxis::Horizontal), - Self::Top => Some(SpecAxis::Vertical), - Self::Bottom => Some(SpecAxis::Vertical), - Self::Center => None, - } - } -} - -impl Switch for AlignArg { - type Other = Align; - - fn switch(self, flow: Flow) -> Self::Other { - let get = |dir: Dir, at_positive_start| { - if dir.is_positive() == at_positive_start { - Align::Start - } else { - Align::End - } - }; - - let flow = flow.switch(flow); - match self { - Self::Left => get(flow.horizontal, true), - Self::Right => get(flow.horizontal, false), - Self::Top => get(flow.vertical, true), - Self::Bottom => get(flow.vertical, false), - Self::Center => Align::Center, - } - } -} - -convert_ident!(AlignArg, "alignment", |v| match v { - "left" => Some(Self::Left), - "right" => Some(Self::Right), - "top" => Some(Self::Top), - "bottom" => Some(Self::Bottom), - "center" => Some(Self::Center), - _ => None, -}); - -impl Display for AlignArg { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.pad(match self { - Self::Left => "left", - Self::Right => "right", - Self::Top => "top", - Self::Bottom => "bottom", - Self::Center => "center", - }) - } -} diff --git a/src/library/boxed.rs b/src/library/boxed.rs deleted file mode 100644 index 1ec17d880..000000000 --- a/src/library/boxed.rs +++ /dev/null @@ -1,44 +0,0 @@ -use crate::geom::Linear; -use crate::layout::{Expansion, Fixed, Stack}; -use crate::prelude::*; - -/// `box`: Layouts its contents into a box. -/// -/// # Keyword arguments -/// - `width`: The width of the box (length or relative to parent's width). -/// - `height`: The height of the box (length or relative to parent's height). -pub fn boxed(mut args: Args, ctx: &mut EvalContext) -> Value { - let snapshot = ctx.state.clone(); - let body = args.find::().unwrap_or_default(); - let width = args.get::<_, Linear>(ctx, "width"); - let height = args.get::<_, Linear>(ctx, "height"); - let main = args.get::<_, Spanned>(ctx, "main"); - let cross = args.get::<_, Spanned>(ctx, "cross"); - ctx.set_flow(Gen::new(main, cross)); - args.done(ctx); - - let flow = ctx.state.flow; - let align = ctx.state.align; - - ctx.start_content_group(); - body.eval(ctx); - let children = ctx.end_content_group(); - - ctx.push(Fixed { - width, - height, - child: LayoutNode::dynamic(Stack { - flow, - align, - expansion: Spec::new( - Expansion::fill_if(width.is_some()), - Expansion::fill_if(height.is_some()), - ) - .switch(flow), - children, - }), - }); - - ctx.state = snapshot; - Value::None -} diff --git a/src/library/color.rs b/src/library/color.rs deleted file mode 100644 index 17c33806f..000000000 --- a/src/library/color.rs +++ /dev/null @@ -1,27 +0,0 @@ -use crate::color::RgbaColor; -use crate::prelude::*; - -/// `rgb`: Create an RGB(A) color. -pub fn rgb(mut args: Args, ctx: &mut EvalContext) -> Value { - let r = args.need::<_, Spanned>(ctx, 0, "red value"); - let g = args.need::<_, Spanned>(ctx, 1, "green value"); - let b = args.need::<_, Spanned>(ctx, 2, "blue value"); - let a = args.get::<_, Spanned>(ctx, 3); - args.done(ctx); - - let mut clamp = |component: Option>, default| { - component.map_or(default, |c| { - if c.v < 0 || c.v > 255 { - ctx.diag(error!(c.span, "should be between 0 and 255")); - } - c.v.max(0).min(255) as u8 - }) - }; - - Value::Color(RgbaColor::new( - clamp(r, 0), - clamp(g, 0), - clamp(b, 0), - clamp(a, 255), - )) -} diff --git a/src/library/graphics.rs b/src/library/graphics.rs deleted file mode 100644 index 779d78b53..000000000 --- a/src/library/graphics.rs +++ /dev/null @@ -1,42 +0,0 @@ -use std::fs::File; -use std::io::BufReader; - -use image::io::Reader; - -use crate::layout::Image; -use crate::prelude::*; - -/// `image`: Include an image. -/// -/// # Positional arguments -/// - The path to the image (string) -pub fn image(mut args: Args, ctx: &mut EvalContext) -> Value { - let path = args.need::<_, Spanned>(ctx, 0, "path"); - let width = args.get::<_, Linear>(ctx, "width"); - let height = args.get::<_, Linear>(ctx, "height"); - - if let Some(path) = path { - if let Ok(file) = File::open(path.v) { - match Reader::new(BufReader::new(file)) - .with_guessed_format() - .map_err(|err| err.into()) - .and_then(|reader| reader.decode()) - .map(|img| img.into_rgba8()) - { - Ok(buf) => { - ctx.push(Image { - buf, - width, - height, - align: ctx.state.align, - }); - } - Err(err) => ctx.diag(error!(path.span, "invalid image: {}", err)), - } - } else { - ctx.diag(error!(path.span, "failed to open image file")); - } - } - - Value::None -} diff --git a/src/layout/graphics.rs b/src/library/insert.rs similarity index 55% rename from src/layout/graphics.rs rename to src/library/insert.rs index 1fa05605c..db1e9e170 100644 --- a/src/layout/graphics.rs +++ b/src/library/insert.rs @@ -1,18 +1,59 @@ use std::fmt::{self, Debug, Formatter}; +use std::fs::File; +use std::io::BufReader; -use super::*; +use image::io::Reader; +use image::RgbaImage; + +use crate::layout::*; +use crate::prelude::*; + +/// `image`: Insert an image. +/// +/// # Positional arguments +/// - The path to the image (string) +pub fn image(mut args: Args, ctx: &mut EvalContext) -> Value { + let path = args.need::<_, Spanned>(ctx, 0, "path"); + let width = args.get::<_, Linear>(ctx, "width"); + let height = args.get::<_, Linear>(ctx, "height"); + + if let Some(path) = path { + if let Ok(file) = File::open(path.v) { + match Reader::new(BufReader::new(file)) + .with_guessed_format() + .map_err(|err| err.into()) + .and_then(|reader| reader.decode()) + .map(|img| img.into_rgba8()) + { + Ok(buf) => { + ctx.push(Image { + buf, + width, + height, + align: ctx.state.align, + }); + } + Err(err) => ctx.diag(error!(path.span, "invalid image: {}", err)), + } + } else { + ctx.diag(error!(path.span, "failed to open image file")); + } + } + + Value::None +} /// An image node. #[derive(Clone, PartialEq)] -pub struct Image { +struct Image { /// The image. - pub buf: RgbaImage, + buf: RgbaImage, /// The fixed width, if any. - pub width: Option, + width: Option, /// The fixed height, if any. - pub height: Option, + height: Option, /// How to align this image node in its parent. - pub align: BoxAlign, + align: BoxAlign, } impl Layout for Image { diff --git a/src/library/layout.rs b/src/library/layout.rs new file mode 100644 index 000000000..26f94f229 --- /dev/null +++ b/src/library/layout.rs @@ -0,0 +1,337 @@ +use std::fmt::{self, Display, Formatter}; + +use crate::geom::{Length, Linear}; +use crate::layout::{Expansion, Fixed, Softness, Spacing, Stack}; +use crate::paper::{Paper, PaperClass}; +use crate::prelude::*; + +/// `align`: Align content along the layouting axes. +/// +/// # Positional arguments +/// - At most two of `left`, `right`, `top`, `bottom`, `center`. +/// +/// When `center` is used as a positional argument, it is automatically inferred +/// which axis it should apply to depending on further arguments, defaulting +/// to the cross axis. +/// +/// # Keyword arguments +/// - `horizontal`: Any of `left`, `right` or `center`. +/// - `vertical`: Any of `top`, `bottom` or `center`. +pub fn align(mut args: Args, ctx: &mut EvalContext) -> Value { + let snapshot = ctx.state.clone(); + let body = args.find::(); + let first = args.get::<_, Spanned>(ctx, 0); + let second = args.get::<_, Spanned>(ctx, 1); + let hor = args.get::<_, Spanned>(ctx, "horizontal"); + let ver = args.get::<_, Spanned>(ctx, "vertical"); + args.done(ctx); + + let prev_main = ctx.state.align.main; + let mut had = Gen::uniform(false); + let mut had_center = false; + + for (axis, Spanned { v: arg, span }) in first + .into_iter() + .chain(second.into_iter()) + .map(|arg| (arg.v.axis(), arg)) + .chain(hor.into_iter().map(|arg| (Some(SpecAxis::Horizontal), arg))) + .chain(ver.into_iter().map(|arg| (Some(SpecAxis::Vertical), arg))) + { + // Check whether we know which axis this alignment belongs to. + if let Some(axis) = axis { + // We know the axis. + let gen_axis = axis.switch(ctx.state.flow); + let gen_align = arg.switch(ctx.state.flow); + + if arg.axis().map_or(false, |a| a != axis) { + ctx.diag(error!( + span, + "invalid alignment `{}` for {} axis", arg, axis, + )); + } else if had.get(gen_axis) { + ctx.diag(error!(span, "duplicate alignment for {} axis", axis)); + } else { + *ctx.state.align.get_mut(gen_axis) = gen_align; + *had.get_mut(gen_axis) = true; + } + } else { + // We don't know the axis: This has to be a `center` alignment for a + // positional argument. + debug_assert_eq!(arg, AlignArg::Center); + + if had.main && had.cross { + ctx.diag(error!(span, "duplicate alignment")); + } else if had_center { + // Both this and the previous one are unspecified `center` + // alignments. Both axes should be centered. + ctx.state.align.main = Align::Center; + ctx.state.align.cross = Align::Center; + had = Gen::uniform(true); + } else { + had_center = true; + } + } + + // If we we know the other alignment, we can handle the unspecified + // `center` alignment. + if had_center && (had.main || had.cross) { + if had.main { + ctx.state.align.cross = Align::Center; + had.cross = true; + } else { + ctx.state.align.main = Align::Center; + had.main = true; + } + had_center = false; + } + } + + // If `had_center` wasn't flushed by now, it's the only argument and then we + // default to applying it to the cross axis. + if had_center { + ctx.state.align.cross = Align::Center; + } + + if ctx.state.align.main != prev_main { + ctx.end_par_group(); + ctx.start_par_group(); + } + + if let Some(body) = body { + body.eval(ctx); + ctx.state = snapshot; + } + + Value::None +} + +/// An argument to `[align]`. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] +enum AlignArg { + Left, + Right, + Top, + Bottom, + Center, +} + +convert_ident!(AlignArg, "alignment", |v| match v { + "left" => Some(Self::Left), + "right" => Some(Self::Right), + "top" => Some(Self::Top), + "bottom" => Some(Self::Bottom), + "center" => Some(Self::Center), + _ => None, +}); + +impl AlignArg { + /// The specific axis this alignment refers to. + /// + /// Returns `None` if this is `Center` since the axis is unknown. + pub fn axis(self) -> Option { + match self { + Self::Left => Some(SpecAxis::Horizontal), + Self::Right => Some(SpecAxis::Horizontal), + Self::Top => Some(SpecAxis::Vertical), + Self::Bottom => Some(SpecAxis::Vertical), + Self::Center => None, + } + } +} + +impl Switch for AlignArg { + type Other = Align; + + fn switch(self, flow: Flow) -> Self::Other { + let get = |dir: Dir, at_positive_start| { + if dir.is_positive() == at_positive_start { + Align::Start + } else { + Align::End + } + }; + + let flow = flow.switch(flow); + match self { + Self::Left => get(flow.horizontal, true), + Self::Right => get(flow.horizontal, false), + Self::Top => get(flow.vertical, true), + Self::Bottom => get(flow.vertical, false), + Self::Center => Align::Center, + } + } +} + +impl Display for AlignArg { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad(match self { + Self::Left => "left", + Self::Right => "right", + Self::Top => "top", + Self::Bottom => "bottom", + Self::Center => "center", + }) + } +} + +/// `box`: Layout content into a box. +/// +/// # Keyword arguments +/// - `width`: The width of the box (length or relative to parent's width). +/// - `height`: The height of the box (length or relative to parent's height). +pub fn boxed(mut args: Args, ctx: &mut EvalContext) -> Value { + let snapshot = ctx.state.clone(); + let body = args.find::().unwrap_or_default(); + let width = args.get::<_, Linear>(ctx, "width"); + let height = args.get::<_, Linear>(ctx, "height"); + let main = args.get::<_, Spanned>(ctx, "main"); + let cross = args.get::<_, Spanned>(ctx, "cross"); + ctx.set_flow(Gen::new(main, cross)); + args.done(ctx); + + let flow = ctx.state.flow; + let align = ctx.state.align; + + ctx.start_content_group(); + body.eval(ctx); + let children = ctx.end_content_group(); + + ctx.push(Fixed { + width, + height, + child: LayoutNode::dynamic(Stack { + flow, + align, + expansion: Spec::new( + Expansion::fill_if(width.is_some()), + Expansion::fill_if(height.is_some()), + ) + .switch(flow), + children, + }), + }); + + ctx.state = snapshot; + Value::None +} + +/// `h`: Add horizontal spacing. +/// +/// # Positional arguments +/// - The spacing (length or relative to font size). +pub fn h(args: Args, ctx: &mut EvalContext) -> Value { + spacing(args, ctx, SpecAxis::Horizontal) +} + +/// `v`: Add vertical spacing. +/// +/// # Positional arguments +/// - The spacing (length or relative to font size). +pub fn v(args: Args, ctx: &mut EvalContext) -> Value { + spacing(args, ctx, SpecAxis::Vertical) +} + +/// Apply spacing along a specific axis. +fn spacing(mut args: Args, ctx: &mut EvalContext, axis: SpecAxis) -> Value { + let spacing = args.need::<_, Linear>(ctx, 0, "spacing"); + args.done(ctx); + + if let Some(linear) = spacing { + let amount = linear.resolve(ctx.state.font.font_size()); + let spacing = Spacing { amount, softness: Softness::Hard }; + if ctx.state.flow.main.axis() == axis { + ctx.end_par_group(); + ctx.push(spacing); + ctx.start_par_group(); + } else { + ctx.push(spacing); + } + } + + Value::None +} + +/// `page`: Configure pages. +/// +/// # Positional arguments +/// - The name of a paper, e.g. `a4` (optional). +/// +/// # Keyword arguments +/// - `width`: The width of pages (length). +/// - `height`: The height of pages (length). +/// - `margins`: The margins for all sides (length or relative to side lengths). +/// - `left`: The left margin (length or relative to width). +/// - `right`: The right margin (length or relative to width). +/// - `top`: The top margin (length or relative to height). +/// - `bottom`: The bottom margin (length or relative to height). +/// - `flip`: Flips custom or paper-defined width and height (boolean). +pub fn page(mut args: Args, ctx: &mut EvalContext) -> Value { + let snapshot = ctx.state.clone(); + let body = args.find::(); + + if let Some(paper) = args.find::() { + ctx.state.page.class = paper.class; + ctx.state.page.size = paper.size(); + } + + if let Some(width) = args.get::<_, Length>(ctx, "width") { + ctx.state.page.class = PaperClass::Custom; + ctx.state.page.size.width = width; + } + + if let Some(height) = args.get::<_, Length>(ctx, "height") { + ctx.state.page.class = PaperClass::Custom; + ctx.state.page.size.height = height; + } + + if let Some(margins) = args.get::<_, Linear>(ctx, "margins") { + ctx.state.page.margins = Sides::uniform(Some(margins)); + } + + if let Some(left) = args.get::<_, Linear>(ctx, "left") { + ctx.state.page.margins.left = Some(left); + } + + if let Some(top) = args.get::<_, Linear>(ctx, "top") { + ctx.state.page.margins.top = Some(top); + } + + if let Some(right) = args.get::<_, Linear>(ctx, "right") { + ctx.state.page.margins.right = Some(right); + } + + if let Some(bottom) = args.get::<_, Linear>(ctx, "bottom") { + ctx.state.page.margins.bottom = Some(bottom); + } + + if args.get::<_, bool>(ctx, "flip").unwrap_or(false) { + let size = &mut ctx.state.page.size; + std::mem::swap(&mut size.width, &mut size.height); + } + + let main = args.get::<_, Spanned>(ctx, "main"); + let cross = args.get::<_, Spanned>(ctx, "cross"); + ctx.set_flow(Gen::new(main, cross)); + + args.done(ctx); + + if let Some(body) = body { + ctx.end_page_group(); + ctx.start_page_group(true); + body.eval(ctx); + ctx.state = snapshot; + } + + ctx.end_page_group(); + ctx.start_page_group(false); + + Value::None +} + +/// `pagebreak`: Start a new page. +pub fn pagebreak(args: Args, ctx: &mut EvalContext) -> Value { + args.done(ctx); + ctx.end_page_group(); + ctx.start_page_group(true); + Value::None +} diff --git a/src/library/mod.rs b/src/library/mod.rs index e59201dc9..bd1feebb9 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -1,20 +1,12 @@ //! The standard library. -mod align; -mod boxed; -mod color; -mod font; -mod graphics; -mod page; -mod spacing; +mod insert; +mod layout; +mod style; -pub use align::*; -pub use boxed::*; -pub use color::*; -pub use font::*; -pub use graphics::*; -pub use page::*; -pub use spacing::*; +pub use insert::*; +pub use layout::*; +pub use style::*; use crate::eval::{Scope, ValueFunc}; diff --git a/src/library/page.rs b/src/library/page.rs deleted file mode 100644 index 057098650..000000000 --- a/src/library/page.rs +++ /dev/null @@ -1,88 +0,0 @@ -use crate::geom::{Length, Linear}; -use crate::paper::{Paper, PaperClass}; -use crate::prelude::*; - -/// `page`: Configure pages. -/// -/// # Positional arguments -/// - The name of a paper, e.g. `a4` (optional). -/// -/// # Keyword arguments -/// - `width`: The width of pages (length). -/// - `height`: The height of pages (length). -/// - `margins`: The margins for all sides (length or relative to side lengths). -/// - `left`: The left margin (length or relative to width). -/// - `right`: The right margin (length or relative to width). -/// - `top`: The top margin (length or relative to height). -/// - `bottom`: The bottom margin (length or relative to height). -/// - `flip`: Flips custom or paper-defined width and height (boolean). -pub fn page(mut args: Args, ctx: &mut EvalContext) -> Value { - let snapshot = ctx.state.clone(); - let body = args.find::(); - - if let Some(paper) = args.find::() { - ctx.state.page.class = paper.class; - ctx.state.page.size = paper.size(); - } - - if let Some(width) = args.get::<_, Length>(ctx, "width") { - ctx.state.page.class = PaperClass::Custom; - ctx.state.page.size.width = width; - } - - if let Some(height) = args.get::<_, Length>(ctx, "height") { - ctx.state.page.class = PaperClass::Custom; - ctx.state.page.size.height = height; - } - - if let Some(margins) = args.get::<_, Linear>(ctx, "margins") { - ctx.state.page.margins = Sides::uniform(Some(margins)); - } - - if let Some(left) = args.get::<_, Linear>(ctx, "left") { - ctx.state.page.margins.left = Some(left); - } - - if let Some(top) = args.get::<_, Linear>(ctx, "top") { - ctx.state.page.margins.top = Some(top); - } - - if let Some(right) = args.get::<_, Linear>(ctx, "right") { - ctx.state.page.margins.right = Some(right); - } - - if let Some(bottom) = args.get::<_, Linear>(ctx, "bottom") { - ctx.state.page.margins.bottom = Some(bottom); - } - - if args.get::<_, bool>(ctx, "flip").unwrap_or(false) { - let size = &mut ctx.state.page.size; - std::mem::swap(&mut size.width, &mut size.height); - } - - let main = args.get::<_, Spanned>(ctx, "main"); - let cross = args.get::<_, Spanned>(ctx, "cross"); - ctx.set_flow(Gen::new(main, cross)); - - args.done(ctx); - - if let Some(body) = body { - ctx.end_page_group(); - ctx.start_page_group(true); - body.eval(ctx); - ctx.state = snapshot; - } - - ctx.end_page_group(); - ctx.start_page_group(false); - - Value::None -} - -/// `pagebreak`: Starts a new page. -pub fn pagebreak(args: Args, ctx: &mut EvalContext) -> Value { - args.done(ctx); - ctx.end_page_group(); - ctx.start_page_group(true); - Value::None -} diff --git a/src/library/spacing.rs b/src/library/spacing.rs deleted file mode 100644 index d6d0d7b0e..000000000 --- a/src/library/spacing.rs +++ /dev/null @@ -1,39 +0,0 @@ -use crate::geom::Linear; -use crate::layout::{Softness, Spacing}; -use crate::prelude::*; - -/// `h`: Add horizontal spacing. -/// -/// # Positional arguments -/// - The spacing (length or relative to font size). -pub fn h(args: Args, ctx: &mut EvalContext) -> Value { - spacing(args, ctx, SpecAxis::Horizontal) -} - -/// `v`: Add vertical spacing. -/// -/// # Positional arguments -/// - The spacing (length or relative to font size). -pub fn v(args: Args, ctx: &mut EvalContext) -> Value { - spacing(args, ctx, SpecAxis::Vertical) -} - -/// Apply spacing along a specific axis. -fn spacing(mut args: Args, ctx: &mut EvalContext, axis: SpecAxis) -> Value { - let spacing = args.need::<_, Linear>(ctx, 0, "spacing"); - args.done(ctx); - - if let Some(linear) = spacing { - let amount = linear.resolve(ctx.state.font.font_size()); - let spacing = Spacing { amount, softness: Softness::Hard }; - if ctx.state.flow.main.axis() == axis { - ctx.end_par_group(); - ctx.push(spacing); - ctx.start_par_group(); - } else { - ctx.push(spacing); - } - } - - Value::None -} diff --git a/src/library/font.rs b/src/library/style.rs similarity index 67% rename from src/library/font.rs rename to src/library/style.rs index 5de0a953c..bb4725365 100644 --- a/src/library/font.rs +++ b/src/library/style.rs @@ -2,6 +2,7 @@ use std::rc::Rc; use fontdock::{FontStretch, FontStyle, FontWeight}; +use crate::color::RgbaColor; use crate::eval::StringLike; use crate::geom::Linear; use crate::prelude::*; @@ -9,8 +10,14 @@ use crate::prelude::*; /// `font`: Configure the font. /// /// # Positional arguments -/// - The font size (optional, length or relative to previous font size). -/// - A font family fallback list (optional, identifiers or strings). +/// - The font size (optional, length or relative to current font size). +/// - All identifier and string arguments are interpreted as an ordered list of +/// fallback font families. +/// +/// An example invocation could look like this: +/// ```typst +/// [font: 12pt, Arial, "Noto Sans", sans-serif] +/// ``` /// /// # Keyword arguments /// - `style` @@ -28,7 +35,7 @@ use crate::prelude::*; /// - `bold` (`700`) /// - `extrabold` (`800`) /// - `black` (`900`) -/// - any integer from the range `100` - `900` (inclusive) +/// - integer between `100` and `900` /// /// - `stretch` /// - `ultra-condensed` @@ -106,3 +113,34 @@ pub fn font(mut args: Args, ctx: &mut EvalContext) -> Value { Value::None } + +/// `rgb`: Create an RGB(A) color. +/// +/// # Positional arguments +/// - The red component (integer between 0 and 255). +/// - The green component (integer between 0 and 255). +/// - The blue component (integer between 0 and 255). +/// - The alpha component (optional, integer between 0 and 255). +pub fn rgb(mut args: Args, ctx: &mut EvalContext) -> Value { + let r = args.need::<_, Spanned>(ctx, 0, "red value"); + let g = args.need::<_, Spanned>(ctx, 1, "green value"); + let b = args.need::<_, Spanned>(ctx, 2, "blue value"); + let a = args.get::<_, Spanned>(ctx, 3); + args.done(ctx); + + let mut clamp = |component: Option>, default| { + component.map_or(default, |c| { + if c.v < 0 || c.v > 255 { + ctx.diag(error!(c.span, "should be between 0 and 255")); + } + c.v.max(0).min(255) as u8 + }) + }; + + Value::Color(RgbaColor::new( + clamp(r, 0), + clamp(g, 0), + clamp(b, 0), + clamp(a, 255), + )) +}