summaryrefslogtreecommitdiff
path: root/crates/typst-library/src/layout/container.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/typst-library/src/layout/container.rs')
-rw-r--r--crates/typst-library/src/layout/container.rs563
1 files changed, 563 insertions, 0 deletions
diff --git a/crates/typst-library/src/layout/container.rs b/crates/typst-library/src/layout/container.rs
new file mode 100644
index 00000000..266d1d88
--- /dev/null
+++ b/crates/typst-library/src/layout/container.rs
@@ -0,0 +1,563 @@
+use crate::diag::{bail, SourceResult};
+use crate::engine::Engine;
+use crate::foundations::{
+ cast, elem, Args, AutoValue, Construct, Content, NativeElement, Packed, Smart,
+ StyleChain, Value,
+};
+use crate::introspection::Locator;
+use crate::layout::{
+ Abs, Corners, Em, Fr, Fragment, Frame, Length, Region, Regions, Rel, Sides, Size,
+ Spacing,
+};
+use crate::visualize::{Paint, Stroke};
+
+/// An inline-level container that sizes content.
+///
+/// All elements except inline math, text, and boxes are block-level and cannot
+/// occur inside of a paragraph. The box function can be used to integrate such
+/// elements into a paragraph. Boxes take the size of their contents by default
+/// but can also be sized explicitly.
+///
+/// # Example
+/// ```example
+/// Refer to the docs
+/// #box(
+/// height: 9pt,
+/// image("docs.svg")
+/// )
+/// for more information.
+/// ```
+#[elem]
+pub struct BoxElem {
+ /// The width of the box.
+ ///
+ /// Boxes can have [fractional]($fraction) widths, as the example below
+ /// demonstrates.
+ ///
+ /// _Note:_ Currently, only boxes and only their widths might be fractionally
+ /// sized within paragraphs. Support for fractionally sized images, shapes,
+ /// and more might be added in the future.
+ ///
+ /// ```example
+ /// Line in #box(width: 1fr, line(length: 100%)) between.
+ /// ```
+ pub width: Sizing,
+
+ /// The height of the box.
+ pub height: Smart<Rel<Length>>,
+
+ /// An amount to shift the box's baseline by.
+ ///
+ /// ```example
+ /// Image: #box(baseline: 40%, image("tiger.jpg", width: 2cm)).
+ /// ```
+ #[resolve]
+ pub baseline: Rel<Length>,
+
+ /// The box's background color. See the
+ /// [rectangle's documentation]($rect.fill) for more details.
+ pub fill: Option<Paint>,
+
+ /// The box's border color. See the
+ /// [rectangle's documentation]($rect.stroke) for more details.
+ #[resolve]
+ #[fold]
+ pub stroke: Sides<Option<Option<Stroke>>>,
+
+ /// How much to round the box's corners. See the
+ /// [rectangle's documentation]($rect.radius) for more details.
+ #[resolve]
+ #[fold]
+ pub radius: Corners<Option<Rel<Length>>>,
+
+ /// How much to pad the box's content.
+ ///
+ /// _Note:_ When the box contains text, its exact size depends on the
+ /// current [text edges]($text.top-edge).
+ ///
+ /// ```example
+ /// #rect(inset: 0pt)[Tight]
+ /// ```
+ #[resolve]
+ #[fold]
+ pub inset: Sides<Option<Rel<Length>>>,
+
+ /// How much to expand the box's size without affecting the layout.
+ ///
+ /// This is useful to prevent padding from affecting line layout. For a
+ /// generalized version of the example below, see the documentation for the
+ /// [raw text's block parameter]($raw.block).
+ ///
+ /// ```example
+ /// An inline
+ /// #box(
+ /// fill: luma(235),
+ /// inset: (x: 3pt, y: 0pt),
+ /// outset: (y: 3pt),
+ /// radius: 2pt,
+ /// )[rectangle].
+ /// ```
+ #[resolve]
+ #[fold]
+ pub outset: Sides<Option<Rel<Length>>>,
+
+ /// Whether to clip the content inside the box.
+ ///
+ /// Clipping is useful when the box's content is larger than the box itself,
+ /// as any content that exceeds the box's bounds will be hidden.
+ ///
+ /// ```example
+ /// #box(
+ /// width: 50pt,
+ /// height: 50pt,
+ /// clip: true,
+ /// image("tiger.jpg", width: 100pt, height: 100pt)
+ /// )
+ /// ```
+ #[default(false)]
+ pub clip: bool,
+
+ /// The contents of the box.
+ #[positional]
+ #[borrowed]
+ pub body: Option<Content>,
+}
+
+/// An inline-level container that can produce arbitrary items that can break
+/// across lines.
+#[elem(Construct)]
+pub struct InlineElem {
+ /// A callback that is invoked with the regions to produce arbitrary
+ /// inline items.
+ #[required]
+ #[internal]
+ body: callbacks::InlineCallback,
+}
+
+impl Construct for InlineElem {
+ fn construct(_: &mut Engine, args: &mut Args) -> SourceResult<Content> {
+ bail!(args.span, "cannot be constructed manually");
+ }
+}
+
+impl InlineElem {
+ /// Create an inline-level item with a custom layouter.
+ #[allow(clippy::type_complexity)]
+ pub fn layouter<T: NativeElement>(
+ captured: Packed<T>,
+ callback: fn(
+ content: &Packed<T>,
+ engine: &mut Engine,
+ locator: Locator,
+ styles: StyleChain,
+ region: Size,
+ ) -> SourceResult<Vec<InlineItem>>,
+ ) -> Self {
+ Self::new(callbacks::InlineCallback::new(captured, callback))
+ }
+}
+
+impl Packed<InlineElem> {
+ /// Layout the element.
+ pub fn layout(
+ &self,
+ engine: &mut Engine,
+ locator: Locator,
+ styles: StyleChain,
+ region: Size,
+ ) -> SourceResult<Vec<InlineItem>> {
+ self.body().call(engine, locator, styles, region)
+ }
+}
+
+/// Layouted items suitable for placing in a paragraph.
+#[derive(Debug, Clone)]
+pub enum InlineItem {
+ /// Absolute spacing between other items, and whether it is weak.
+ Space(Abs, bool),
+ /// Layouted inline-level content.
+ Frame(Frame),
+}
+
+/// A block-level container.
+///
+/// Such a container can be used to separate content, size it, and give it a
+/// background or border.
+///
+/// # Examples
+/// With a block, you can give a background to content while still allowing it
+/// to break across multiple pages.
+/// ```example
+/// #set page(height: 100pt)
+/// #block(
+/// fill: luma(230),
+/// inset: 8pt,
+/// radius: 4pt,
+/// lorem(30),
+/// )
+/// ```
+///
+/// Blocks are also useful to force elements that would otherwise be inline to
+/// become block-level, especially when writing show rules.
+/// ```example
+/// #show heading: it => it.body
+/// = Blockless
+/// More text.
+///
+/// #show heading: it => block(it.body)
+/// = Blocky
+/// More text.
+/// ```
+#[elem]
+pub struct BlockElem {
+ /// The block's width.
+ ///
+ /// ```example
+ /// #set align(center)
+ /// #block(
+ /// width: 60%,
+ /// inset: 8pt,
+ /// fill: silver,
+ /// lorem(10),
+ /// )
+ /// ```
+ pub width: Smart<Rel<Length>>,
+
+ /// The block's height. When the height is larger than the remaining space
+ /// on a page and [`breakable`]($block.breakable) is `{true}`, the
+ /// block will continue on the next page with the remaining height.
+ ///
+ /// ```example
+ /// #set page(height: 80pt)
+ /// #set align(center)
+ /// #block(
+ /// width: 80%,
+ /// height: 150%,
+ /// fill: aqua,
+ /// )
+ /// ```
+ pub height: Sizing,
+
+ /// Whether the block can be broken and continue on the next page.
+ ///
+ /// ```example
+ /// #set page(height: 80pt)
+ /// The following block will
+ /// jump to its own page.
+ /// #block(
+ /// breakable: false,
+ /// lorem(15),
+ /// )
+ /// ```
+ #[default(true)]
+ pub breakable: bool,
+
+ /// The block's background color. See the
+ /// [rectangle's documentation]($rect.fill) for more details.
+ pub fill: Option<Paint>,
+
+ /// The block's border color. See the
+ /// [rectangle's documentation]($rect.stroke) for more details.
+ #[resolve]
+ #[fold]
+ pub stroke: Sides<Option<Option<Stroke>>>,
+
+ /// How much to round the block's corners. See the
+ /// [rectangle's documentation]($rect.radius) for more details.
+ #[resolve]
+ #[fold]
+ pub radius: Corners<Option<Rel<Length>>>,
+
+ /// How much to pad the block's content. See the
+ /// [box's documentation]($box.inset) for more details.
+ #[resolve]
+ #[fold]
+ pub inset: Sides<Option<Rel<Length>>>,
+
+ /// How much to expand the block's size without affecting the layout. See
+ /// the [box's documentation]($box.outset) for more details.
+ #[resolve]
+ #[fold]
+ pub outset: Sides<Option<Rel<Length>>>,
+
+ /// The spacing around the block. When `{auto}`, inherits the paragraph
+ /// [`spacing`]($par.spacing).
+ ///
+ /// For two adjacent blocks, the larger of the first block's `above` and the
+ /// second block's `below` spacing wins. Moreover, block spacing takes
+ /// precedence over paragraph [`spacing`]($par.spacing).
+ ///
+ /// Note that this is only a shorthand to set `above` and `below` to the
+ /// same value. Since the values for `above` and `below` might differ, a
+ /// [context] block only provides access to `{block.above}` and
+ /// `{block.below}`, not to `{block.spacing}` directly.
+ ///
+ /// This property can be used in combination with a show rule to adjust the
+ /// spacing around arbitrary block-level elements.
+ ///
+ /// ```example
+ /// #set align(center)
+ /// #show math.equation: set block(above: 8pt, below: 16pt)
+ ///
+ /// This sum of $x$ and $y$:
+ /// $ x + y = z $
+ /// A second paragraph.
+ /// ```
+ #[external]
+ #[default(Em::new(1.2).into())]
+ pub spacing: Spacing,
+
+ /// The spacing between this block and its predecessor.
+ #[parse(
+ let spacing = args.named("spacing")?;
+ args.named("above")?.or(spacing)
+ )]
+ pub above: Smart<Spacing>,
+
+ /// The spacing between this block and its successor.
+ #[parse(args.named("below")?.or(spacing))]
+ pub below: Smart<Spacing>,
+
+ /// Whether to clip the content inside the block.
+ ///
+ /// Clipping is useful when the block's content is larger than the block itself,
+ /// as any content that exceeds the block's bounds will be hidden.
+ ///
+ /// ```example
+ /// #block(
+ /// width: 50pt,
+ /// height: 50pt,
+ /// clip: true,
+ /// image("tiger.jpg", width: 100pt, height: 100pt)
+ /// )
+ /// ```
+ #[default(false)]
+ pub clip: bool,
+
+ /// Whether this block must stick to the following one, with no break in
+ /// between.
+ ///
+ /// This is, by default, set on heading blocks to prevent orphaned headings
+ /// at the bottom of the page.
+ ///
+ /// ```example
+ /// >>> #set page(height: 140pt)
+ /// // Disable stickiness of headings.
+ /// #show heading: set block(sticky: false)
+ /// #lorem(20)
+ ///
+ /// = Chapter
+ /// #lorem(10)
+ /// ```
+ #[default(false)]
+ pub sticky: bool,
+
+ /// The contents of the block.
+ #[positional]
+ #[borrowed]
+ pub body: Option<BlockBody>,
+}
+
+impl BlockElem {
+ /// Create a block with a custom single-region layouter.
+ ///
+ /// Such a block must have `breakable: false` (which is set by this
+ /// constructor).
+ pub fn single_layouter<T: NativeElement>(
+ captured: Packed<T>,
+ f: fn(
+ content: &Packed<T>,
+ engine: &mut Engine,
+ locator: Locator,
+ styles: StyleChain,
+ region: Region,
+ ) -> SourceResult<Frame>,
+ ) -> Self {
+ Self::new()
+ .with_breakable(false)
+ .with_body(Some(BlockBody::SingleLayouter(
+ callbacks::BlockSingleCallback::new(captured, f),
+ )))
+ }
+
+ /// Create a block with a custom multi-region layouter.
+ pub fn multi_layouter<T: NativeElement>(
+ captured: Packed<T>,
+ f: fn(
+ content: &Packed<T>,
+ engine: &mut Engine,
+ locator: Locator,
+ styles: StyleChain,
+ regions: Regions,
+ ) -> SourceResult<Fragment>,
+ ) -> Self {
+ Self::new().with_body(Some(BlockBody::MultiLayouter(
+ callbacks::BlockMultiCallback::new(captured, f),
+ )))
+ }
+}
+
+/// The contents of a block.
+#[derive(Debug, Clone, PartialEq, Hash)]
+pub enum BlockBody {
+ /// The block contains normal content.
+ Content(Content),
+ /// The block contains a layout callback that needs access to just one
+ /// base region.
+ SingleLayouter(callbacks::BlockSingleCallback),
+ /// The block contains a layout callback that needs access to the exact
+ /// regions.
+ MultiLayouter(callbacks::BlockMultiCallback),
+}
+
+impl Default for BlockBody {
+ fn default() -> Self {
+ Self::Content(Content::default())
+ }
+}
+
+cast! {
+ BlockBody,
+ self => match self {
+ Self::Content(content) => content.into_value(),
+ _ => Value::Auto,
+ },
+ v: Content => Self::Content(v),
+}
+
+/// Defines how to size something along an axis.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub enum Sizing {
+ /// A track that fits its item's contents.
+ Auto,
+ /// A size specified in absolute terms and relative to the parent's size.
+ Rel(Rel),
+ /// A size specified as a fraction of the remaining free space in the
+ /// parent.
+ Fr(Fr),
+}
+
+impl Sizing {
+ /// Whether this is an automatic sizing.
+ pub fn is_auto(self) -> bool {
+ matches!(self, Self::Auto)
+ }
+
+ /// Whether this is fractional sizing.
+ pub fn is_fractional(self) -> bool {
+ matches!(self, Self::Fr(_))
+ }
+}
+
+impl Default for Sizing {
+ fn default() -> Self {
+ Self::Auto
+ }
+}
+
+impl From<Smart<Rel>> for Sizing {
+ fn from(smart: Smart<Rel>) -> Self {
+ match smart {
+ Smart::Auto => Self::Auto,
+ Smart::Custom(rel) => Self::Rel(rel),
+ }
+ }
+}
+
+impl<T: Into<Spacing>> From<T> for Sizing {
+ fn from(spacing: T) -> Self {
+ match spacing.into() {
+ Spacing::Rel(rel) => Self::Rel(rel),
+ Spacing::Fr(fr) => Self::Fr(fr),
+ }
+ }
+}
+
+cast! {
+ Sizing,
+ self => match self {
+ Self::Auto => Value::Auto,
+ Self::Rel(rel) => rel.into_value(),
+ Self::Fr(fr) => fr.into_value(),
+ },
+ _: AutoValue => Self::Auto,
+ v: Rel<Length> => Self::Rel(v),
+ v: Fr => Self::Fr(v),
+}
+
+/// Manual closure implementations for layout callbacks.
+///
+/// Normal closures are not `Hash`, so we can't use them.
+mod callbacks {
+ use super::*;
+
+ macro_rules! callback {
+ ($name:ident = ($($param:ident: $param_ty:ty),* $(,)?) -> $ret:ty) => {
+ #[derive(Debug, Clone, PartialEq, Hash)]
+ pub struct $name {
+ captured: Content,
+ f: fn(&Content, $($param_ty),*) -> $ret,
+ }
+
+ impl $name {
+ pub fn new<T: NativeElement>(
+ captured: Packed<T>,
+ f: fn(&Packed<T>, $($param_ty),*) -> $ret,
+ ) -> Self {
+ Self {
+ // Type-erased the content.
+ captured: captured.pack(),
+ // Safety: The only difference between the two function
+ // pointer types is the type of the first parameter,
+ // which changes from `&Packed<T>` to `&Content`. This
+ // is safe because:
+ // - `Packed<T>` is a transparent wrapper around
+ // `Content`, so for any `T` it has the same memory
+ // representation as `Content`.
+ // - While `Packed<T>` imposes the additional constraint
+ // that the content is of type `T`, this constraint is
+ // upheld: It is initially the case because we store a
+ // `Packed<T>` above. It keeps being the case over the
+ // lifetime of the closure because `capture` is a
+ // private field and `Content`'s `Clone` impl is
+ // guaranteed to retain the type (if it didn't,
+ // literally everything would break).
+ #[allow(clippy::missing_transmute_annotations)]
+ f: unsafe { std::mem::transmute(f) },
+ }
+ }
+
+ pub fn call(&self, $($param: $param_ty),*) -> $ret {
+ (self.f)(&self.captured, $($param),*)
+ }
+ }
+ };
+ }
+
+ callback! {
+ InlineCallback = (
+ engine: &mut Engine,
+ locator: Locator,
+ styles: StyleChain,
+ region: Size,
+ ) -> SourceResult<Vec<InlineItem>>
+ }
+
+ callback! {
+ BlockSingleCallback = (
+ engine: &mut Engine,
+ locator: Locator,
+ styles: StyleChain,
+ region: Region,
+ ) -> SourceResult<Frame>
+ }
+
+ callback! {
+ BlockMultiCallback = (
+ engine: &mut Engine,
+ locator: Locator,
+ styles: StyleChain,
+ regions: Regions,
+ ) -> SourceResult<Fragment>
+ }
+}