mirror of
https://github.com/typst/typst
synced 2025-05-19 11:35:27 +08:00
Support for inside and outside margins (#1308)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
This commit is contained in:
parent
93e6638bfe
commit
3284e7fac7
@ -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());
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
BIN
tests/ref/layout/page-binding.png
Normal file
BIN
tests/ref/layout/page-binding.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
@ -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())));
|
||||
|
46
tests/typ/layout/page-binding.typ
Normal file
46
tests/typ/layout/page-binding.typ
Normal 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)
|
Loading…
x
Reference in New Issue
Block a user