summaryrefslogtreecommitdiff
path: root/crates/typst-library/src/layout/container.rs
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2023-07-02 19:59:52 +0200
committerLaurenz <laurmaedje@gmail.com>2023-07-02 20:07:43 +0200
commitebfdb1dafa430786db10dad2ef7d5467c1bdbed1 (patch)
tree2bbc24ddb4124c4bb14dec0e536129d4de37b056 /crates/typst-library/src/layout/container.rs
parent3ab19185093d7709f824b95b979060ce125389d8 (diff)
Move everything into `crates/` directory
Diffstat (limited to 'crates/typst-library/src/layout/container.rs')
-rw-r--r--crates/typst-library/src/layout/container.rs497
1 files changed, 497 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..c79669d0
--- /dev/null
+++ b/crates/typst-library/src/layout/container.rs
@@ -0,0 +1,497 @@
+use typst::eval::AutoValue;
+
+use super::VElem;
+use crate::layout::Spacing;
+use crate::prelude::*;
+
+/// 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 }
+/// ```example
+/// Refer to the docs
+/// #box(
+/// height: 9pt,
+/// image("docs.svg")
+/// )
+/// for more information.
+/// ```
+///
+/// Display: Box
+/// Category: layout
+#[element(Layout)]
+pub struct BoxElem {
+ /// The width of the box.
+ ///
+ /// Boxes can have [fractional]($type/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]($func/rect.fill) for more details.
+ pub fill: Option<Paint>,
+
+ /// The box's border color. See the
+ /// [rectangle's documentation]($func/rect.stroke) for more details.
+ #[resolve]
+ #[fold]
+ pub stroke: Sides<Option<Option<PartialStroke>>>,
+
+ /// How much to round the box's corners. See the [rectangle's
+ /// documentation]($func/rect.radius) for more details.
+ #[resolve]
+ #[fold]
+ pub radius: Corners<Option<Rel<Length>>>,
+
+ /// How much to pad the box's content. See the [rectangle's
+ /// documentation]($func/rect.inset) for more details.
+ #[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]($func/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.
+ #[default(false)]
+ pub clip: bool,
+
+ /// The contents of the box.
+ #[positional]
+ pub body: Option<Content>,
+}
+
+impl Layout for BoxElem {
+ #[tracing::instrument(name = "BoxElem::layout", skip_all)]
+ fn layout(
+ &self,
+ vt: &mut Vt,
+ styles: StyleChain,
+ regions: Regions,
+ ) -> SourceResult<Fragment> {
+ let width = match self.width(styles) {
+ Sizing::Auto => Smart::Auto,
+ Sizing::Rel(rel) => Smart::Custom(rel),
+ Sizing::Fr(_) => Smart::Custom(Ratio::one().into()),
+ };
+
+ // Resolve the sizing to a concrete size.
+ let sizing = Axes::new(width, self.height(styles));
+ let expand = sizing.as_ref().map(Smart::is_custom);
+ let size = sizing
+ .resolve(styles)
+ .zip(regions.base())
+ .map(|(s, b)| s.map(|v| v.relative_to(b)))
+ .unwrap_or(regions.base());
+
+ // Apply inset.
+ let mut body = self.body(styles).unwrap_or_default();
+ let inset = self.inset(styles);
+ if inset.iter().any(|v| !v.is_zero()) {
+ body = body.padded(inset.map(|side| side.map(Length::from)));
+ }
+
+ // Select the appropriate base and expansion for the child depending
+ // on whether it is automatically or relatively sized.
+ let pod = Regions::one(size, expand);
+ let mut frame = body.layout(vt, styles, pod)?.into_frame();
+
+ // Enforce correct size.
+ *frame.size_mut() = expand.select(size, frame.size());
+
+ // Apply baseline shift.
+ let shift = self.baseline(styles).relative_to(frame.height());
+ if !shift.is_zero() {
+ frame.set_baseline(frame.baseline() - shift);
+ }
+
+ // Clip the contents
+ if self.clip(styles) {
+ frame.clip();
+ }
+
+ // Prepare fill and stroke.
+ let fill = self.fill(styles);
+ let stroke = self.stroke(styles).map(|s| s.map(PartialStroke::unwrap_or_default));
+
+ // Add fill and/or stroke.
+ if fill.is_some() || stroke.iter().any(Option::is_some) {
+ let outset = self.outset(styles);
+ let radius = self.radius(styles);
+ frame.fill_and_stroke(fill, stroke, outset, radius, self.span());
+ }
+
+ // Apply metadata.
+ frame.meta(styles, false);
+
+ Ok(Fragment::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 { #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.
+/// ```
+///
+/// Display: Block
+/// Category: layout
+#[element(Layout)]
+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`]($func/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: Smart<Rel<Length>>,
+
+ /// 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]($func/rect.fill) for more details.
+ pub fill: Option<Paint>,
+
+ /// The block's border color. See the
+ /// [rectangle's documentation]($func/rect.stroke) for more details.
+ #[resolve]
+ #[fold]
+ pub stroke: Sides<Option<Option<PartialStroke>>>,
+
+ /// How much to round the block's corners. See the [rectangle's
+ /// documentation]($func/rect.radius) for more details.
+ #[resolve]
+ #[fold]
+ pub radius: Corners<Option<Rel<Length>>>,
+
+ /// How much to pad the block's content. See the [rectangle's
+ /// documentation]($func/rect.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 [rectangle's documentation]($func/rect.outset) for more details.
+ #[resolve]
+ #[fold]
+ pub outset: Sides<Option<Rel<Length>>>,
+
+ /// The spacing around this block. This is shorthand to set `above` and
+ /// `below` to the same value.
+ ///
+ /// ```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. Takes precedence
+ /// over `spacing`. Can be used in combination with a show rule to adjust
+ /// the spacing around arbitrary block-level elements.
+ #[external]
+ #[default(Em::new(1.2).into())]
+ pub above: Spacing,
+ #[internal]
+ #[parse(
+ let spacing = args.named("spacing")?;
+ args.named("above")?
+ .map(VElem::block_around)
+ .or_else(|| spacing.map(VElem::block_spacing))
+ )]
+ #[default(VElem::block_spacing(Em::new(1.2).into()))]
+ pub above: VElem,
+
+ /// The spacing between this block and its successor. Takes precedence
+ /// over `spacing`.
+ #[external]
+ #[default(Em::new(1.2).into())]
+ pub below: Spacing,
+ #[internal]
+ #[parse(
+ args.named("below")?
+ .map(VElem::block_around)
+ .or_else(|| spacing.map(VElem::block_spacing))
+ )]
+ #[default(VElem::block_spacing(Em::new(1.2).into()))]
+ pub below: VElem,
+
+ /// Whether to clip the content inside the block.
+ #[default(false)]
+ pub clip: bool,
+
+ /// The contents of the block.
+ #[positional]
+ pub body: Option<Content>,
+
+ /// Whether this block must stick to the following one.
+ ///
+ /// Use this to prevent page breaks between e.g. a heading and its body.
+ #[internal]
+ #[default(false)]
+ pub sticky: bool,
+}
+
+impl Layout for BlockElem {
+ #[tracing::instrument(name = "BlockElem::layout", skip_all)]
+ fn layout(
+ &self,
+ vt: &mut Vt,
+ styles: StyleChain,
+ regions: Regions,
+ ) -> SourceResult<Fragment> {
+ // Apply inset.
+ let mut body = self.body(styles).unwrap_or_default();
+ let inset = self.inset(styles);
+ if inset.iter().any(|v| !v.is_zero()) {
+ body = body.clone().padded(inset.map(|side| side.map(Length::from)));
+ }
+
+ // Resolve the sizing to a concrete size.
+ let sizing = Axes::new(self.width(styles), self.height(styles));
+ let mut expand = sizing.as_ref().map(Smart::is_custom);
+ let mut size = sizing
+ .resolve(styles)
+ .zip(regions.base())
+ .map(|(s, b)| s.map(|v| v.relative_to(b)))
+ .unwrap_or(regions.base());
+
+ // Layout the child.
+ let mut frames = if self.breakable(styles) {
+ // Measure to ensure frames for all regions have the same width.
+ if sizing.x == Smart::Auto {
+ let pod = Regions::one(size, Axes::splat(false));
+ let frame = body.measure(vt, styles, pod)?.into_frame();
+ size.x = frame.width();
+ expand.x = true;
+ }
+
+ let mut pod = regions;
+ pod.size.x = size.x;
+ pod.expand = expand;
+
+ if expand.y {
+ pod.full = size.y;
+ }
+
+ // Generate backlog for fixed height.
+ let mut heights = vec![];
+ if sizing.y.is_custom() {
+ let mut remaining = size.y;
+ for region in regions.iter() {
+ let limited = region.y.min(remaining);
+ heights.push(limited);
+ remaining -= limited;
+ if Abs::zero().fits(remaining) {
+ break;
+ }
+ }
+
+ if let Some(last) = heights.last_mut() {
+ *last += remaining;
+ }
+
+ pod.size.y = heights[0];
+ pod.backlog = &heights[1..];
+ pod.last = None;
+ }
+
+ let mut frames = body.layout(vt, styles, pod)?.into_frames();
+ for (frame, &height) in frames.iter_mut().zip(&heights) {
+ *frame.size_mut() =
+ expand.select(Size::new(size.x, height), frame.size());
+ }
+ frames
+ } else {
+ let pod = Regions::one(size, expand);
+ let mut frames = body.layout(vt, styles, pod)?.into_frames();
+ *frames[0].size_mut() = expand.select(size, frames[0].size());
+ frames
+ };
+
+ // Clip the contents
+ if self.clip(styles) {
+ for frame in frames.iter_mut() {
+ frame.clip();
+ }
+ }
+
+ // Prepare fill and stroke.
+ let fill = self.fill(styles);
+ let stroke = self.stroke(styles).map(|s| s.map(PartialStroke::unwrap_or_default));
+
+ // Add fill and/or stroke.
+ if fill.is_some() || stroke.iter().any(Option::is_some) {
+ let mut skip = false;
+ if let [first, rest @ ..] = frames.as_slice() {
+ skip = first.is_empty() && rest.iter().any(|frame| !frame.is_empty());
+ }
+
+ let outset = self.outset(styles);
+ let radius = self.radius(styles);
+ for frame in frames.iter_mut().skip(skip as usize) {
+ frame.fill_and_stroke(
+ fill.clone(),
+ stroke.clone(),
+ outset,
+ radius,
+ self.span(),
+ );
+ }
+ }
+
+ // Apply metadata.
+ for frame in &mut frames {
+ frame.meta(styles, false);
+ }
+
+ Ok(Fragment::frames(frames))
+ }
+}
+
+/// Defines how to size a grid cell along an axis.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub enum Sizing {
+ /// A track that fits its cell's contents.
+ Auto,
+ /// A track size specified in absolute terms and relative to the parent's
+ /// size.
+ Rel(Rel<Length>),
+ /// A track size specified as a fraction of the remaining free space in the
+ /// parent.
+ Fr(Fr),
+}
+
+impl Sizing {
+ /// 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<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),
+}