From 11e44516fae84f907ea992311fcfdc3636101f14 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Wed, 25 Nov 2020 16:56:29 +0100 Subject: =?UTF-8?q?Merge=20some=20modules=20=F0=9F=A5=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/layout/document.rs | 37 ------ src/layout/graphics.rs | 62 --------- src/layout/mod.rs | 36 +++++- src/layout/node.rs | 12 +- src/layout/pad.rs | 12 +- src/library/align.rs | 185 -------------------------- src/library/boxed.rs | 44 ------- src/library/color.rs | 27 ---- src/library/font.rs | 108 ---------------- src/library/graphics.rs | 42 ------ src/library/insert.rs | 103 +++++++++++++++ src/library/layout.rs | 337 ++++++++++++++++++++++++++++++++++++++++++++++++ src/library/mod.rs | 20 +-- src/library/page.rs | 88 ------------- src/library/spacing.rs | 39 ------ src/library/style.rs | 146 +++++++++++++++++++++ 16 files changed, 636 insertions(+), 662 deletions(-) delete mode 100644 src/layout/document.rs delete mode 100644 src/layout/graphics.rs delete mode 100644 src/library/align.rs delete mode 100644 src/library/boxed.rs delete mode 100644 src/library/color.rs delete mode 100644 src/library/font.rs delete mode 100644 src/library/graphics.rs create mode 100644 src/library/insert.rs create mode 100644 src/library/layout.rs delete mode 100644 src/library/page.rs delete mode 100644 src/library/spacing.rs create mode 100644 src/library/style.rs diff --git a/src/layout/document.rs b/src/layout/document.rs deleted file mode 100644 index 112457d6..00000000 --- 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/graphics.rs b/src/layout/graphics.rs deleted file mode 100644 index 1fa05605..00000000 --- a/src/layout/graphics.rs +++ /dev/null @@ -1,62 +0,0 @@ -use std::fmt::{self, Debug, Formatter}; - -use super::*; - -/// An image node. -#[derive(Clone, PartialEq)] -pub struct Image { - /// The image. - pub buf: RgbaImage, - /// The fixed width, if any. - pub width: Option, - /// The fixed height, if any. - pub height: Option, - /// How to align this image node in its parent. - pub align: BoxAlign, -} - -impl Layout for Image { - fn layout(&self, _: &mut LayoutContext, areas: &Areas) -> Layouted { - let Area { rem, full } = areas.current; - let (pixel_width, pixel_height) = self.buf.dimensions(); - let pixel_ratio = (pixel_width as f64) / (pixel_height as f64); - - let width = self.width.map(|w| w.resolve(full.width)); - let height = self.height.map(|w| w.resolve(full.height)); - - let size = match (width, height) { - (Some(width), Some(height)) => Size::new(width, height), - (Some(width), None) => Size::new(width, width / pixel_ratio), - (None, Some(height)) => Size::new(height * pixel_ratio, height), - (None, None) => { - let ratio = rem.width / rem.height; - if ratio < pixel_ratio { - Size::new(rem.width, rem.width / pixel_ratio) - } else { - // TODO: Fix issue with line spacing. - Size::new(rem.height * pixel_ratio, rem.height) - } - } - }; - - let mut boxed = BoxLayout::new(size); - boxed.push( - Point::ZERO, - LayoutElement::Image(ImageElement { buf: self.buf.clone(), size }), - ); - - Layouted::Layout(boxed, self.align) - } -} - -impl Debug for Image { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.pad("Image") - } -} - -impl From for LayoutNode { - fn from(image: Image) -> Self { - Self::dynamic(image) - } -} diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 2d4553b4..57add044 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 35e742a5..f01ded56 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 09cf016b..00830a07 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 4f4a1750..00000000 --- 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 1ec17d88..00000000 --- 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 17c33806..00000000 --- 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/font.rs b/src/library/font.rs deleted file mode 100644 index 5de0a953..00000000 --- a/src/library/font.rs +++ /dev/null @@ -1,108 +0,0 @@ -use std::rc::Rc; - -use fontdock::{FontStretch, FontStyle, FontWeight}; - -use crate::eval::StringLike; -use crate::geom::Linear; -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). -/// -/// # Keyword arguments -/// - `style` -/// - `normal` -/// - `italic` -/// - `oblique` -/// -/// - `weight` -/// - `thin` or `hairline` (`100`) -/// - `extralight` (`200`) -/// - `light` (`300`) -/// - `regular` (`400`) -/// - `medium` (`500`) -/// - `semibold` (`600`) -/// - `bold` (`700`) -/// - `extrabold` (`800`) -/// - `black` (`900`) -/// - any integer from the range `100` - `900` (inclusive) -/// -/// - `stretch` -/// - `ultra-condensed` -/// - `extra-condensed` -/// - `condensed` -/// - `semi-condensed` -/// - `normal` -/// - `semi-expanded` -/// - `expanded` -/// - `extra-expanded` -/// - `ultra-expanded` -/// -/// - Any other keyword argument whose value is a dictionary of strings defines -/// a fallback class, for example: -/// ```typst -/// [font: serif = ("Source Serif Pro", "Noto Serif")] -/// ``` -/// This class can be used in the fallback list or other fallback classes as -/// long as the resulting fallback tree is acyclic. -/// ```typst -/// [font: "My Serif", serif] -/// ``` -pub fn font(mut args: Args, ctx: &mut EvalContext) -> Value { - let snapshot = ctx.state.clone(); - let body = args.find::(); - - if let Some(linear) = args.find::() { - if linear.is_absolute() { - ctx.state.font.size = linear.abs; - ctx.state.font.scale = Relative::ONE.into(); - } else { - ctx.state.font.scale = linear; - } - } - - let mut needs_flattening = false; - let list: Vec<_> = args.find_all::().map(|s| s.to_lowercase()).collect(); - if !list.is_empty() { - Rc::make_mut(&mut ctx.state.font.families).list = list; - needs_flattening = true; - } - - if let Some(style) = args.get::<_, FontStyle>(ctx, "style") { - ctx.state.font.variant.style = style; - } - - if let Some(weight) = args.get::<_, FontWeight>(ctx, "weight") { - ctx.state.font.variant.weight = weight; - } - - if let Some(stretch) = args.get::<_, FontStretch>(ctx, "stretch") { - ctx.state.font.variant.stretch = stretch; - } - - for (class, dict) in args.find_all_str::>() { - let fallback = Args(dict) - .find_all::() - .map(|s| s.to_lowercase()) - .collect(); - - Rc::make_mut(&mut ctx.state.font.families).update_class_list(class, fallback); - needs_flattening = true; - } - - args.done(ctx); - - if needs_flattening { - Rc::make_mut(&mut ctx.state.font.families).flatten(); - } - - if let Some(body) = body { - body.eval(ctx); - ctx.state = snapshot; - } - - Value::None -} diff --git a/src/library/graphics.rs b/src/library/graphics.rs deleted file mode 100644 index 779d78b5..00000000 --- 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/library/insert.rs b/src/library/insert.rs new file mode 100644 index 00000000..db1e9e17 --- /dev/null +++ b/src/library/insert.rs @@ -0,0 +1,103 @@ +use std::fmt::{self, Debug, Formatter}; +use std::fs::File; +use std::io::BufReader; + +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)] +struct Image { + /// The image. + buf: RgbaImage, + /// The fixed width, if any. + width: Option, + /// The fixed height, if any. + height: Option, + /// How to align this image node in its parent. + align: BoxAlign, +} + +impl Layout for Image { + fn layout(&self, _: &mut LayoutContext, areas: &Areas) -> Layouted { + let Area { rem, full } = areas.current; + let (pixel_width, pixel_height) = self.buf.dimensions(); + let pixel_ratio = (pixel_width as f64) / (pixel_height as f64); + + let width = self.width.map(|w| w.resolve(full.width)); + let height = self.height.map(|w| w.resolve(full.height)); + + let size = match (width, height) { + (Some(width), Some(height)) => Size::new(width, height), + (Some(width), None) => Size::new(width, width / pixel_ratio), + (None, Some(height)) => Size::new(height * pixel_ratio, height), + (None, None) => { + let ratio = rem.width / rem.height; + if ratio < pixel_ratio { + Size::new(rem.width, rem.width / pixel_ratio) + } else { + // TODO: Fix issue with line spacing. + Size::new(rem.height * pixel_ratio, rem.height) + } + } + }; + + let mut boxed = BoxLayout::new(size); + boxed.push( + Point::ZERO, + LayoutElement::Image(ImageElement { buf: self.buf.clone(), size }), + ); + + Layouted::Layout(boxed, self.align) + } +} + +impl Debug for Image { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad("Image") + } +} + +impl From for LayoutNode { + fn from(image: Image) -> Self { + Self::dynamic(image) + } +} diff --git a/src/library/layout.rs b/src/library/layout.rs new file mode 100644 index 00000000..26f94f22 --- /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 e59201dc..bd1feebb 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 05709865..00000000 --- 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 d6d0d7b0..00000000 --- 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/style.rs b/src/library/style.rs new file mode 100644 index 00000000..bb472536 --- /dev/null +++ b/src/library/style.rs @@ -0,0 +1,146 @@ +use std::rc::Rc; + +use fontdock::{FontStretch, FontStyle, FontWeight}; + +use crate::color::RgbaColor; +use crate::eval::StringLike; +use crate::geom::Linear; +use crate::prelude::*; + +/// `font`: Configure the font. +/// +/// # Positional arguments +/// - 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` +/// - `normal` +/// - `italic` +/// - `oblique` +/// +/// - `weight` +/// - `thin` or `hairline` (`100`) +/// - `extralight` (`200`) +/// - `light` (`300`) +/// - `regular` (`400`) +/// - `medium` (`500`) +/// - `semibold` (`600`) +/// - `bold` (`700`) +/// - `extrabold` (`800`) +/// - `black` (`900`) +/// - integer between `100` and `900` +/// +/// - `stretch` +/// - `ultra-condensed` +/// - `extra-condensed` +/// - `condensed` +/// - `semi-condensed` +/// - `normal` +/// - `semi-expanded` +/// - `expanded` +/// - `extra-expanded` +/// - `ultra-expanded` +/// +/// - Any other keyword argument whose value is a dictionary of strings defines +/// a fallback class, for example: +/// ```typst +/// [font: serif = ("Source Serif Pro", "Noto Serif")] +/// ``` +/// This class can be used in the fallback list or other fallback classes as +/// long as the resulting fallback tree is acyclic. +/// ```typst +/// [font: "My Serif", serif] +/// ``` +pub fn font(mut args: Args, ctx: &mut EvalContext) -> Value { + let snapshot = ctx.state.clone(); + let body = args.find::(); + + if let Some(linear) = args.find::() { + if linear.is_absolute() { + ctx.state.font.size = linear.abs; + ctx.state.font.scale = Relative::ONE.into(); + } else { + ctx.state.font.scale = linear; + } + } + + let mut needs_flattening = false; + let list: Vec<_> = args.find_all::().map(|s| s.to_lowercase()).collect(); + if !list.is_empty() { + Rc::make_mut(&mut ctx.state.font.families).list = list; + needs_flattening = true; + } + + if let Some(style) = args.get::<_, FontStyle>(ctx, "style") { + ctx.state.font.variant.style = style; + } + + if let Some(weight) = args.get::<_, FontWeight>(ctx, "weight") { + ctx.state.font.variant.weight = weight; + } + + if let Some(stretch) = args.get::<_, FontStretch>(ctx, "stretch") { + ctx.state.font.variant.stretch = stretch; + } + + for (class, dict) in args.find_all_str::>() { + let fallback = Args(dict) + .find_all::() + .map(|s| s.to_lowercase()) + .collect(); + + Rc::make_mut(&mut ctx.state.font.families).update_class_list(class, fallback); + needs_flattening = true; + } + + args.done(ctx); + + if needs_flattening { + Rc::make_mut(&mut ctx.state.font.families).flatten(); + } + + if let Some(body) = body { + body.eval(ctx); + ctx.state = snapshot; + } + + 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), + )) +} -- cgit v1.2.3