use std::fmt::{self, Display, Formatter}; use crate::eval::Softness; use crate::layout::{Expansion, NodeFixed, NodeSpacing, NodeStack}; use crate::paper::{Paper, PaperClass}; use crate::prelude::*; /// `align`: Align content along the layouting axes. /// /// Which axis an alignment should apply to (main or cross) is inferred from /// either the argument itself (for anything other than `center`) or from the /// second argument if present, defaulting to the cross axis for a single /// `center` alignment. /// /// # Positional arguments /// - Alignments: variadic, of type `alignment`. /// /// # Named arguments /// - Horizontal alignment: `horizontal`, of type `alignment`. /// - Vertical alignment: `vertical`, of type `alignment`. /// /// # Relevant types and constants /// - Type `alignment` /// - `left` /// - `right` /// - `top` /// - `bottom` /// - `center` pub fn align(ctx: &mut EvalContext, args: &mut Args) -> Value { let snapshot = ctx.state.clone(); let first = args.find(ctx); let second = args.find(ctx); let hor = args.get(ctx, "horizontal"); let ver = args.get(ctx, "vertical"); 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: Spanned| (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.dirs); let gen_align = arg.switch(ctx.state.dirs); if arg.axis().map_or(false, |a| a != axis) { ctx.diag(error!(span, "invalid alignment for {} axis", 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, Alignment::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 != snapshot.align.main { ctx.end_par_group(); ctx.start_par_group(); } if let Some(body) = args.find::(ctx) { body.eval(ctx); ctx.state = snapshot; } Value::None } #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] pub(crate) enum Alignment { Left, Center, Right, Top, Bottom, } impl Alignment { /// The specific axis this alignment refers to. 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 Alignment { type Other = Align; fn switch(self, dirs: LayoutDirs) -> Self::Other { let get = |dir: Dir, at_positive_start| { if dir.is_positive() == at_positive_start { Align::Start } else { Align::End } }; let dirs = dirs.switch(dirs); match self { Self::Left => get(dirs.horizontal, true), Self::Right => get(dirs.horizontal, false), Self::Top => get(dirs.vertical, true), Self::Bottom => get(dirs.vertical, false), Self::Center => Align::Center, } } } impl_type! { Alignment: "alignment", } impl Display for Alignment { fn fmt(&self, f: &mut Formatter) -> fmt::Result { f.pad(match self { Self::Left => "left", Self::Center => "center", Self::Right => "right", Self::Top => "top", Self::Bottom => "bottom", }) } } /// `box`: Layout content into a box. /// /// # Named arguments /// - Width of the box: `width`, of type `linear` relative to parent width. /// - Height of the box: `height`, of type `linear` relative to parent height. pub fn box_(ctx: &mut EvalContext, args: &mut Args) -> Value { let snapshot = ctx.state.clone(); let width = args.get(ctx, "width"); let height = args.get(ctx, "height"); let main = args.get(ctx, "main-dir"); let cross = args.get(ctx, "cross-dir"); ctx.set_dirs(Gen::new(main, cross)); let dirs = ctx.state.dirs; let align = ctx.state.align; ctx.start_content_group(); if let Some(body) = args.find::(ctx) { body.eval(ctx); } let children = ctx.end_content_group(); let fill_if = |c| if c { Expansion::Fill } else { Expansion::Fit }; let expand = Spec::new(fill_if(width.is_some()), fill_if(height.is_some())); ctx.push(NodeFixed { width, height, child: NodeStack { dirs, align, expand, children }.into(), }); ctx.state = snapshot; Value::None } impl_type! { Dir: "direction" } /// `h`: Add horizontal spacing. /// /// # Positional arguments /// - Amount of spacing: of type `linear` relative to current font size. pub fn h(ctx: &mut EvalContext, args: &mut Args) -> Value { spacing(ctx, args, SpecAxis::Horizontal) } /// `v`: Add vertical spacing. /// /// # Positional arguments /// - Amount of spacing: of type `linear` relative to current font size. pub fn v(ctx: &mut EvalContext, args: &mut Args) -> Value { spacing(ctx, args, SpecAxis::Vertical) } /// Apply spacing along a specific axis. fn spacing(ctx: &mut EvalContext, args: &mut Args, axis: SpecAxis) -> Value { let spacing: Option = args.require(ctx, "spacing"); if let Some(linear) = spacing { let amount = linear.resolve(ctx.state.font.font_size()); let spacing = NodeSpacing { amount, softness: Softness::Hard }; if axis == ctx.state.dirs.main.axis() { ctx.end_par_group(); ctx.push(spacing); ctx.start_par_group(); } else { ctx.push(spacing); } } Value::None } /// `page`: Configure pages. /// /// # Positional arguments /// - Paper name: optional, of type `string`, see [here](crate::paper) for a /// full list of all paper names. /// /// # Named arguments /// - Width of the page: `width`, of type `length`. /// - Height of the page: `height`, of type `length`. /// - Margins for all sides: `margins`, of type `linear` relative to sides. /// - Left margin: `left`, of type `linear` relative to width. /// - Right margin: `right`, of type `linear` relative to width. /// - Top margin: `top`, of type `linear` relative to height. /// - Bottom margin: `bottom`, of type `linear` relative to height. /// - Flip width and height: `flip`, of type `bool`. pub fn page(ctx: &mut EvalContext, args: &mut Args) -> Value { let snapshot = ctx.state.clone(); if let Some(name) = args.find::>(ctx) { if let Some(paper) = Paper::from_name(&name.v) { ctx.state.page.class = paper.class; ctx.state.page.size = paper.size(); ctx.state.page.expand = Spec::uniform(Expansion::Fill); } else { ctx.diag(error!(name.span, "invalid paper name")); } } if let Some(width) = args.get(ctx, "width") { ctx.state.page.class = PaperClass::Custom; ctx.state.page.size.width = width; ctx.state.page.expand.horizontal = Expansion::Fill; } if let Some(height) = args.get(ctx, "height") { ctx.state.page.class = PaperClass::Custom; ctx.state.page.size.height = height; ctx.state.page.expand.vertical = Expansion::Fill; } if let Some(margins) = args.get(ctx, "margins") { ctx.state.page.margins = Sides::uniform(Some(margins)); } if let Some(left) = args.get(ctx, "left") { ctx.state.page.margins.left = Some(left); } if let Some(top) = args.get(ctx, "top") { ctx.state.page.margins.top = Some(top); } if let Some(right) = args.get(ctx, "right") { ctx.state.page.margins.right = Some(right); } if let Some(bottom) = args.get(ctx, "bottom") { ctx.state.page.margins.bottom = Some(bottom); } if args.get(ctx, "flip").unwrap_or(false) { let page = &mut ctx.state.page; std::mem::swap(&mut page.size.width, &mut page.size.height); std::mem::swap(&mut page.expand.horizontal, &mut page.expand.vertical); } let main = args.get(ctx, "main-dir"); let cross = args.get(ctx, "cross-dir"); ctx.set_dirs(Gen::new(main, cross)); let mut softness = ctx.end_page_group(|_| false); if let Some(body) = args.find::(ctx) { // TODO: Restrict body to a single page? ctx.start_page_group(Softness::Hard); body.eval(ctx); ctx.end_page_group(|s| s == Softness::Hard); softness = Softness::Soft; ctx.state = snapshot; } ctx.start_page_group(softness); Value::None } /// `pagebreak`: Start a new page. pub fn pagebreak(ctx: &mut EvalContext, _: &mut Args) -> Value { ctx.end_page_group(|_| true); ctx.start_page_group(Softness::Hard); Value::None }