diff --git a/library/src/lib.rs b/library/src/lib.rs index c11b818ec..ac99425fe 100644 --- a/library/src/lib.rs +++ b/library/src/lib.rs @@ -97,6 +97,7 @@ fn global(math: Module, calc: Module) -> Module { global.define("bibliography", meta::BibliographyElem::func()); global.define("locate", meta::locate); global.define("style", meta::style); + global.define("layout", meta::layout); global.define("counter", meta::counter); global.define("numbering", meta::numbering); global.define("state", meta::state); diff --git a/library/src/meta/context.rs b/library/src/meta/context.rs index 7426d27d7..3902f47aa 100644 --- a/library/src/meta/context.rs +++ b/library/src/meta/context.rs @@ -121,3 +121,101 @@ impl Show for StyleElem { Ok(self.func().call_vt(vt, [styles.to_map().into()])?.display()) } } + +/// Provides access to the current outer container's (or page's, if none) size (width and height). +/// +/// The given function must accept a single parameter, `size`, which is a dictionary with keys +/// `width` and `height`, both having the type [`length`]($type/length). +/// +/// That is, if this `layout` call is done inside (for example) a box of size 800pt (width) +/// by 400pt (height), then the specified function will be given the parameter +/// `(width: 800pt, height: 400pt)`. +/// +/// If, however, this `layout` call is placed directly on the page, not inside any container, +/// then the function will be given `(width: page_width, height: page_height)`, where `page_width` +/// and `page_height` correspond to the current page's respective dimensions, minus its margins. +/// +/// This is useful, for example, to convert a [`ratio`]($type/ratio) value (such as `5%`, `100%` +/// etc.), which are usually based upon the outer container's dimensions (precisely what this +/// function gives), to a fixed length (in `pt`). +/// +/// This is also useful if you're trying to make content fit a certain box, and doing certain +/// arithmetic using `pt` (for example, comparing different lengths) is required. +/// +/// Please note: This function may provide a width or height of `infpt` if one of the page +/// dimensions is `auto`, under certain circumstances. This should not normally occur for +/// usual page sizes, however. +/// +/// ```example +/// layout(size => { +/// // work with the width and height of the container we're in +/// // using size.width and size.height +/// }) +/// +/// layout(size => { +/// // convert 49% (of page width) to 'pt' +/// // note that "ratio" values are always relative to a certain, possibly arbitrary length, +/// // but it's usually the current container's width or height (e.g., for table columns, +/// // 15% would be relative to the width, but, for rows, it would be relative to the height). +/// let percentage_of_width = (49% / 1%) * 0.01 * size.width +/// // ... use the converted value ... +/// }) +/// +/// // The following two boxes are equivalent, and will have rectangles sized 200pt and 40pt: +/// +/// #box(width: 200pt, height: 40pt, { +/// rect(width: 100%, height: 100%) +/// }) +/// +/// #box(width: 200pt, height: 40pt, layout(size => { +/// rect(width: size.width, height: size.height) +/// })) +/// ``` +/// +/// Display: Layout +/// Category: meta +/// Returns: content +#[func] +pub fn layout( + /// A function to call with the outer container's size. Its return value is displayed + /// in the document. + /// + /// This function is called once for each time the content returned by + /// `layout` appears in the document. That makes it possible to generate + /// content that depends on the size of the container it is inside. + func: Func, +) -> Value { + LayoutElem::new(func).pack().into() +} + +/// Executes a `layout` call. +/// +/// Display: Layout +/// Category: special +#[element(Layout)] +struct LayoutElem { + /// The function to call with the outer container's (or page's) size. + #[required] + func: Func, +} + +impl Layout for LayoutElem { + fn layout( + &self, + vt: &mut Vt, + styles: StyleChain, + regions: Regions, + ) -> SourceResult { + // Gets the current region's base size, which will be the size of the outer container, + // or of the page if there is no such container. + let Size { x, y } = regions.base(); + let size_dict = dict! { "width" => x, "height" => y }.into(); + + let result = self + .func() + .call_vt(vt, [size_dict])? // calls func(size) + .display(); + + result.layout(vt, styles, regions) + } +} diff --git a/tests/ref/layout/block-sizing.png b/tests/ref/layout/block-sizing.png index ff95c34c0..f6655e63e 100644 Binary files a/tests/ref/layout/block-sizing.png and b/tests/ref/layout/block-sizing.png differ diff --git a/tests/ref/layout/page.png b/tests/ref/layout/page.png index 4881fa2b9..bcf325261 100644 Binary files a/tests/ref/layout/page.png and b/tests/ref/layout/page.png differ diff --git a/tests/typ/layout/block-sizing.typ b/tests/typ/layout/block-sizing.typ index a768c3e3a..181bbe310 100644 --- a/tests/typ/layout/block-sizing.typ +++ b/tests/typ/layout/block-sizing.typ @@ -14,3 +14,11 @@ fill: aqua, lorem(8) + colbreak(), ) + +--- +// Layout inside a block with certain dimensions should provide those dimensions. + +#set page(height: 120pt) +#block(width: 60pt, height: 80pt, layout(size => [ + This block has a width of #size.width and height of #size.height +])) diff --git a/tests/typ/layout/page.typ b/tests/typ/layout/page.typ index ff050e55f..f5c7822d0 100644 --- a/tests/typ/layout/page.typ +++ b/tests/typ/layout/page.typ @@ -31,3 +31,12 @@ // Should result in one forest-colored A11 page and one auto-sized page. #page("a11", flipped: true, fill: forest)[] #pagebreak() + +--- +// Layout without any container should provide the page's dimensions, minus its margins. + +#page(width: 100pt, height: 100pt, { + layout(size => [This page has a width of #size.width and height of #size.height ]) + h(1em) + place(left, rect(width: 80pt, stroke: blue)) +})