From 3284e7fac7c695e42fd2c666befd92742cac96be Mon Sep 17 00:00:00 2001 From: Luis David Licea Torres Date: Mon, 12 Jun 2023 04:46:34 -0600 Subject: [PATCH] Support for inside and outside margins (#1308) Co-authored-by: Laurenz --- docs/src/lib.rs | 6 +- library/src/layout/page.rs | 185 +++++++++++++++++++++++++++++- src/model/styles.rs | 9 ++ tests/ref/layout/page-binding.png | Bin 0 -> 17255 bytes tests/src/tests.rs | 6 +- tests/typ/layout/page-binding.typ | 46 ++++++++ 6 files changed, 241 insertions(+), 11 deletions(-) create mode 100644 tests/ref/layout/page-binding.png create mode 100644 tests/typ/layout/page-binding.typ diff --git a/docs/src/lib.rs b/docs/src/lib.rs index bc3be0cc2..5ddb1198d 100644 --- a/docs/src/lib.rs +++ b/docs/src/lib.rs @@ -19,8 +19,8 @@ use serde_yaml as yaml; use typst::doc::Frame; use typst::eval::{CastInfo, Func, FuncInfo, Library, Module, ParamInfo, Value}; use typst::font::{Font, FontBook}; -use typst::geom::{Abs, Sides, Smart}; -use typst_library::layout::PageElem; +use typst::geom::{Abs, Smart}; +use typst_library::layout::{Margin, PageElem}; use unscanny::Scanner; static SRC: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/src"); @@ -43,7 +43,7 @@ static LIBRARY: Lazy> = Lazy::new(|| { lib.styles .set(PageElem::set_width(Smart::Custom(Abs::pt(240.0).into()))); lib.styles.set(PageElem::set_height(Smart::Auto)); - lib.styles.set(PageElem::set_margin(Sides::splat(Some(Smart::Custom( + lib.styles.set(PageElem::set_margin(Margin::splat(Some(Smart::Custom( Abs::pt(15.0).into(), ))))); typst::eval::set_lang_items(lib.items.clone()); diff --git a/library/src/layout/page.rs b/library/src/layout/page.rs index 26ebbc533..a3f99d564 100644 --- a/library/src/layout/page.rs +++ b/library/src/layout/page.rs @@ -4,6 +4,7 @@ use std::str::FromStr; use super::{AlignElem, ColumnsElem}; use crate::meta::{Counter, CounterKey, Numbering}; use crate::prelude::*; +use crate::text::TextElem; /// Layouts its child onto one or multiple pages. /// @@ -97,11 +98,18 @@ pub struct PageElem { /// - `right`: The right margin. /// - `bottom`: The bottom margin. /// - `left`: The left margin. + /// - `inside`: The margin at the inner side of the page (where the + /// [binding]($func/page.binding) is). + /// - `outside`: The margin at the outer side of the page (opposite to the + /// [binding]($func/page.binding)). /// - `x`: The horizontal margins. /// - `y`: The vertical margins. /// - `rest`: The margins on all sides except those for which the /// dictionary explicitly sets a size. /// + /// The values for `left` and `right` are mutually exclusive with + /// the values for `inside` and `outside`. + /// /// ```example /// #set page( /// width: 3cm, @@ -116,7 +124,18 @@ pub struct PageElem { /// ) /// ``` #[fold] - pub margin: Sides>>>, + pub margin: Margin, + + /// On which side the pages will be bound. + /// + /// - `{auto}`: Equivalent to `left` if the [text direction]($func/text.dir) + /// is left-to-right and `right` if it is right-to-left. + /// - `left`: Bound on the left side. + /// - `right`: Bound on the right side. + /// + /// This affects the meaning of the `inside` and `outside` options for + /// margins. + pub binding: Smart, /// How many columns the page has. /// @@ -301,13 +320,23 @@ impl PageElem { } // Determine the margins. - let default = Rel::from(0.1190 * min); - let margin = self - .margin(styles) - .map(|side| side.unwrap_or(default)) + let default = Rel::::from(0.1190 * min); + let margin = self.margin(styles); + let two_sided = margin.two_sided.unwrap_or(false); + let margin = margin + .sides + .map(|side| side.and_then(Smart::as_custom).unwrap_or(default)) .resolve(styles) .relative_to(size); + // Determine the binding. + let binding = + self.binding(styles) + .unwrap_or_else(|| match TextElem::dir_in(styles) { + Dir::LTR => Binding::Left, + _ => Binding::Right, + }); + // Realize columns. let mut child = self.body(); let columns = self.columns(styles); @@ -352,6 +381,14 @@ impl PageElem { // The padded width of the page's content without margins. let pw = frame.width(); + // If two sided, left becomes inside and right becomes outside. + // Thus, for left-bound pages, we want to swap on even pages and + // for right-bound pages, we want to swap on odd pages. + let mut margin = margin; + if two_sided && binding.swap(number) { + std::mem::swap(&mut margin.left, &mut margin.right); + } + // Realize margins. frame.set_size(frame.size() + margin.sum_by_axis()); frame.translate(Point::new(margin.left, margin.top)); @@ -437,6 +474,144 @@ pub struct PagebreakElem { pub weak: bool, } +/// Specification of the page's margins. +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] +pub struct Margin { + /// The margins for each side. + pub sides: Sides>>>, + /// Whether to swap `left` and `right` to make them `inside` and `outside` + /// (when to swap depends on the binding). + pub two_sided: Option, +} + +impl Margin { + /// Create an instance with four equal components. + pub fn splat(value: Option>>) -> Self { + Self { sides: Sides::splat(value), two_sided: None } + } +} + +impl Fold for Margin { + type Output = Margin; + + fn fold(self, outer: Self::Output) -> Self::Output { + let sides = + self.sides + .zip(outer.sides) + .map(|(inner, outer)| match (inner, outer) { + (Some(value), Some(outer)) => Some(value.fold(outer)), + _ => inner.or(outer), + }); + let two_sided = self.two_sided.or(outer.two_sided); + Margin { sides, two_sided } + } +} + +cast! { + Margin, + self => { + let mut dict = Dict::new(); + let mut handle = |key: &str, component: Value| { + let value = component.into_value(); + if value != Value::None { + dict.insert(key.into(), value); + } + }; + + handle("top", self.sides.top.into_value()); + handle("bottom", self.sides.bottom.into_value()); + if self.two_sided.unwrap_or(false) { + handle("inside", self.sides.left.into_value()); + handle("outside", self.sides.right.into_value()); + } else { + handle("left", self.sides.left.into_value()); + handle("right", self.sides.right.into_value()); + } + + Value::Dict(dict) + }, + _: AutoValue => Self::splat(Some(Smart::Auto)), + v: Rel => Self::splat(Some(Smart::Custom(v))), + mut dict: Dict => { + let mut take = |key| dict.take(key).ok().map(Value::cast).transpose(); + + let rest = take("rest")?; + let x = take("x")?.or(rest); + let y = take("y")?.or(rest); + let top = take("top")?.or(y); + let bottom = take("bottom")?.or(y); + let outside = take("outside")?; + let inside = take("inside")?; + let left = take("left")?; + let right = take("right")?; + + let implicitly_two_sided = outside.is_some() || inside.is_some(); + let implicitly_not_two_sided = left.is_some() || right.is_some(); + if implicitly_two_sided && implicitly_not_two_sided { + bail!("`inside` and `outside` are mutually exclusive with `left` and `right`"); + } + + // - If 'implicitly_two_sided' is false here, then + // 'implicitly_not_two_sided' will be guaranteed to be true + // due to the previous two 'if' conditions. + // - If both are false, this means that this margin change does not + // affect lateral margins, and thus shouldn't make a difference on + // the 'two_sided' attribute of this margin. + let two_sided = (implicitly_two_sided || implicitly_not_two_sided) + .then_some(implicitly_two_sided); + + dict.finish(&[ + "left", "top", "right", "bottom", "outside", "inside", "x", "y", "rest", + ])?; + + Margin { + sides: Sides { + left: inside.or(left).or(x), + top, + right: outside.or(right).or(x), + bottom, + }, + two_sided, + } + } +} + +/// Specification of the page's binding. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum Binding { + /// Bound on the left, as customary in LTR languages. + Left, + /// Bound on the right, as customary in RTL languages. + Right, +} + +impl Binding { + /// Whether to swap left and right margin for the page with this number. + fn swap(self, number: NonZeroUsize) -> bool { + match self { + // Left-bound must swap on even pages + // (because it is correct on the first page). + Self::Left => number.get() % 2 == 0, + // Right-bound must swap on odd pages + // (because it is wrong on the first page). + Self::Right => number.get() % 2 == 1, + } + } +} + +cast! { + Binding, + self => match self { + Self::Left => GenAlign::Specific(Align::Left).into_value(), + Self::Right => GenAlign::Specific(Align::Right).into_value(), + }, + v: GenAlign => match v { + GenAlign::Specific(Align::Left) => Self::Left, + GenAlign::Specific(Align::Right) => Self::Right, + _ => Err("must be `left` or `right`")?, + }, +} + /// A header, footer, foreground or background definition. #[derive(Debug, Clone, Hash)] pub enum Marginal { diff --git a/src/model/styles.rs b/src/model/styles.rs index 5b6430c25..23748a3ff 100644 --- a/src/model/styles.rs +++ b/src/model/styles.rs @@ -720,6 +720,15 @@ impl Resolve for Option { } /// A property that is folded to determine its final value. +/// +/// In the example below, the chain of stroke values is folded into a single +/// value: `4pt + red`. +/// +/// ```example +/// #set rect(stroke: red) +/// #set rect(stroke: 4pt) +/// #rect() +/// ``` pub trait Fold { /// The type of the folded output. type Output; diff --git a/tests/ref/layout/page-binding.png b/tests/ref/layout/page-binding.png new file mode 100644 index 0000000000000000000000000000000000000000..59d73fa452df5d3d8bc635c1df301a3706a068ec GIT binary patch literal 17255 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 z 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 fCHe2(@p+6KkG@e#g+Is&_V Library { lib.styles .set(PageElem::set_width(Smart::Custom(Abs::pt(120.0).into()))); lib.styles.set(PageElem::set_height(Smart::Auto)); - lib.styles.set(PageElem::set_margin(Sides::splat(Some(Smart::Custom( + lib.styles.set(PageElem::set_margin(Margin::splat(Some(Smart::Custom( Abs::pt(10.0).into(), ))))); lib.styles.set(TextElem::set_size(TextSize(Abs::pt(10.0).into()))); diff --git a/tests/typ/layout/page-binding.typ b/tests/typ/layout/page-binding.typ new file mode 100644 index 000000000..66c8a3c6c --- /dev/null +++ b/tests/typ/layout/page-binding.typ @@ -0,0 +1,46 @@ +// Tests multi-page document with binding. + +--- +#set page(height: 100pt, margin: (inside: 30pt, outside: 20pt)) +#set par(justify: true) +#set text(size: 8pt) + +#page(margin: (x: 20pt), { + set align(center + horizon) + text(20pt, strong[Title]) + v(2em, weak: true) + text(15pt)[Author] +}) + += Introduction +#lorem(35) + +--- +// Test setting the binding explicitly. +#set page(margin: (inside: 30pt)) +#rect(width: 100%)[Bound] +#pagebreak() +#rect(width: 100%)[Left] + +--- +// Test setting the binding explicitly. +#set page(binding: right, margin: (inside: 30pt)) +#rect(width: 100%)[Bound] +#pagebreak() +#rect(width: 100%)[Right] + +--- +// Test setting the binding implicitly. +#set page(margin: (inside: 30pt)) +#set text(lang: "he") +#rect(width: 100%)[Bound] +#pagebreak() +#rect(width: 100%)[Right] + +--- +// Error: 19-44 `inside` and `outside` are mutually exclusive with `left` and `right` +#set page(margin: (left: 1cm, outside: 2cm)) + +--- +// Error: 20-23 must be `left` or `right` +#set page(binding: top)