summaryrefslogtreecommitdiff
path: root/src/library/layout.rs
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2020-11-25 16:56:29 +0100
committerLaurenz <laurmaedje@gmail.com>2020-11-25 16:56:29 +0100
commit11e44516fae84f907ea992311fcfdc3636101f14 (patch)
treeff9b6a04c3accd5c0f75f1ceb60e578c389a2606 /src/library/layout.rs
parent761931405c68efe0a35d96524df797dda7155723 (diff)
Merge some modules 🥞
Diffstat (limited to 'src/library/layout.rs')
-rw-r--r--src/library/layout.rs337
1 files changed, 337 insertions, 0 deletions
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::<SynTree>();
+ let first = args.get::<_, Spanned<AlignArg>>(ctx, 0);
+ let second = args.get::<_, Spanned<AlignArg>>(ctx, 1);
+ let hor = args.get::<_, Spanned<AlignArg>>(ctx, "horizontal");
+ let ver = args.get::<_, Spanned<AlignArg>>(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<SpecAxis> {
+ 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::<SynTree>().unwrap_or_default();
+ let width = args.get::<_, Linear>(ctx, "width");
+ let height = args.get::<_, Linear>(ctx, "height");
+ let main = args.get::<_, Spanned<Dir>>(ctx, "main");
+ let cross = args.get::<_, Spanned<Dir>>(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::<SynTree>();
+
+ if let Some(paper) = args.find::<Paper>() {
+ 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<Dir>>(ctx, "main");
+ let cross = args.get::<_, Spanned<Dir>>(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
+}