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 000000000..59d73fa45 Binary files /dev/null and b/tests/ref/layout/page-binding.png differ diff --git a/tests/src/tests.rs b/tests/src/tests.rs index debb3c921..34070e9c9 100644 --- a/tests/src/tests.rs +++ b/tests/src/tests.rs @@ -24,11 +24,11 @@ use typst::diag::{bail, FileError, FileResult, StrResult}; use typst::doc::{Document, Frame, FrameItem, Meta}; use typst::eval::{func, Datetime, Library, NoneValue, Value}; use typst::font::{Font, FontBook}; -use typst::geom::{Abs, Color, RgbaColor, Sides, Smart}; +use typst::geom::{Abs, Color, RgbaColor, Smart}; use typst::syntax::{Source, SourceId, Span, SyntaxNode}; use typst::util::{Buffer, PathExt}; use typst::World; -use typst_library::layout::PageElem; +use typst_library::layout::{Margin, PageElem}; use typst_library::text::{TextElem, TextSize}; const TYP_DIR: &str = "typ"; @@ -177,7 +177,7 @@ fn library() -> 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)