Support for inside and outside margins (#1308)

Co-authored-by: Laurenz <laurmaedje@gmail.com>
This commit is contained in:
Luis David Licea Torres 2023-06-12 04:46:34 -06:00 committed by GitHub
parent 93e6638bfe
commit 3284e7fac7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 241 additions and 11 deletions

View File

@ -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<Prehashed<Library>> = 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());

View File

@ -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<Option<Smart<Rel<Length>>>>,
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<Binding>,
/// 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::<Length>::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<Option<Smart<Rel<Length>>>>,
/// Whether to swap `left` and `right` to make them `inside` and `outside`
/// (when to swap depends on the binding).
pub two_sided: Option<bool>,
}
impl Margin {
/// Create an instance with four equal components.
pub fn splat(value: Option<Smart<Rel<Length>>>) -> 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<Length> => 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 {

View File

@ -720,6 +720,15 @@ impl<T: Resolve> Resolve for Option<T> {
}
/// 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;

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -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())));

View File

@ -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)