summaryrefslogtreecommitdiff
path: root/crates/typst-library/src/layout
diff options
context:
space:
mode:
Diffstat (limited to 'crates/typst-library/src/layout')
-rw-r--r--crates/typst-library/src/layout/align.rs46
-rw-r--r--crates/typst-library/src/layout/columns.rs169
-rw-r--r--crates/typst-library/src/layout/container.rs502
-rw-r--r--crates/typst-library/src/layout/enum.rs319
-rw-r--r--crates/typst-library/src/layout/flow.rs714
-rw-r--r--crates/typst-library/src/layout/fragment.rs87
-rw-r--r--crates/typst-library/src/layout/grid.rs734
-rw-r--r--crates/typst-library/src/layout/hide.rs29
-rw-r--r--crates/typst-library/src/layout/list.rs238
-rw-r--r--crates/typst-library/src/layout/measure.rs53
-rw-r--r--crates/typst-library/src/layout/mod.rs786
-rw-r--r--crates/typst-library/src/layout/pad.rs121
-rw-r--r--crates/typst-library/src/layout/page.rs949
-rw-r--r--crates/typst-library/src/layout/par.rs1520
-rw-r--r--crates/typst-library/src/layout/place.rs124
-rw-r--r--crates/typst-library/src/layout/regions.rs144
-rw-r--r--crates/typst-library/src/layout/repeat.rs75
-rw-r--r--crates/typst-library/src/layout/spacing.rs262
-rw-r--r--crates/typst-library/src/layout/stack.rs345
-rw-r--r--crates/typst-library/src/layout/table.rs361
-rw-r--r--crates/typst-library/src/layout/terms.rs162
-rw-r--r--crates/typst-library/src/layout/transform.rs187
22 files changed, 0 insertions, 7927 deletions
diff --git a/crates/typst-library/src/layout/align.rs b/crates/typst-library/src/layout/align.rs
deleted file mode 100644
index 9c18266d..00000000
--- a/crates/typst-library/src/layout/align.rs
+++ /dev/null
@@ -1,46 +0,0 @@
-use crate::prelude::*;
-
-/// Aligns content horizontally and vertically.
-///
-/// # Example
-/// ```example
-/// #set align(center)
-///
-/// Centered text, a sight to see \
-/// In perfect balance, visually \
-/// Not left nor right, it stands alone \
-/// A work of art, a visual throne
-/// ```
-#[elem(Show)]
-pub struct AlignElem {
- /// The [alignment]($alignment) along both axes.
- ///
- /// ```example
- /// #set page(height: 6cm)
- /// #set text(lang: "ar")
- ///
- /// مثال
- /// #align(
- /// end + horizon,
- /// rect(inset: 12pt)[ركن]
- /// )
- /// ```
- #[positional]
- #[fold]
- #[default]
- pub alignment: Align,
-
- /// The content to align.
- #[required]
- pub body: Content,
-}
-
-impl Show for AlignElem {
- #[tracing::instrument(name = "AlignElem::show", skip_all)]
- fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
- Ok(self
- .body()
- .clone()
- .styled(Self::set_alignment(self.alignment(styles))))
- }
-}
diff --git a/crates/typst-library/src/layout/columns.rs b/crates/typst-library/src/layout/columns.rs
deleted file mode 100644
index bf111506..00000000
--- a/crates/typst-library/src/layout/columns.rs
+++ /dev/null
@@ -1,169 +0,0 @@
-use crate::prelude::*;
-use crate::text::TextElem;
-
-/// Separates a region into multiple equally sized columns.
-///
-/// The `column` function allows to separate the interior of any container into
-/// multiple columns. It will not equalize the height of the columns, instead,
-/// the columns will take up the height of their container or the remaining
-/// height on the page. The columns function can break across pages if
-/// necessary.
-///
-/// If you need to insert columns across your whole document, you can use the
-/// [`{page}` function's `columns` parameter]($page.columns) instead.
-///
-/// # Example
-/// ```example
-/// = Towards Advanced Deep Learning
-///
-/// #box(height: 68pt,
-/// columns(2, gutter: 11pt)[
-/// #set par(justify: true)
-/// This research was funded by the
-/// National Academy of Sciences.
-/// NAoS provided support for field
-/// tests and interviews with a
-/// grant of up to USD 40.000 for a
-/// period of 6 months.
-/// ]
-/// )
-///
-/// In recent years, deep learning has
-/// increasingly been used to solve a
-/// variety of problems.
-/// ```
-#[elem(Layout)]
-pub struct ColumnsElem {
- /// The number of columns.
- #[positional]
- #[default(NonZeroUsize::new(2).unwrap())]
- pub count: NonZeroUsize,
-
- /// The size of the gutter space between each column.
- #[resolve]
- #[default(Ratio::new(0.04).into())]
- pub gutter: Rel<Length>,
-
- /// The content that should be layouted into the columns.
- #[required]
- pub body: Content,
-}
-
-impl Layout for ColumnsElem {
- #[tracing::instrument(name = "ColumnsElem::layout", skip_all)]
- fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- let body = self.body();
-
- // Separating the infinite space into infinite columns does not make
- // much sense.
- if !regions.size.x.is_finite() {
- return body.layout(vt, styles, regions);
- }
-
- // Determine the width of the gutter and each column.
- let columns = self.count(styles).get();
- let gutter = self.gutter(styles).relative_to(regions.base().x);
- let width = (regions.size.x - gutter * (columns - 1) as f64) / columns as f64;
-
- let backlog: Vec<_> = std::iter::once(&regions.size.y)
- .chain(regions.backlog)
- .flat_map(|&height| std::iter::repeat(height).take(columns))
- .skip(1)
- .collect();
-
- // Create the pod regions.
- let pod = Regions {
- size: Size::new(width, regions.size.y),
- full: regions.full,
- backlog: &backlog,
- last: regions.last,
- expand: Axes::new(true, regions.expand.y),
- root: regions.root,
- };
-
- // Layout the children.
- let mut frames = body.layout(vt, styles, pod)?.into_iter();
- let mut finished = vec![];
-
- let dir = TextElem::dir_in(styles);
- let total_regions = (frames.len() as f32 / columns as f32).ceil() as usize;
-
- // Stitch together the columns for each region.
- for region in regions.iter().take(total_regions) {
- // The height should be the parent height if we should expand.
- // Otherwise its the maximum column height for the frame. In that
- // case, the frame is first created with zero height and then
- // resized.
- let height = if regions.expand.y { region.y } else { Abs::zero() };
- let mut output = Frame::hard(Size::new(regions.size.x, height));
- let mut cursor = Abs::zero();
-
- for _ in 0..columns {
- let Some(frame) = frames.next() else { break };
- if !regions.expand.y {
- output.size_mut().y.set_max(frame.height());
- }
-
- let width = frame.width();
- let x = if dir == Dir::LTR {
- cursor
- } else {
- regions.size.x - cursor - width
- };
-
- output.push_frame(Point::with_x(x), frame);
- cursor += width + gutter;
- }
-
- finished.push(output);
- }
-
- Ok(Fragment::frames(finished))
- }
-}
-
-/// Forces a column break.
-///
-/// The function will behave like a [page break]($pagebreak) when used in a
-/// single column layout or the last column on a page. Otherwise, content after
-/// the column break will be placed in the next column.
-///
-/// # Example
-/// ```example
-/// #set page(columns: 2)
-/// Preliminary findings from our
-/// ongoing research project have
-/// revealed a hitherto unknown
-/// phenomenon of extraordinary
-/// significance.
-///
-/// #colbreak()
-/// Through rigorous experimentation
-/// and analysis, we have discovered
-/// a hitherto uncharacterized process
-/// that defies our current
-/// understanding of the fundamental
-/// laws of nature.
-/// ```
-#[elem(title = "Column Break", Behave)]
-pub struct ColbreakElem {
- /// If `{true}`, the column break is skipped if the current column is
- /// already empty.
- #[default(false)]
- pub weak: bool,
-}
-
-impl Behave for ColbreakElem {
- fn behaviour(&self) -> Behaviour {
- if self.weak(StyleChain::default()) {
- Behaviour::Weak(1)
- } else {
- Behaviour::Destructive
- }
- }
-}
diff --git a/crates/typst-library/src/layout/container.rs b/crates/typst-library/src/layout/container.rs
deleted file mode 100644
index 9268f8df..00000000
--- a/crates/typst-library/src/layout/container.rs
+++ /dev/null
@@ -1,502 +0,0 @@
-use typst::eval::AutoValue;
-
-use crate::layout::{Spacing, VElem};
-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
-/// Refer to the docs
-/// #box(
-/// height: 9pt,
-/// image("docs.svg")
-/// )
-/// for more information.
-/// ```
-#[elem(Layout)]
-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.
- #[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_map(regions.base(), |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);
- }
-
- // Prepare fill and stroke.
- let fill = self.fill(styles);
- let stroke = self.stroke(styles).map(|s| s.map(Stroke::unwrap_or_default));
-
- // Clip the contents
- if self.clip(styles) {
- let outset = self.outset(styles).relative_to(frame.size());
- let size = frame.size() + outset.sum_by_axis();
- let radius = self.radius(styles);
- frame.clip(clip_rect(size, radius, &stroke));
- }
-
- // 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);
- frame.set_kind(FrameKind::Hard);
-
- 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
-/// 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(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`]($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]($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 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_map(regions.base(), |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
- };
-
- // Prepare fill and stroke.
- let fill = self.fill(styles);
- let stroke = self.stroke(styles).map(|s| s.map(Stroke::unwrap_or_default));
-
- // Clip the contents
- if self.clip(styles) {
- for frame in frames.iter_mut() {
- let outset = self.outset(styles).relative_to(frame.size());
- let size = frame.size() + outset.sum_by_axis();
- let radius = self.radius(styles);
- frame.clip(clip_rect(size, radius, &stroke));
- }
- }
-
- // 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.set_kind(FrameKind::Hard);
- 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),
-}
diff --git a/crates/typst-library/src/layout/enum.rs b/crates/typst-library/src/layout/enum.rs
deleted file mode 100644
index 0c98c18a..00000000
--- a/crates/typst-library/src/layout/enum.rs
+++ /dev/null
@@ -1,319 +0,0 @@
-use std::str::FromStr;
-
-use crate::layout::{BlockElem, GridLayouter, ParElem, Sizing, Spacing};
-use crate::meta::{Numbering, NumberingPattern};
-use crate::prelude::*;
-use crate::text::TextElem;
-
-/// A numbered list.
-///
-/// Displays a sequence of items vertically and numbers them consecutively.
-///
-/// # Example
-/// ```example
-/// Automatically numbered:
-/// + Preparations
-/// + Analysis
-/// + Conclusions
-///
-/// Manually numbered:
-/// 2. What is the first step?
-/// 5. I am confused.
-/// + Moving on ...
-///
-/// Multiple lines:
-/// + This enum item has multiple
-/// lines because the next line
-/// is indented.
-///
-/// Function call.
-/// #enum[First][Second]
-/// ```
-///
-/// You can easily switch all your enumerations to a different numbering style
-/// with a set rule.
-/// ```example
-/// #set enum(numbering: "a)")
-///
-/// + Starting off ...
-/// + Don't forget step two
-/// ```
-///
-/// You can also use [`enum.item`]($enum.item) to programmatically customize the
-/// number of each item in the enumeration:
-///
-/// ```example
-/// #enum(
-/// enum.item(1)[First step],
-/// enum.item(5)[Fifth step],
-/// enum.item(10)[Tenth step]
-/// )
-/// ```
-///
-/// # Syntax
-/// This functions also has dedicated syntax:
-///
-/// - Starting a line with a plus sign creates an automatically numbered
-/// enumeration item.
-/// - Starting a line with a number followed by a dot creates an explicitly
-/// numbered enumeration item.
-///
-/// Enumeration items can contain multiple paragraphs and other block-level
-/// content. All content that is indented more than an item's marker becomes
-/// part of that item.
-#[elem(scope, title = "Numbered List", Layout)]
-pub struct EnumElem {
- /// If this is `{false}`, the items are spaced apart with
- /// [enum spacing]($enum.spacing). If it is `{true}`, they use normal
- /// [leading]($par.leading) instead. This makes the enumeration more
- /// compact, which can look better if the items are short.
- ///
- /// In markup mode, the value of this parameter is determined based on
- /// whether items are separated with a blank line. If items directly follow
- /// each other, this is set to `{true}`; if items are separated by a blank
- /// line, this is set to `{false}`.
- ///
- /// ```example
- /// + If an enum has a lot of text, and
- /// maybe other inline content, it
- /// should not be tight anymore.
- ///
- /// + To make an enum wide, simply
- /// insert a blank line between the
- /// items.
- /// ```
- #[default(true)]
- pub tight: bool,
-
- /// How to number the enumeration. Accepts a
- /// [numbering pattern or function]($numbering).
- ///
- /// If the numbering pattern contains multiple counting symbols, they apply
- /// to nested enums. If given a function, the function receives one argument
- /// if `full` is `{false}` and multiple arguments if `full` is `{true}`.
- ///
- /// ```example
- /// #set enum(numbering: "1.a)")
- /// + Different
- /// + Numbering
- /// + Nested
- /// + Items
- /// + Style
- ///
- /// #set enum(numbering: n => super[#n])
- /// + Superscript
- /// + Numbering!
- /// ```
- #[default(Numbering::Pattern(NumberingPattern::from_str("1.").unwrap()))]
- #[borrowed]
- pub numbering: Numbering,
-
- /// Which number to start the enumeration with.
- ///
- /// ```example
- /// #enum(
- /// start: 3,
- /// [Skipping],
- /// [Ahead],
- /// )
- /// ```
- #[default(1)]
- pub start: usize,
-
- /// Whether to display the full numbering, including the numbers of
- /// all parent enumerations.
- ///
- ///
- /// ```example
- /// #set enum(numbering: "1.a)", full: true)
- /// + Cook
- /// + Heat water
- /// + Add integredients
- /// + Eat
- /// ```
- #[default(false)]
- pub full: bool,
-
- /// The indentation of each item.
- #[resolve]
- pub indent: Length,
-
- /// The space between the numbering and the body of each item.
- #[resolve]
- #[default(Em::new(0.5).into())]
- pub body_indent: Length,
-
- /// The spacing between the items of a wide (non-tight) enumeration.
- ///
- /// If set to `{auto}`, uses the spacing [below blocks]($block.below).
- pub spacing: Smart<Spacing>,
-
- /// The horizontal alignment that enum numbers should have.
- ///
- /// By default, this is set to `{end}`, which aligns enum numbers
- /// towards end of the current text direction (in left-to-right script,
- /// for example, this is the same as `{right}`). The choice of `{end}`
- /// for horizontal alignment of enum numbers is usually preferred over
- /// `{start}`, as numbers then grow away from the text instead of towards
- /// it, avoiding certain visual issues. This option lets you override this
- /// behavior, however.
- ///
- /// ````example
- /// #set enum(number-align: start)
- ///
- /// Here are some powers of two:
- /// 1. One
- /// 2. Two
- /// 4. Four
- /// 8. Eight
- /// 16. Sixteen
- /// 32. Thirty two
- /// ````
- #[default(HAlign::End)]
- pub number_align: HAlign,
-
- /// The numbered list's items.
- ///
- /// When using the enum syntax, adjacent items are automatically collected
- /// into enumerations, even through constructs like for loops.
- ///
- /// ```example
- /// #for phase in (
- /// "Launch",
- /// "Orbit",
- /// "Descent",
- /// ) [+ #phase]
- /// ```
- #[variadic]
- pub children: Vec<EnumItem>,
-
- /// The numbers of parent items.
- #[internal]
- #[fold]
- parents: Parent,
-}
-
-#[scope]
-impl EnumElem {
- #[elem]
- type EnumItem;
-}
-
-impl Layout for EnumElem {
- #[tracing::instrument(name = "EnumElem::layout", skip_all)]
- fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- let numbering = self.numbering(styles);
- let indent = self.indent(styles);
- let body_indent = self.body_indent(styles);
- let gutter = if self.tight(styles) {
- ParElem::leading_in(styles).into()
- } else {
- self.spacing(styles)
- .unwrap_or_else(|| *BlockElem::below_in(styles).amount())
- };
-
- let mut cells = vec![];
- let mut number = self.start(styles);
- let mut parents = self.parents(styles);
- let full = self.full(styles);
-
- // Horizontally align based on the given respective parameter.
- // Vertically align to the top to avoid inheriting `horizon` or `bottom`
- // alignment from the context and having the number be displaced in
- // relation to the item it refers to.
- let number_align = self.number_align(styles) + VAlign::Top;
-
- for item in self.children() {
- number = item.number(styles).unwrap_or(number);
-
- let resolved = if full {
- parents.push(number);
- let content = numbering.apply_vt(vt, &parents)?.display();
- parents.pop();
- content
- } else {
- match numbering {
- Numbering::Pattern(pattern) => {
- TextElem::packed(pattern.apply_kth(parents.len(), number))
- }
- other => other.apply_vt(vt, &[number])?.display(),
- }
- };
-
- // Disable overhang as a workaround to end-aligned dots glitching
- // and decreasing spacing between numbers and items.
- let resolved =
- resolved.aligned(number_align).styled(TextElem::set_overhang(false));
-
- cells.push(Content::empty());
- cells.push(resolved);
- cells.push(Content::empty());
- cells.push(item.body().clone().styled(Self::set_parents(Parent(number))));
- number = number.saturating_add(1);
- }
-
- let layouter = GridLayouter::new(
- Axes::with_x(&[
- Sizing::Rel(indent.into()),
- Sizing::Auto,
- Sizing::Rel(body_indent.into()),
- Sizing::Auto,
- ]),
- Axes::with_y(&[gutter.into()]),
- &cells,
- regions,
- styles,
- self.span(),
- );
-
- Ok(layouter.layout(vt)?.fragment)
- }
-}
-
-/// An enumeration item.
-#[elem(name = "item", title = "Numbered List Item")]
-pub struct EnumItem {
- /// The item's number.
- #[positional]
- pub number: Option<usize>,
-
- /// The item's body.
- #[required]
- pub body: Content,
-}
-
-cast! {
- EnumItem,
- array: Array => {
- let mut iter = array.into_iter();
- let (number, body) = match (iter.next(), iter.next(), iter.next()) {
- (Some(a), Some(b), None) => (a.cast()?, b.cast()?),
- _ => bail!("array must contain exactly two entries"),
- };
- Self::new(body).with_number(number)
- },
- v: Content => v.to::<Self>().cloned().unwrap_or_else(|| Self::new(v.clone())),
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Hash)]
-struct Parent(usize);
-
-cast! {
- Parent,
- self => self.0.into_value(),
- v: usize => Self(v),
-}
-
-impl Fold for Parent {
- type Output = Vec<usize>;
-
- fn fold(self, mut outer: Self::Output) -> Self::Output {
- outer.push(self.0);
- outer
- }
-}
diff --git a/crates/typst-library/src/layout/flow.rs b/crates/typst-library/src/layout/flow.rs
deleted file mode 100644
index 17a39254..00000000
--- a/crates/typst-library/src/layout/flow.rs
+++ /dev/null
@@ -1,714 +0,0 @@
-use std::mem;
-
-use comemo::Prehashed;
-
-use crate::layout::{
- AlignElem, BlockElem, ColbreakElem, ColumnsElem, ParElem, PlaceElem, Spacing, VElem,
-};
-use crate::meta::{FootnoteElem, FootnoteEntry};
-use crate::prelude::*;
-use crate::visualize::{
- CircleElem, EllipseElem, ImageElem, LineElem, PathElem, PolygonElem, RectElem,
- SquareElem,
-};
-
-/// Arranges spacing, paragraphs and block-level elements into a flow.
-///
-/// This element is responsible for layouting both the top-level content flow
-/// and the contents of boxes.
-#[elem(Layout)]
-pub struct FlowElem {
- /// The children that will be arranges into a flow.
- #[variadic]
- pub children: Vec<Prehashed<Content>>,
-}
-
-impl Layout for FlowElem {
- #[tracing::instrument(name = "FlowElem::layout", skip_all)]
- fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- if !regions.size.x.is_finite() && regions.expand.x {
- bail!(error!(self.span(), "cannot expand into infinite width"));
- }
- if !regions.size.y.is_finite() && regions.expand.y {
- bail!(error!(self.span(), "cannot expand into infinite height"));
- }
- let mut layouter = FlowLayouter::new(regions, styles);
-
- for mut child in self.children().iter().map(|c| &**c) {
- let outer = styles;
- let mut styles = styles;
- if let Some((elem, map)) = child.to_styled() {
- child = elem;
- styles = outer.chain(map);
- }
-
- if let Some(elem) = child.to::<VElem>() {
- layouter.layout_spacing(vt, elem, styles)?;
- } else if let Some(elem) = child.to::<ParElem>() {
- layouter.layout_par(vt, elem, styles)?;
- } else if child.is::<LineElem>()
- || child.is::<RectElem>()
- || child.is::<SquareElem>()
- || child.is::<EllipseElem>()
- || child.is::<CircleElem>()
- || child.is::<ImageElem>()
- || child.is::<PolygonElem>()
- || child.is::<PathElem>()
- {
- let layoutable = child.with::<dyn Layout>().unwrap();
- layouter.layout_single(vt, layoutable, styles)?;
- } else if child.is::<MetaElem>() {
- let mut frame = Frame::soft(Size::zero());
- frame.meta(styles, true);
- layouter.items.push(FlowItem::Frame {
- frame,
- align: Axes::splat(FixedAlign::Start),
- sticky: true,
- movable: false,
- });
- } else if let Some(placed) = child.to::<PlaceElem>() {
- layouter.layout_placed(vt, placed, styles)?;
- } else if child.can::<dyn Layout>() {
- layouter.layout_multiple(vt, child, styles)?;
- } else if child.is::<ColbreakElem>() {
- if !layouter.regions.backlog.is_empty() || layouter.regions.last.is_some()
- {
- layouter.finish_region(vt)?;
- }
- } else {
- bail!(child.span(), "unexpected flow child");
- }
- }
-
- layouter.finish(vt)
- }
-}
-
-/// Performs flow layout.
-struct FlowLayouter<'a> {
- /// Whether this is the root flow.
- root: bool,
- /// The regions to layout children into.
- regions: Regions<'a>,
- /// The shared styles.
- styles: StyleChain<'a>,
- /// Whether the flow should expand to fill the region.
- expand: Axes<bool>,
- /// The initial size of `regions.size` that was available before we started
- /// subtracting.
- initial: Size,
- /// Whether the last block was a paragraph.
- last_was_par: bool,
- /// Spacing and layouted blocks for the current region.
- items: Vec<FlowItem>,
- /// A queue of floats.
- pending_floats: Vec<FlowItem>,
- /// Whether we have any footnotes in the current region.
- has_footnotes: bool,
- /// Footnote configuration.
- footnote_config: FootnoteConfig,
- /// Finished frames for previous regions.
- finished: Vec<Frame>,
-}
-
-/// Cached footnote configuration.
-struct FootnoteConfig {
- separator: Content,
- clearance: Abs,
- gap: Abs,
-}
-
-/// A prepared item in a flow layout.
-#[derive(Debug)]
-enum FlowItem {
- /// Spacing between other items and whether it is weak.
- Absolute(Abs, bool),
- /// Fractional spacing between other items.
- Fractional(Fr),
- /// A frame for a layouted block, how to align it, whether it sticks to the
- /// item after it (for orphan prevention), and whether it is movable
- /// (to keep it together with its footnotes).
- Frame { frame: Frame, align: Axes<FixedAlign>, sticky: bool, movable: bool },
- /// An absolutely placed frame.
- Placed {
- frame: Frame,
- x_align: FixedAlign,
- y_align: Smart<Option<FixedAlign>>,
- delta: Axes<Rel<Abs>>,
- float: bool,
- clearance: Abs,
- },
- /// A footnote frame (can also be the separator).
- Footnote(Frame),
-}
-
-impl FlowItem {
- /// The inherent height of the item.
- fn height(&self) -> Abs {
- match self {
- Self::Absolute(v, _) => *v,
- Self::Fractional(_) | Self::Placed { .. } => Abs::zero(),
- Self::Frame { frame, .. } | Self::Footnote(frame) => frame.height(),
- }
- }
-}
-
-impl<'a> FlowLayouter<'a> {
- /// Create a new flow layouter.
- fn new(mut regions: Regions<'a>, styles: StyleChain<'a>) -> Self {
- let expand = regions.expand;
-
- // Disable vertical expansion & root for children.
- regions.expand.y = false;
- let root = mem::replace(&mut regions.root, false);
-
- Self {
- root,
- regions,
- styles,
- expand,
- initial: regions.size,
- last_was_par: false,
- items: vec![],
- pending_floats: vec![],
- has_footnotes: false,
- footnote_config: FootnoteConfig {
- separator: FootnoteEntry::separator_in(styles),
- clearance: FootnoteEntry::clearance_in(styles),
- gap: FootnoteEntry::gap_in(styles),
- },
- finished: vec![],
- }
- }
-
- /// Layout vertical spacing.
- #[tracing::instrument(name = "FlowLayouter::layout_spacing", skip_all)]
- fn layout_spacing(
- &mut self,
- vt: &mut Vt,
- v: &VElem,
- styles: StyleChain,
- ) -> SourceResult<()> {
- self.layout_item(
- vt,
- match v.amount() {
- Spacing::Rel(rel) => FlowItem::Absolute(
- rel.resolve(styles).relative_to(self.initial.y),
- v.weakness(styles) > 0,
- ),
- Spacing::Fr(fr) => FlowItem::Fractional(*fr),
- },
- )
- }
-
- /// Layout a paragraph.
- #[tracing::instrument(name = "FlowLayouter::layout_par", skip_all)]
- fn layout_par(
- &mut self,
- vt: &mut Vt,
- par: &ParElem,
- styles: StyleChain,
- ) -> SourceResult<()> {
- let align = AlignElem::alignment_in(styles).resolve(styles);
- let leading = ParElem::leading_in(styles);
- let consecutive = self.last_was_par;
- let lines = par
- .layout(vt, styles, consecutive, self.regions.base(), self.regions.expand.x)?
- .into_frames();
-
- let mut sticky = self.items.len();
- for (i, item) in self.items.iter().enumerate().rev() {
- match *item {
- FlowItem::Absolute(_, _) => {}
- FlowItem::Frame { sticky: true, .. } => sticky = i,
- _ => break,
- }
- }
-
- if let Some(first) = lines.first() {
- if !self.regions.size.y.fits(first.height()) && !self.regions.in_last() {
- let carry: Vec<_> = self.items.drain(sticky..).collect();
- self.finish_region(vt)?;
- for item in carry {
- self.layout_item(vt, item)?;
- }
- }
- }
-
- for (i, frame) in lines.into_iter().enumerate() {
- if i > 0 {
- self.layout_item(vt, FlowItem::Absolute(leading, true))?;
- }
-
- self.layout_item(
- vt,
- FlowItem::Frame { frame, align, sticky: false, movable: true },
- )?;
- }
-
- self.last_was_par = true;
- Ok(())
- }
-
- /// Layout into a single region.
- #[tracing::instrument(name = "FlowLayouter::layout_single", skip_all)]
- fn layout_single(
- &mut self,
- vt: &mut Vt,
- content: &dyn Layout,
- styles: StyleChain,
- ) -> SourceResult<()> {
- let align = AlignElem::alignment_in(styles).resolve(styles);
- let sticky = BlockElem::sticky_in(styles);
- let pod = Regions::one(self.regions.base(), Axes::splat(false));
- let frame = content.layout(vt, styles, pod)?.into_frame();
- self.layout_item(vt, FlowItem::Frame { frame, align, sticky, movable: true })?;
- self.last_was_par = false;
- Ok(())
- }
-
- /// Layout a placed element.
- fn layout_placed(
- &mut self,
- vt: &mut Vt,
- placed: &PlaceElem,
- styles: StyleChain,
- ) -> SourceResult<()> {
- let float = placed.float(styles);
- let clearance = placed.clearance(styles);
- let alignment = placed.alignment(styles);
- let delta = Axes::new(placed.dx(styles), placed.dy(styles)).resolve(styles);
- let x_align = alignment.map_or(FixedAlign::Center, |align| {
- align.x().unwrap_or_default().resolve(styles)
- });
- let y_align = alignment.map(|align| align.y().map(VAlign::fix));
- let frame = placed.layout(vt, styles, self.regions)?.into_frame();
- let item = FlowItem::Placed { frame, x_align, y_align, delta, float, clearance };
- self.layout_item(vt, item)
- }
-
- /// Layout into multiple regions.
- fn layout_multiple(
- &mut self,
- vt: &mut Vt,
- block: &Content,
- styles: StyleChain,
- ) -> SourceResult<()> {
- // Temporarily delegerate rootness to the columns.
- let is_root = self.root;
- if is_root && block.is::<ColumnsElem>() {
- self.root = false;
- self.regions.root = true;
- }
-
- let mut notes = Vec::new();
-
- if self.regions.is_full() {
- // Skip directly if region is already full.
- self.finish_region(vt)?;
- }
-
- // How to align the block.
- let align = if let Some(align) = block.to::<AlignElem>() {
- align.alignment(styles)
- } else if let Some((_, local)) = block.to_styled() {
- AlignElem::alignment_in(styles.chain(local))
- } else {
- AlignElem::alignment_in(styles)
- }
- .resolve(styles);
-
- // Layout the block itself.
- let sticky = BlockElem::sticky_in(styles);
- let fragment = block.layout(vt, styles, self.regions)?;
-
- for (i, frame) in fragment.into_iter().enumerate() {
- // Find footnotes in the frame.
- if self.root {
- find_footnotes(&mut notes, &frame);
- }
-
- if i > 0 {
- self.finish_region(vt)?;
- }
-
- let item = FlowItem::Frame { frame, align, sticky, movable: false };
- self.layout_item(vt, item)?;
- }
-
- self.try_handle_footnotes(vt, notes)?;
-
- self.root = is_root;
- self.regions.root = false;
- self.last_was_par = false;
-
- Ok(())
- }
-
- /// Layout a finished frame.
- #[tracing::instrument(name = "FlowLayouter::layout_item", skip_all)]
- fn layout_item(&mut self, vt: &mut Vt, mut item: FlowItem) -> SourceResult<()> {
- match item {
- FlowItem::Absolute(v, weak) => {
- if weak
- && !self
- .items
- .iter()
- .any(|item| matches!(item, FlowItem::Frame { .. }))
- {
- return Ok(());
- }
- self.regions.size.y -= v
- }
- FlowItem::Fractional(_) => {}
- FlowItem::Frame { ref frame, movable, .. } => {
- let height = frame.height();
- if !self.regions.size.y.fits(height) && !self.regions.in_last() {
- self.finish_region(vt)?;
- }
-
- self.regions.size.y -= height;
- if self.root && movable {
- let mut notes = Vec::new();
- find_footnotes(&mut notes, frame);
- self.items.push(item);
- if !self.handle_footnotes(vt, &mut notes, true, false)? {
- let item = self.items.pop();
- self.finish_region(vt)?;
- self.items.extend(item);
- self.regions.size.y -= height;
- self.handle_footnotes(vt, &mut notes, true, true)?;
- }
- return Ok(());
- }
- }
- FlowItem::Placed { float: false, .. } => {}
- FlowItem::Placed {
- ref mut frame,
- ref mut y_align,
- float: true,
- clearance,
- ..
- } => {
- // If the float doesn't fit, queue it for the next region.
- if !self.regions.size.y.fits(frame.height() + clearance)
- && !self.regions.in_last()
- {
- self.pending_floats.push(item);
- return Ok(());
- }
-
- // Select the closer placement, top or bottom.
- if y_align.is_auto() {
- let ratio = (self.regions.size.y
- - (frame.height() + clearance) / 2.0)
- / self.regions.full;
- let better_align =
- if ratio <= 0.5 { FixedAlign::End } else { FixedAlign::Start };
- *y_align = Smart::Custom(Some(better_align));
- }
-
- // Add some clearance so that the float doesn't touch the main
- // content.
- frame.size_mut().y += clearance;
- if *y_align == Smart::Custom(Some(FixedAlign::End)) {
- frame.translate(Point::with_y(clearance));
- }
-
- self.regions.size.y -= frame.height();
-
- // Find footnotes in the frame.
- if self.root {
- let mut notes = vec![];
- find_footnotes(&mut notes, frame);
- self.try_handle_footnotes(vt, notes)?;
- }
- }
- FlowItem::Footnote(_) => {}
- }
-
- self.items.push(item);
- Ok(())
- }
-
- /// Finish the frame for one region.
- fn finish_region(&mut self, vt: &mut Vt) -> SourceResult<()> {
- // Trim weak spacing.
- while self
- .items
- .last()
- .map_or(false, |item| matches!(item, FlowItem::Absolute(_, true)))
- {
- self.items.pop();
- }
-
- // Determine the used size.
- let mut fr = Fr::zero();
- let mut used = Size::zero();
- let mut footnote_height = Abs::zero();
- let mut float_top_height = Abs::zero();
- let mut float_bottom_height = Abs::zero();
- let mut first_footnote = true;
- for item in &self.items {
- match item {
- FlowItem::Absolute(v, _) => used.y += *v,
- FlowItem::Fractional(v) => fr += *v,
- FlowItem::Frame { frame, .. } => {
- used.y += frame.height();
- used.x.set_max(frame.width());
- }
- FlowItem::Placed { float: false, .. } => {}
- FlowItem::Placed { frame, float: true, y_align, .. } => match y_align {
- Smart::Custom(Some(FixedAlign::Start)) => {
- float_top_height += frame.height()
- }
- Smart::Custom(Some(FixedAlign::End)) => {
- float_bottom_height += frame.height()
- }
- _ => {}
- },
- FlowItem::Footnote(frame) => {
- footnote_height += frame.height();
- if !first_footnote {
- footnote_height += self.footnote_config.gap;
- }
- first_footnote = false;
- used.x.set_max(frame.width());
- }
- }
- }
- used.y += footnote_height + float_top_height + float_bottom_height;
-
- // Determine the size of the flow in this region depending on whether
- // the region expands. Also account for fractional spacing and
- // footnotes.
- let mut size = self.expand.select(self.initial, used).min(self.initial);
- if (fr.get() > 0.0 || self.has_footnotes) && self.initial.y.is_finite() {
- size.y = self.initial.y;
- }
-
- let mut output = Frame::soft(size);
- let mut ruler = FixedAlign::Start;
- let mut float_top_offset = Abs::zero();
- let mut offset = float_top_height;
- let mut float_bottom_offset = Abs::zero();
- let mut footnote_offset = Abs::zero();
-
- // Place all frames.
- for item in self.items.drain(..) {
- match item {
- FlowItem::Absolute(v, _) => {
- offset += v;
- }
- FlowItem::Fractional(v) => {
- let remaining = self.initial.y - used.y;
- offset += v.share(fr, remaining);
- }
- FlowItem::Frame { frame, align, .. } => {
- ruler = ruler.max(align.y);
- let x = align.x.position(size.x - frame.width());
- let y = offset + ruler.position(size.y - used.y);
- let pos = Point::new(x, y);
- offset += frame.height();
- output.push_frame(pos, frame);
- }
- FlowItem::Placed { frame, x_align, y_align, delta, float, .. } => {
- let x = x_align.position(size.x - frame.width());
- let y = if float {
- match y_align {
- Smart::Custom(Some(FixedAlign::Start)) => {
- let y = float_top_offset;
- float_top_offset += frame.height();
- y
- }
- Smart::Custom(Some(FixedAlign::End)) => {
- let y = size.y - footnote_height - float_bottom_height
- + float_bottom_offset;
- float_bottom_offset += frame.height();
- y
- }
- _ => unreachable!("float must be y aligned"),
- }
- } else {
- match y_align {
- Smart::Custom(Some(align)) => {
- align.position(size.y - frame.height())
- }
- _ => offset + ruler.position(size.y - used.y),
- }
- };
-
- let pos = Point::new(x, y)
- + delta.zip_map(size, Rel::relative_to).to_point();
-
- output.push_frame(pos, frame);
- }
- FlowItem::Footnote(frame) => {
- let y = size.y - footnote_height + footnote_offset;
- footnote_offset += frame.height() + self.footnote_config.gap;
- output.push_frame(Point::with_y(y), frame);
- }
- }
- }
-
- // Advance to the next region.
- self.finished.push(output);
- self.regions.next();
- self.initial = self.regions.size;
- self.has_footnotes = false;
-
- // Try to place floats.
- for item in mem::take(&mut self.pending_floats) {
- self.layout_item(vt, item)?;
- }
-
- Ok(())
- }
-
- /// Finish layouting and return the resulting fragment.
- fn finish(mut self, vt: &mut Vt) -> SourceResult<Fragment> {
- if self.expand.y {
- while !self.regions.backlog.is_empty() {
- self.finish_region(vt)?;
- }
- }
-
- self.finish_region(vt)?;
- while !self.items.is_empty() {
- self.finish_region(vt)?;
- }
-
- Ok(Fragment::frames(self.finished))
- }
-}
-
-impl FlowLayouter<'_> {
- fn try_handle_footnotes(
- &mut self,
- vt: &mut Vt,
- mut notes: Vec<FootnoteElem>,
- ) -> SourceResult<()> {
- if self.root && !self.handle_footnotes(vt, &mut notes, false, false)? {
- self.finish_region(vt)?;
- self.handle_footnotes(vt, &mut notes, false, true)?;
- }
- Ok(())
- }
-
- /// Processes all footnotes in the frame.
- #[tracing::instrument(skip_all)]
- fn handle_footnotes(
- &mut self,
- vt: &mut Vt,
- notes: &mut Vec<FootnoteElem>,
- movable: bool,
- force: bool,
- ) -> SourceResult<bool> {
- let items_len = self.items.len();
- let notes_len = notes.len();
-
- // Process footnotes one at a time.
- let mut k = 0;
- while k < notes.len() {
- if notes[k].is_ref() {
- k += 1;
- continue;
- }
-
- if !self.has_footnotes {
- self.layout_footnote_separator(vt)?;
- }
-
- self.regions.size.y -= self.footnote_config.gap;
- let checkpoint = vt.locator.clone();
- let frames = FootnoteEntry::new(notes[k].clone())
- .pack()
- .layout(vt, self.styles, self.regions.with_root(false))?
- .into_frames();
-
- // If the entries didn't fit, abort (to keep footnote and entry
- // together).
- if !force
- && (k == 0 || movable)
- && frames.first().map_or(false, Frame::is_empty)
- {
- // Remove existing footnotes attempts because we need to
- // move the item to the next page.
- notes.truncate(notes_len);
-
- // Undo region modifications.
- for item in self.items.drain(items_len..) {
- self.regions.size.y -= item.height();
- }
-
- // Undo Vt modifications.
- *vt.locator = checkpoint;
-
- return Ok(false);
- }
-
- let prev = notes.len();
- for (i, frame) in frames.into_iter().enumerate() {
- find_footnotes(notes, &frame);
- if i > 0 {
- self.finish_region(vt)?;
- self.layout_footnote_separator(vt)?;
- self.regions.size.y -= self.footnote_config.gap;
- }
- self.regions.size.y -= frame.height();
- self.items.push(FlowItem::Footnote(frame));
- }
-
- k += 1;
-
- // Process the nested notes before dealing with further top-level
- // notes.
- let nested = notes.len() - prev;
- if nested > 0 {
- notes[k..].rotate_right(nested);
- }
- }
-
- Ok(true)
- }
-
- /// Layout and save the footnote separator, typically a line.
- #[tracing::instrument(skip_all)]
- fn layout_footnote_separator(&mut self, vt: &mut Vt) -> SourceResult<()> {
- let expand = Axes::new(self.regions.expand.x, false);
- let pod = Regions::one(self.regions.base(), expand);
- let separator = &self.footnote_config.separator;
-
- let mut frame = separator.layout(vt, self.styles, pod)?.into_frame();
- frame.size_mut().y += self.footnote_config.clearance;
- frame.translate(Point::with_y(self.footnote_config.clearance));
-
- self.has_footnotes = true;
- self.regions.size.y -= frame.height();
- self.items.push(FlowItem::Footnote(frame));
-
- Ok(())
- }
-}
-
-/// Finds all footnotes in the frame.
-#[tracing::instrument(skip_all)]
-fn find_footnotes(notes: &mut Vec<FootnoteElem>, frame: &Frame) {
- for (_, item) in frame.items() {
- match item {
- FrameItem::Group(group) => find_footnotes(notes, &group.frame),
- FrameItem::Meta(Meta::Elem(content), _)
- if !notes.iter().any(|note| note.location() == content.location()) =>
- {
- let Some(footnote) = content.to::<FootnoteElem>() else { continue };
- notes.push(footnote.clone());
- }
- _ => {}
- }
- }
-}
diff --git a/crates/typst-library/src/layout/fragment.rs b/crates/typst-library/src/layout/fragment.rs
deleted file mode 100644
index 3550df2a..00000000
--- a/crates/typst-library/src/layout/fragment.rs
+++ /dev/null
@@ -1,87 +0,0 @@
-use crate::prelude::*;
-
-/// A partial layout result.
-#[derive(Clone)]
-pub struct Fragment(Vec<Frame>);
-
-impl Fragment {
- /// Create a fragment from a single frame.
- pub fn frame(frame: Frame) -> Self {
- Self(vec![frame])
- }
-
- /// Create a fragment from multiple frames.
- pub fn frames(frames: Vec<Frame>) -> Self {
- Self(frames)
- }
-
- /// Return `true` if the length is 0.
- pub fn is_empty(&self) -> bool {
- self.0.is_empty()
- }
-
- /// The number of frames in the fragment.
- pub fn len(&self) -> usize {
- self.0.len()
- }
-
- /// Extract the first and only frame.
- ///
- /// Panics if there are multiple frames.
- #[track_caller]
- pub fn into_frame(self) -> Frame {
- assert_eq!(self.0.len(), 1, "expected exactly one frame");
- self.0.into_iter().next().unwrap()
- }
-
- /// Extract the frames.
- pub fn into_frames(self) -> Vec<Frame> {
- self.0
- }
-
- /// Iterate over the contained frames.
- pub fn iter(&self) -> std::slice::Iter<Frame> {
- self.0.iter()
- }
-
- /// Iterate over the contained frames.
- pub fn iter_mut(&mut self) -> std::slice::IterMut<Frame> {
- self.0.iter_mut()
- }
-}
-
-impl Debug for Fragment {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- match self.0.as_slice() {
- [frame] => frame.fmt(f),
- frames => frames.fmt(f),
- }
- }
-}
-
-impl IntoIterator for Fragment {
- type Item = Frame;
- type IntoIter = std::vec::IntoIter<Frame>;
-
- fn into_iter(self) -> Self::IntoIter {
- self.0.into_iter()
- }
-}
-
-impl<'a> IntoIterator for &'a Fragment {
- type Item = &'a Frame;
- type IntoIter = std::slice::Iter<'a, Frame>;
-
- fn into_iter(self) -> Self::IntoIter {
- self.0.iter()
- }
-}
-
-impl<'a> IntoIterator for &'a mut Fragment {
- type Item = &'a mut Frame;
- type IntoIter = std::slice::IterMut<'a, Frame>;
-
- fn into_iter(self) -> Self::IntoIter {
- self.0.iter_mut()
- }
-}
diff --git a/crates/typst-library/src/layout/grid.rs b/crates/typst-library/src/layout/grid.rs
deleted file mode 100644
index 1cde6564..00000000
--- a/crates/typst-library/src/layout/grid.rs
+++ /dev/null
@@ -1,734 +0,0 @@
-use smallvec::{smallvec, SmallVec};
-
-use crate::layout::Sizing;
-use crate::prelude::*;
-use crate::text::TextElem;
-
-/// Arranges content in a grid.
-///
-/// The grid element allows you to arrange content in a grid. You can define the
-/// number of rows and columns, as well as the size of the gutters between them.
-/// There are multiple sizing modes for columns and rows that can be used to
-/// create complex layouts.
-///
-/// The sizing of the grid is determined by the track sizes specified in the
-/// arguments. Because each of the sizing parameters accepts the same values, we
-/// will explain them just once, here. Each sizing argument accepts an array of
-/// individual track sizes. A track size is either:
-///
-/// - `{auto}`: The track will be sized to fit its contents. It will be at most
-/// as large as the remaining space. If there is more than one `{auto}` track
-/// which, and together they claim more than the available space, the `{auto}`
-/// tracks will fairly distribute the available space among themselves.
-///
-/// - A fixed or relative length (e.g. `{10pt}` or `{20% - 1cm}`): The track
-/// will be exactly of this size.
-///
-/// - A fractional length (e.g. `{1fr}`): Once all other tracks have been sized,
-/// the remaining space will be divided among the fractional tracks according
-/// to their fractions. For example, if there are two fractional tracks, each
-/// with a fraction of `{1fr}`, they will each take up half of the remaining
-/// space.
-///
-/// To specify a single track, the array can be omitted in favor of a single
-/// value. To specify multiple `{auto}` tracks, enter the number of tracks
-/// instead of an array. For example, `columns:` `{3}` is equivalent to
-/// `columns:` `{(auto, auto, auto)}`.
-///
-/// # Examples
-/// The example below demonstrates the different track sizing options.
-///
-/// ```example
-/// // We use `rect` to emphasize the
-/// // area of cells.
-/// #set rect(
-/// inset: 8pt,
-/// fill: rgb("e4e5ea"),
-/// width: 100%,
-/// )
-///
-/// #grid(
-/// columns: (60pt, 1fr, 2fr),
-/// rows: (auto, 60pt),
-/// gutter: 3pt,
-/// rect[Fixed width, auto height],
-/// rect[1/3 of the remains],
-/// rect[2/3 of the remains],
-/// rect(height: 100%)[Fixed height],
-/// image("tiger.jpg", height: 100%),
-/// image("tiger.jpg", height: 100%),
-/// )
-/// ```
-///
-/// You can also [spread]($arguments/#spreading) an array of strings or content
-/// into a grid to populate its cells.
-///
-/// ```example
-/// #grid(
-/// columns: 5,
-/// gutter: 5pt,
-/// ..range(25).map(str)
-/// )
-/// ```
-#[elem(Layout)]
-pub struct GridElem {
- /// The column sizes.
- ///
- /// Either specify a track size array or provide an integer to create a grid
- /// with that many `{auto}`-sized columns. Note that opposed to rows and
- /// gutters, providing a single track size will only ever create a single
- /// column.
- #[borrowed]
- pub columns: TrackSizings,
-
- /// The row sizes.
- ///
- /// If there are more cells than fit the defined rows, the last row is
- /// repeated until there are no more cells.
- #[borrowed]
- pub rows: TrackSizings,
-
- /// The gaps between rows & columns.
- ///
- /// If there are more gutters than defined sizes, the last gutter is repeated.
- #[external]
- pub gutter: TrackSizings,
-
- /// The gaps between columns. Takes precedence over `gutter`.
- #[parse(
- let gutter = args.named("gutter")?;
- args.named("column-gutter")?.or_else(|| gutter.clone())
- )]
- #[borrowed]
- pub column_gutter: TrackSizings,
-
- /// The gaps between rows. Takes precedence over `gutter`.
- #[parse(args.named("row-gutter")?.or_else(|| gutter.clone()))]
- #[borrowed]
- pub row_gutter: TrackSizings,
-
- /// The contents of the grid cells.
- ///
- /// The cells are populated in row-major order.
- #[variadic]
- pub children: Vec<Content>,
-}
-
-impl Layout for GridElem {
- #[tracing::instrument(name = "GridElem::layout", skip_all)]
- fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- let columns = self.columns(styles);
- let rows = self.rows(styles);
- let column_gutter = self.column_gutter(styles);
- let row_gutter = self.row_gutter(styles);
-
- // Prepare grid layout by unifying content and gutter tracks.
- let layouter = GridLayouter::new(
- Axes::new(&columns.0, &rows.0),
- Axes::new(&column_gutter.0, &row_gutter.0),
- &self.children,
- regions,
- styles,
- self.span(),
- );
-
- // Measure the columns and layout the grid row-by-row.
- Ok(layouter.layout(vt)?.fragment)
- }
-}
-
-/// Track sizing definitions.
-#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
-pub struct TrackSizings(pub SmallVec<[Sizing; 4]>);
-
-cast! {
- TrackSizings,
- self => self.0.into_value(),
- sizing: Sizing => Self(smallvec![sizing]),
- count: NonZeroUsize => Self(smallvec![Sizing::Auto; count.get()]),
- values: Array => Self(values.into_iter().map(Value::cast).collect::<StrResult<_>>()?),
-}
-
-/// Performs grid layout.
-pub struct GridLayouter<'a> {
- /// The grid cells.
- cells: &'a [Content],
- /// Whether this is an RTL grid.
- is_rtl: bool,
- /// Whether this grid has gutters.
- has_gutter: bool,
- /// The column tracks including gutter tracks.
- cols: Vec<Sizing>,
- /// The row tracks including gutter tracks.
- rows: Vec<Sizing>,
- /// The regions to layout children into.
- regions: Regions<'a>,
- /// The inherited styles.
- styles: StyleChain<'a>,
- /// Resolved column sizes.
- rcols: Vec<Abs>,
- /// The sum of `rcols`.
- width: Abs,
- /// Resolve row sizes, by region.
- rrows: Vec<Vec<RowPiece>>,
- /// Rows in the current region.
- lrows: Vec<Row>,
- /// The initial size of the current region before we started subtracting.
- initial: Size,
- /// Frames for finished regions.
- finished: Vec<Frame>,
- /// The span of the grid element.
- span: Span,
-}
-
-/// The resulting sizes of columns and rows in a grid.
-#[derive(Debug)]
-pub struct GridLayout {
- /// The fragment.
- pub fragment: Fragment,
- /// The column widths.
- pub cols: Vec<Abs>,
- /// The heights of the resulting rows segments, by region.
- pub rows: Vec<Vec<RowPiece>>,
-}
-
-/// Details about a resulting row piece.
-#[derive(Debug)]
-pub struct RowPiece {
- /// The height of the segment.
- pub height: Abs,
- /// The index of the row.
- pub y: usize,
-}
-
-/// Produced by initial row layout, auto and relative rows are already finished,
-/// fractional rows not yet.
-enum Row {
- /// Finished row frame of auto or relative row with y index.
- Frame(Frame, usize),
- /// Fractional row with y index.
- Fr(Fr, usize),
-}
-
-impl<'a> GridLayouter<'a> {
- /// Create a new grid layouter.
- ///
- /// This prepares grid layout by unifying content and gutter tracks.
- pub fn new(
- tracks: Axes<&[Sizing]>,
- gutter: Axes<&[Sizing]>,
- cells: &'a [Content],
- regions: Regions<'a>,
- styles: StyleChain<'a>,
- span: Span,
- ) -> Self {
- let mut cols = vec![];
- let mut rows = vec![];
-
- // Number of content columns: Always at least one.
- let c = tracks.x.len().max(1);
-
- // Number of content rows: At least as many as given, but also at least
- // as many as needed to place each item.
- let r = {
- let len = cells.len();
- let given = tracks.y.len();
- let needed = len / c + (len % c).clamp(0, 1);
- given.max(needed)
- };
-
- let has_gutter = gutter.any(|tracks| !tracks.is_empty());
- let auto = Sizing::Auto;
- let zero = Sizing::Rel(Rel::zero());
- let get_or = |tracks: &[_], idx, default| {
- tracks.get(idx).or(tracks.last()).copied().unwrap_or(default)
- };
-
- // Collect content and gutter columns.
- for x in 0..c {
- cols.push(get_or(tracks.x, x, auto));
- if has_gutter {
- cols.push(get_or(gutter.x, x, zero));
- }
- }
-
- // Collect content and gutter rows.
- for y in 0..r {
- rows.push(get_or(tracks.y, y, auto));
- if has_gutter {
- rows.push(get_or(gutter.y, y, zero));
- }
- }
-
- // Remove superfluous gutter tracks.
- if has_gutter {
- cols.pop();
- rows.pop();
- }
-
- // Reverse for RTL.
- let is_rtl = TextElem::dir_in(styles) == Dir::RTL;
- if is_rtl {
- cols.reverse();
- }
-
- // We use these regions for auto row measurement. Since at that moment,
- // columns are already sized, we can enable horizontal expansion.
- let mut regions = regions;
- regions.expand = Axes::new(true, false);
-
- Self {
- cells,
- is_rtl,
- has_gutter,
- rows,
- regions,
- styles,
- rcols: vec![Abs::zero(); cols.len()],
- cols,
- width: Abs::zero(),
- rrows: vec![],
- lrows: vec![],
- initial: regions.size,
- finished: vec![],
- span,
- }
- }
-
- /// Determines the columns sizes and then layouts the grid row-by-row.
- pub fn layout(mut self, vt: &mut Vt) -> SourceResult<GridLayout> {
- self.measure_columns(vt)?;
-
- for y in 0..self.rows.len() {
- // Skip to next region if current one is full, but only for content
- // rows, not for gutter rows.
- if self.regions.is_full() && (!self.has_gutter || y % 2 == 0) {
- self.finish_region(vt)?;
- }
-
- match self.rows[y] {
- Sizing::Auto => self.layout_auto_row(vt, y)?,
- Sizing::Rel(v) => self.layout_relative_row(vt, v, y)?,
- Sizing::Fr(v) => self.lrows.push(Row::Fr(v, y)),
- }
- }
-
- self.finish_region(vt)?;
-
- Ok(GridLayout {
- fragment: Fragment::frames(self.finished),
- cols: self.rcols,
- rows: self.rrows,
- })
- }
-
- /// Determine all column sizes.
- #[tracing::instrument(name = "GridLayouter::measure_columns", skip_all)]
- fn measure_columns(&mut self, vt: &mut Vt) -> SourceResult<()> {
- // Sum of sizes of resolved relative tracks.
- let mut rel = Abs::zero();
-
- // Sum of fractions of all fractional tracks.
- let mut fr = Fr::zero();
-
- // Resolve the size of all relative columns and compute the sum of all
- // fractional tracks.
- for (&col, rcol) in self.cols.iter().zip(&mut self.rcols) {
- match col {
- Sizing::Auto => {}
- Sizing::Rel(v) => {
- let resolved =
- v.resolve(self.styles).relative_to(self.regions.base().x);
- *rcol = resolved;
- rel += resolved;
- }
- Sizing::Fr(v) => fr += v,
- }
- }
-
- // Size that is not used by fixed-size columns.
- let available = self.regions.size.x - rel;
- if available >= Abs::zero() {
- // Determine size of auto columns.
- let (auto, count) = self.measure_auto_columns(vt, available)?;
-
- // If there is remaining space, distribute it to fractional columns,
- // otherwise shrink auto columns.
- let remaining = available - auto;
- if remaining >= Abs::zero() {
- self.grow_fractional_columns(remaining, fr);
- } else {
- self.shrink_auto_columns(available, count);
- }
- }
-
- // Sum up the resolved column sizes once here.
- self.width = self.rcols.iter().sum();
-
- Ok(())
- }
-
- /// Measure the size that is available to auto columns.
- fn measure_auto_columns(
- &mut self,
- vt: &mut Vt,
- available: Abs,
- ) -> SourceResult<(Abs, usize)> {
- let mut auto = Abs::zero();
- let mut count = 0;
-
- // Determine size of auto columns by laying out all cells in those
- // columns, measuring them and finding the largest one.
- for (x, &col) in self.cols.iter().enumerate() {
- if col != Sizing::Auto {
- continue;
- }
-
- let mut resolved = Abs::zero();
- for y in 0..self.rows.len() {
- if let Some(cell) = self.cell(x, y) {
- // For relative rows, we can already resolve the correct
- // base and for auto and fr we could only guess anyway.
- let height = match self.rows[y] {
- Sizing::Rel(v) => {
- v.resolve(self.styles).relative_to(self.regions.base().y)
- }
- _ => self.regions.base().y,
- };
-
- let size = Size::new(available, height);
- let pod = Regions::one(size, Axes::splat(false));
- let frame = cell.measure(vt, self.styles, pod)?.into_frame();
- resolved.set_max(frame.width());
- }
- }
-
- self.rcols[x] = resolved;
- auto += resolved;
- count += 1;
- }
-
- Ok((auto, count))
- }
-
- /// Distribute remaining space to fractional columns.
- fn grow_fractional_columns(&mut self, remaining: Abs, fr: Fr) {
- if fr.is_zero() {
- return;
- }
-
- for (&col, rcol) in self.cols.iter().zip(&mut self.rcols) {
- if let Sizing::Fr(v) = col {
- *rcol = v.share(fr, remaining);
- }
- }
- }
-
- /// Redistribute space to auto columns so that each gets a fair share.
- fn shrink_auto_columns(&mut self, available: Abs, count: usize) {
- let mut last;
- let mut fair = -Abs::inf();
- let mut redistribute = available;
- let mut overlarge = count;
- let mut changed = true;
-
- // Iteratively remove columns that don't need to be shrunk.
- while changed && overlarge > 0 {
- changed = false;
- last = fair;
- fair = redistribute / (overlarge as f64);
-
- for (&col, &rcol) in self.cols.iter().zip(&self.rcols) {
- // Remove an auto column if it is not overlarge (rcol <= fair),
- // but also hasn't already been removed (rcol > last).
- if col == Sizing::Auto && rcol <= fair && rcol > last {
- redistribute -= rcol;
- overlarge -= 1;
- changed = true;
- }
- }
- }
-
- // Redistribute space fairly among overlarge columns.
- for (&col, rcol) in self.cols.iter().zip(&mut self.rcols) {
- if col == Sizing::Auto && *rcol > fair {
- *rcol = fair;
- }
- }
- }
-
- /// Layout a row with automatic height. Such a row may break across multiple
- /// regions.
- fn layout_auto_row(&mut self, vt: &mut Vt, y: usize) -> SourceResult<()> {
- // Determine the size for each region of the row. If the first region
- // ends up empty for some column, skip the region and remeasure.
- let mut resolved = match self.measure_auto_row(vt, y, true)? {
- Some(resolved) => resolved,
- None => {
- self.finish_region(vt)?;
- self.measure_auto_row(vt, y, false)?.unwrap()
- }
- };
-
- // Nothing to layout.
- if resolved.is_empty() {
- return Ok(());
- }
-
- // Layout into a single region.
- if let &[first] = resolved.as_slice() {
- let frame = self.layout_single_row(vt, first, y)?;
- self.push_row(frame, y);
- return Ok(());
- }
-
- // Expand all but the last region.
- // Skip the first region if the space is eaten up by an fr row.
- let len = resolved.len();
- for (region, target) in self
- .regions
- .iter()
- .zip(&mut resolved[..len - 1])
- .skip(self.lrows.iter().any(|row| matches!(row, Row::Fr(..))) as usize)
- {
- target.set_max(region.y);
- }
-
- // Layout into multiple regions.
- let fragment = self.layout_multi_row(vt, &resolved, y)?;
- let len = fragment.len();
- for (i, frame) in fragment.into_iter().enumerate() {
- self.push_row(frame, y);
- if i + 1 < len {
- self.finish_region(vt)?;
- }
- }
-
- Ok(())
- }
-
- /// Measure the regions sizes of an auto row. The option is always `Some(_)`
- /// if `can_skip` is false.
- fn measure_auto_row(
- &mut self,
- vt: &mut Vt,
- y: usize,
- can_skip: bool,
- ) -> SourceResult<Option<Vec<Abs>>> {
- let mut resolved: Vec<Abs> = vec![];
-
- for (x, &rcol) in self.rcols.iter().enumerate() {
- if let Some(cell) = self.cell(x, y) {
- let mut pod = self.regions;
- pod.size.x = rcol;
-
- let frames = cell.measure(vt, self.styles, pod)?.into_frames();
-
- // Skip the first region if one cell in it is empty. Then,
- // remeasure.
- if let [first, rest @ ..] = frames.as_slice() {
- if can_skip
- && first.is_empty()
- && rest.iter().any(|frame| !frame.is_empty())
- {
- return Ok(None);
- }
- }
-
- let mut sizes = frames.iter().map(|frame| frame.height());
- for (target, size) in resolved.iter_mut().zip(&mut sizes) {
- target.set_max(size);
- }
-
- // New heights are maximal by virtue of being new. Note that
- // this extend only uses the rest of the sizes iterator.
- resolved.extend(sizes);
- }
- }
-
- Ok(Some(resolved))
- }
-
- /// Layout a row with relative height. Such a row cannot break across
- /// multiple regions, but it may force a region break.
- fn layout_relative_row(
- &mut self,
- vt: &mut Vt,
- v: Rel<Length>,
- y: usize,
- ) -> SourceResult<()> {
- let resolved = v.resolve(self.styles).relative_to(self.regions.base().y);
- let frame = self.layout_single_row(vt, resolved, y)?;
-
- // Skip to fitting region.
- let height = frame.height();
- while !self.regions.size.y.fits(height) && !self.regions.in_last() {
- self.finish_region(vt)?;
-
- // Don't skip multiple regions for gutter and don't push a row.
- if self.has_gutter && y % 2 == 1 {
- return Ok(());
- }
- }
-
- self.push_row(frame, y);
-
- Ok(())
- }
-
- /// Layout a row with fixed height and return its frame.
- fn layout_single_row(
- &mut self,
- vt: &mut Vt,
- height: Abs,
- y: usize,
- ) -> SourceResult<Frame> {
- if !height.is_finite() {
- bail!(error!(self.span, "cannot create grid with infinite height"));
- }
-
- let mut output = Frame::soft(Size::new(self.width, height));
- let mut pos = Point::zero();
-
- for (x, &rcol) in self.rcols.iter().enumerate() {
- if let Some(cell) = self.cell(x, y) {
- let size = Size::new(rcol, height);
- let mut pod = Regions::one(size, Axes::splat(true));
- if self.rows[y] == Sizing::Auto {
- pod.full = self.regions.full;
- }
- let frame = cell.layout(vt, self.styles, pod)?.into_frame();
- output.push_frame(pos, frame);
- }
-
- pos.x += rcol;
- }
-
- Ok(output)
- }
-
- /// Layout a row spanning multiple regions.
- fn layout_multi_row(
- &mut self,
- vt: &mut Vt,
- heights: &[Abs],
- y: usize,
- ) -> SourceResult<Fragment> {
- // Prepare frames.
- let mut outputs: Vec<_> = heights
- .iter()
- .map(|&h| Frame::soft(Size::new(self.width, h)))
- .collect();
-
- // Prepare regions.
- let size = Size::new(self.width, heights[0]);
- let mut pod = Regions::one(size, Axes::splat(true));
- pod.full = self.regions.full;
- pod.backlog = &heights[1..];
-
- // Layout the row.
- let mut pos = Point::zero();
- for (x, &rcol) in self.rcols.iter().enumerate() {
- if let Some(cell) = self.cell(x, y) {
- pod.size.x = rcol;
-
- // Push the layouted frames into the individual output frames.
- let fragment = cell.layout(vt, self.styles, pod)?;
- for (output, frame) in outputs.iter_mut().zip(fragment) {
- output.push_frame(pos, frame);
- }
- }
-
- pos.x += rcol;
- }
-
- Ok(Fragment::frames(outputs))
- }
-
- /// Push a row frame into the current region.
- fn push_row(&mut self, frame: Frame, y: usize) {
- self.regions.size.y -= frame.height();
- self.lrows.push(Row::Frame(frame, y));
- }
-
- /// Finish rows for one region.
- fn finish_region(&mut self, vt: &mut Vt) -> SourceResult<()> {
- // Determine the height of existing rows in the region.
- let mut used = Abs::zero();
- let mut fr = Fr::zero();
- for row in &self.lrows {
- match row {
- Row::Frame(frame, _) => used += frame.height(),
- Row::Fr(v, _) => fr += *v,
- }
- }
-
- // Determine the size of the grid in this region, expanding fully if
- // there are fr rows.
- let mut size = Size::new(self.width, used).min(self.initial);
- if fr.get() > 0.0 && self.initial.y.is_finite() {
- size.y = self.initial.y;
- }
-
- // The frame for the region.
- let mut output = Frame::soft(size);
- let mut pos = Point::zero();
- let mut rrows = vec![];
-
- // Place finished rows and layout fractional rows.
- for row in std::mem::take(&mut self.lrows) {
- let (frame, y) = match row {
- Row::Frame(frame, y) => (frame, y),
- Row::Fr(v, y) => {
- let remaining = self.regions.full - used;
- let height = v.share(fr, remaining);
- (self.layout_single_row(vt, height, y)?, y)
- }
- };
-
- let height = frame.height();
- output.push_frame(pos, frame);
- rrows.push(RowPiece { height, y });
- pos.y += height;
- }
-
- self.finished.push(output);
- self.rrows.push(rrows);
- self.regions.next();
- self.initial = self.regions.size;
-
- Ok(())
- }
-
- /// Get the content of the cell in column `x` and row `y`.
- ///
- /// Returns `None` if it's a gutter cell.
- #[track_caller]
- fn cell(&self, mut x: usize, y: usize) -> Option<&'a Content> {
- assert!(x < self.cols.len());
- assert!(y < self.rows.len());
-
- // Columns are reorder, but the cell slice is not.
- if self.is_rtl {
- x = self.cols.len() - 1 - x;
- }
-
- if self.has_gutter {
- // Even columns and rows are children, odd ones are gutter.
- if x % 2 == 0 && y % 2 == 0 {
- let c = 1 + self.cols.len() / 2;
- self.cells.get((y / 2) * c + x / 2)
- } else {
- None
- }
- } else {
- let c = self.cols.len();
- self.cells.get(y * c + x)
- }
- }
-}
diff --git a/crates/typst-library/src/layout/hide.rs b/crates/typst-library/src/layout/hide.rs
deleted file mode 100644
index af3d0631..00000000
--- a/crates/typst-library/src/layout/hide.rs
+++ /dev/null
@@ -1,29 +0,0 @@
-use smallvec::smallvec;
-
-use crate::prelude::*;
-
-/// Hides content without affecting layout.
-///
-/// The `hide` function allows you to hide content while the layout still 'sees'
-/// it. This is useful to create whitespace that is exactly as large as some
-/// content. It may also be useful to redact content because its arguments are
-/// not included in the output.
-///
-/// # Example
-/// ```example
-/// Hello Jane \
-/// #hide[Hello] Joe
-/// ```
-#[elem(Show)]
-pub struct HideElem {
- /// The content to hide.
- #[required]
- pub body: Content,
-}
-
-impl Show for HideElem {
- #[tracing::instrument(name = "HideElem::show", skip(self))]
- fn show(&self, _: &mut Vt, _: StyleChain) -> SourceResult<Content> {
- Ok(self.body().clone().styled(MetaElem::set_data(smallvec![Meta::Hide])))
- }
-}
diff --git a/crates/typst-library/src/layout/list.rs b/crates/typst-library/src/layout/list.rs
deleted file mode 100644
index 7c089760..00000000
--- a/crates/typst-library/src/layout/list.rs
+++ /dev/null
@@ -1,238 +0,0 @@
-use crate::layout::{BlockElem, GridLayouter, ParElem, Sizing, Spacing};
-use crate::prelude::*;
-use crate::text::TextElem;
-
-/// A bullet list.
-///
-/// Displays a sequence of items vertically, with each item introduced by a
-/// marker.
-///
-/// # Example
-/// ```example
-/// Normal list.
-/// - Text
-/// - Math
-/// - Layout
-/// - ...
-///
-/// Multiple lines.
-/// - This list item spans multiple
-/// lines because it is indented.
-///
-/// Function call.
-/// #list(
-/// [Foundations],
-/// [Calculate],
-/// [Construct],
-/// [Data Loading],
-/// )
-/// ```
-///
-/// # Syntax
-/// This functions also has dedicated syntax: Start a line with a hyphen,
-/// followed by a space to create a list item. A list item can contain multiple
-/// paragraphs and other block-level content. All content that is indented
-/// more than an item's marker becomes part of that item.
-#[elem(scope, title = "Bullet List", Layout)]
-pub struct ListElem {
- /// If this is `{false}`, the items are spaced apart with
- /// [list spacing]($list.spacing). If it is `{true}`, they use normal
- /// [leading]($par.leading) instead. This makes the list more compact, which
- /// can look better if the items are short.
- ///
- /// In markup mode, the value of this parameter is determined based on
- /// whether items are separated with a blank line. If items directly follow
- /// each other, this is set to `{true}`; if items are separated by a blank
- /// line, this is set to `{false}`.
- ///
- /// ```example
- /// - If a list has a lot of text, and
- /// maybe other inline content, it
- /// should not be tight anymore.
- ///
- /// - To make a list wide, simply insert
- /// a blank line between the items.
- /// ```
- #[default(true)]
- pub tight: bool,
-
- /// The marker which introduces each item.
- ///
- /// Instead of plain content, you can also pass an array with multiple
- /// markers that should be used for nested lists. If the list nesting depth
- /// exceeds the number of markers, the last one is repeated. For total
- /// control, you may pass a function that maps the list's nesting depth
- /// (starting from `{0}`) to a desired marker.
- ///
- /// ```example
- /// #set list(marker: [--])
- /// - A more classic list
- /// - With en-dashes
- ///
- /// #set list(marker: ([•], [--]))
- /// - Top-level
- /// - Nested
- /// - Items
- /// - Items
- /// ```
- #[borrowed]
- #[default(ListMarker::Content(vec![TextElem::packed('•')]))]
- pub marker: ListMarker,
-
- /// The indent of each item.
- #[resolve]
- pub indent: Length,
-
- /// The spacing between the marker and the body of each item.
- #[resolve]
- #[default(Em::new(0.5).into())]
- pub body_indent: Length,
-
- /// The spacing between the items of a wide (non-tight) list.
- ///
- /// If set to `{auto}`, uses the spacing [below blocks]($block.below).
- pub spacing: Smart<Spacing>,
-
- /// The bullet list's children.
- ///
- /// When using the list syntax, adjacent items are automatically collected
- /// into lists, even through constructs like for loops.
- ///
- /// ```example
- /// #for letter in "ABC" [
- /// - Letter #letter
- /// ]
- /// ```
- #[variadic]
- pub children: Vec<ListItem>,
-
- /// The nesting depth.
- #[internal]
- #[fold]
- depth: Depth,
-}
-
-#[scope]
-impl ListElem {
- #[elem]
- type ListItem;
-}
-
-impl Layout for ListElem {
- #[tracing::instrument(name = "ListElem::layout", skip_all)]
- fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- let indent = self.indent(styles);
- let body_indent = self.body_indent(styles);
- let gutter = if self.tight(styles) {
- ParElem::leading_in(styles).into()
- } else {
- self.spacing(styles)
- .unwrap_or_else(|| *BlockElem::below_in(styles).amount())
- };
-
- let depth = self.depth(styles);
- let marker = self
- .marker(styles)
- .resolve(vt, depth)?
- // avoid '#set align' interference with the list
- .aligned(HAlign::Start + VAlign::Top);
-
- let mut cells = vec![];
- for item in self.children() {
- cells.push(Content::empty());
- cells.push(marker.clone());
- cells.push(Content::empty());
- cells.push(item.body().clone().styled(Self::set_depth(Depth)));
- }
-
- let layouter = GridLayouter::new(
- Axes::with_x(&[
- Sizing::Rel(indent.into()),
- Sizing::Auto,
- Sizing::Rel(body_indent.into()),
- Sizing::Auto,
- ]),
- Axes::with_y(&[gutter.into()]),
- &cells,
- regions,
- styles,
- self.span(),
- );
-
- Ok(layouter.layout(vt)?.fragment)
- }
-}
-
-/// A bullet list item.
-#[elem(name = "item", title = "Bullet List Item")]
-pub struct ListItem {
- /// The item's body.
- #[required]
- pub body: Content,
-}
-
-cast! {
- ListItem,
- v: Content => v.to::<Self>().cloned().unwrap_or_else(|| Self::new(v.clone())),
-}
-
-/// A list's marker.
-#[derive(Debug, Clone, PartialEq, Hash)]
-pub enum ListMarker {
- Content(Vec<Content>),
- Func(Func),
-}
-
-impl ListMarker {
- /// Resolve the marker for the given depth.
- fn resolve(&self, vt: &mut Vt, depth: usize) -> SourceResult<Content> {
- Ok(match self {
- Self::Content(list) => {
- list.get(depth).or(list.last()).cloned().unwrap_or_default()
- }
- Self::Func(func) => func.call_vt(vt, [depth])?.display(),
- })
- }
-}
-
-cast! {
- ListMarker,
- self => match self {
- Self::Content(vec) => if vec.len() == 1 {
- vec.into_iter().next().unwrap().into_value()
- } else {
- vec.into_value()
- },
- Self::Func(func) => func.into_value(),
- },
- v: Content => Self::Content(vec![v]),
- array: Array => {
- if array.is_empty() {
- bail!("array must contain at least one marker");
- }
- Self::Content(array.into_iter().map(Value::display).collect())
- },
- v: Func => Self::Func(v),
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Hash)]
-struct Depth;
-
-cast! {
- Depth,
- self => Value::None,
- _: Value => Self,
-}
-
-impl Fold for Depth {
- type Output = usize;
-
- fn fold(self, outer: Self::Output) -> Self::Output {
- outer + 1
- }
-}
diff --git a/crates/typst-library/src/layout/measure.rs b/crates/typst-library/src/layout/measure.rs
deleted file mode 100644
index d41b7f95..00000000
--- a/crates/typst-library/src/layout/measure.rs
+++ /dev/null
@@ -1,53 +0,0 @@
-use crate::prelude::*;
-
-/// Measures the layouted size of content.
-///
-/// The `measure` function lets you determine the layouted size of content. Note
-/// that an infinite space is assumed, therefore the measured height/width may
-/// not necessarily match the final height/width of the measured content. If you
-/// want to measure in the current layout dimensions, you can combine `measure`
-/// and [`layout`]($layout).
-///
-/// # Example
-/// The same content can have a different size depending on the styles that
-/// are active when it is layouted. For example, in the example below
-/// `[#content]` is of course bigger when we increase the font size.
-///
-/// ```example
-/// #let content = [Hello!]
-/// #content
-/// #set text(14pt)
-/// #content
-/// ```
-///
-/// To do a meaningful measurement, you therefore first need to retrieve the
-/// active styles with the [`style`]($style) function. You can then pass them to
-/// the `measure` function.
-///
-/// ```example
-/// #let thing(body) = style(styles => {
-/// let size = measure(body, styles)
-/// [Width of "#body" is #size.width]
-/// })
-///
-/// #thing[Hey] \
-/// #thing[Welcome]
-/// ```
-///
-/// The measure function returns a dictionary with the entries `width` and
-/// `height`, both of type [`length`]($length).
-#[func]
-pub fn measure(
- /// The virtual machine.
- vm: &mut Vm,
- /// The content whose size to measure.
- content: Content,
- /// The styles with which to layout the content.
- styles: Styles,
-) -> SourceResult<Dict> {
- let pod = Regions::one(Axes::splat(Abs::inf()), Axes::splat(false));
- let styles = StyleChain::new(&styles);
- let frame = content.measure(&mut vm.vt, styles, pod)?.into_frame();
- let Size { x, y } = frame.size();
- Ok(dict! { "width" => x, "height" => y })
-}
diff --git a/crates/typst-library/src/layout/mod.rs b/crates/typst-library/src/layout/mod.rs
deleted file mode 100644
index bcbe3b2b..00000000
--- a/crates/typst-library/src/layout/mod.rs
+++ /dev/null
@@ -1,786 +0,0 @@
-//! Composable layouts.
-
-mod align;
-mod columns;
-mod container;
-#[path = "enum.rs"]
-mod enum_;
-mod flow;
-mod fragment;
-mod grid;
-mod hide;
-mod list;
-#[path = "measure.rs"]
-mod measure_;
-mod pad;
-mod page;
-mod par;
-mod place;
-mod regions;
-mod repeat;
-mod spacing;
-mod stack;
-mod table;
-mod terms;
-mod transform;
-
-pub use self::align::*;
-pub use self::columns::*;
-pub use self::container::*;
-pub use self::enum_::*;
-pub use self::flow::*;
-pub use self::fragment::*;
-pub use self::grid::*;
-pub use self::hide::*;
-pub use self::list::*;
-pub use self::measure_::*;
-pub use self::pad::*;
-pub use self::page::*;
-pub use self::par::*;
-pub use self::place::*;
-pub use self::regions::*;
-pub use self::repeat::*;
-pub use self::spacing::*;
-pub use self::stack::*;
-pub use self::table::*;
-pub use self::terms::*;
-pub use self::transform::*;
-
-use std::borrow::Cow;
-use std::mem;
-
-use typed_arena::Arena;
-use typst::diag::SourceResult;
-use typst::eval::Tracer;
-use typst::model::{applicable, realize, DelayedErrors, StyleVecBuilder};
-
-use crate::math::{EquationElem, LayoutMath};
-use crate::meta::{CiteElem, CiteGroup, DocumentElem};
-use crate::prelude::*;
-use crate::shared::BehavedBuilder;
-use crate::text::{LinebreakElem, SmartquoteElem, SpaceElem, TextElem};
-use crate::visualize::{
- CircleElem, EllipseElem, ImageElem, LineElem, PathElem, PolygonElem, RectElem,
- SquareElem,
-};
-
-/// Hook up all layout definitions.
-pub(super) fn define(global: &mut Scope) {
- global.category("layout");
- global.define_type::<Length>();
- global.define_type::<Angle>();
- global.define_type::<Ratio>();
- global.define_type::<Rel<Length>>();
- global.define_type::<Fr>();
- global.define_type::<Dir>();
- global.define_type::<Align>();
- global.define_elem::<PageElem>();
- global.define_elem::<PagebreakElem>();
- global.define_elem::<VElem>();
- global.define_elem::<ParElem>();
- global.define_elem::<ParbreakElem>();
- global.define_elem::<HElem>();
- global.define_elem::<BoxElem>();
- global.define_elem::<BlockElem>();
- global.define_elem::<ListElem>();
- global.define_elem::<EnumElem>();
- global.define_elem::<TermsElem>();
- global.define_elem::<TableElem>();
- global.define_elem::<StackElem>();
- global.define_elem::<GridElem>();
- global.define_elem::<ColumnsElem>();
- global.define_elem::<ColbreakElem>();
- global.define_elem::<PlaceElem>();
- global.define_elem::<AlignElem>();
- global.define_elem::<PadElem>();
- global.define_elem::<RepeatElem>();
- global.define_elem::<MoveElem>();
- global.define_elem::<ScaleElem>();
- global.define_elem::<RotateElem>();
- global.define_elem::<HideElem>();
- global.define_func::<measure>();
-}
-
-/// Root-level layout.
-pub trait LayoutRoot {
- /// Layout into one frame per page.
- fn layout_root(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Document>;
-}
-
-impl LayoutRoot for Content {
- #[tracing::instrument(name = "Content::layout_root", skip_all)]
- fn layout_root(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Document> {
- #[comemo::memoize]
- fn cached(
- content: &Content,
- world: Tracked<dyn World + '_>,
- introspector: Tracked<Introspector>,
- locator: Tracked<Locator>,
- delayed: TrackedMut<DelayedErrors>,
- tracer: TrackedMut<Tracer>,
- styles: StyleChain,
- ) -> SourceResult<Document> {
- let mut locator = Locator::chained(locator);
- let mut vt = Vt {
- world,
- introspector,
- locator: &mut locator,
- delayed,
- tracer,
- };
- let scratch = Scratch::default();
- let (realized, styles) = realize_root(&mut vt, &scratch, content, styles)?;
- realized
- .with::<dyn LayoutRoot>()
- .unwrap()
- .layout_root(&mut vt, styles)
- }
-
- tracing::info!("Starting layout");
- cached(
- self,
- vt.world,
- vt.introspector,
- vt.locator.track(),
- TrackedMut::reborrow_mut(&mut vt.delayed),
- TrackedMut::reborrow_mut(&mut vt.tracer),
- styles,
- )
- }
-}
-
-/// Layout into regions.
-pub trait Layout {
- /// Layout into one frame per region.
- fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment>;
-
- /// Layout without side effects.
- ///
- /// This element must be layouted again in the same order for the results to
- /// be valid.
- #[tracing::instrument(name = "Layout::measure", skip_all)]
- fn measure(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- let mut locator = Locator::chained(vt.locator.track());
- let mut vt = Vt {
- world: vt.world,
- introspector: vt.introspector,
- locator: &mut locator,
- tracer: TrackedMut::reborrow_mut(&mut vt.tracer),
- delayed: TrackedMut::reborrow_mut(&mut vt.delayed),
- };
- self.layout(&mut vt, styles, regions)
- }
-}
-
-impl Layout for Content {
- #[tracing::instrument(name = "Content::layout", skip_all)]
- fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- #[allow(clippy::too_many_arguments)]
- #[comemo::memoize]
- fn cached(
- content: &Content,
- world: Tracked<dyn World + '_>,
- introspector: Tracked<Introspector>,
- locator: Tracked<Locator>,
- delayed: TrackedMut<DelayedErrors>,
- tracer: TrackedMut<Tracer>,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- let mut locator = Locator::chained(locator);
- let mut vt = Vt {
- world,
- introspector,
- locator: &mut locator,
- delayed,
- tracer,
- };
- let scratch = Scratch::default();
- let (realized, styles) = realize_block(&mut vt, &scratch, content, styles)?;
- realized
- .with::<dyn Layout>()
- .unwrap()
- .layout(&mut vt, styles, regions)
- }
-
- tracing::info!("Layouting `Content`");
-
- let fragment = cached(
- self,
- vt.world,
- vt.introspector,
- vt.locator.track(),
- TrackedMut::reborrow_mut(&mut vt.delayed),
- TrackedMut::reborrow_mut(&mut vt.tracer),
- styles,
- regions,
- )?;
-
- vt.locator.visit_frames(&fragment);
- Ok(fragment)
- }
-}
-
-/// Realize into an element that is capable of root-level layout.
-#[tracing::instrument(skip_all)]
-fn realize_root<'a>(
- vt: &mut Vt,
- scratch: &'a Scratch<'a>,
- content: &'a Content,
- styles: StyleChain<'a>,
-) -> SourceResult<(Cow<'a, Content>, StyleChain<'a>)> {
- if content.can::<dyn LayoutRoot>() && !applicable(content, styles) {
- return Ok((Cow::Borrowed(content), styles));
- }
-
- let mut builder = Builder::new(vt, scratch, true);
- builder.accept(content, styles)?;
- builder.interrupt_page(Some(styles), true)?;
- let (pages, shared) = builder.doc.unwrap().pages.finish();
- Ok((Cow::Owned(DocumentElem::new(pages.to_vec()).pack()), shared))
-}
-
-/// Realize into an element that is capable of block-level layout.
-#[tracing::instrument(skip_all)]
-fn realize_block<'a>(
- vt: &mut Vt,
- scratch: &'a Scratch<'a>,
- content: &'a Content,
- styles: StyleChain<'a>,
-) -> SourceResult<(Cow<'a, Content>, StyleChain<'a>)> {
- // These elements implement `Layout` but still require a flow for
- // proper layout.
- if content.can::<dyn Layout>()
- && !content.is::<BoxElem>()
- && !content.is::<LineElem>()
- && !content.is::<RectElem>()
- && !content.is::<SquareElem>()
- && !content.is::<EllipseElem>()
- && !content.is::<CircleElem>()
- && !content.is::<ImageElem>()
- && !content.is::<PolygonElem>()
- && !content.is::<PathElem>()
- && !content.is::<PlaceElem>()
- && !applicable(content, styles)
- {
- return Ok((Cow::Borrowed(content), styles));
- }
-
- let mut builder = Builder::new(vt, scratch, false);
- builder.accept(content, styles)?;
- builder.interrupt_par()?;
- let (children, shared) = builder.flow.0.finish();
- Ok((Cow::Owned(FlowElem::new(children.to_vec()).pack()), shared))
-}
-
-/// Builds a document or a flow element from content.
-struct Builder<'a, 'v, 't> {
- /// The virtual typesetter.
- vt: &'v mut Vt<'t>,
- /// Scratch arenas for building.
- scratch: &'a Scratch<'a>,
- /// The current document building state.
- doc: Option<DocBuilder<'a>>,
- /// The current flow building state.
- flow: FlowBuilder<'a>,
- /// The current paragraph building state.
- par: ParBuilder<'a>,
- /// The current list building state.
- list: ListBuilder<'a>,
- /// The current citation grouping state.
- cites: CiteGroupBuilder<'a>,
-}
-
-/// Temporary storage arenas for building.
-#[derive(Default)]
-struct Scratch<'a> {
- /// An arena where intermediate style chains are stored.
- styles: Arena<StyleChain<'a>>,
- /// An arena where intermediate content resulting from show rules is stored.
- content: Arena<Content>,
-}
-
-impl<'a, 'v, 't> Builder<'a, 'v, 't> {
- fn new(vt: &'v mut Vt<'t>, scratch: &'a Scratch<'a>, top: bool) -> Self {
- Self {
- vt,
- scratch,
- doc: top.then(DocBuilder::default),
- flow: FlowBuilder::default(),
- par: ParBuilder::default(),
- list: ListBuilder::default(),
- cites: CiteGroupBuilder::default(),
- }
- }
-
- fn accept(
- &mut self,
- mut content: &'a Content,
- styles: StyleChain<'a>,
- ) -> SourceResult<()> {
- if content.can::<dyn LayoutMath>() && !content.is::<EquationElem>() {
- content =
- self.scratch.content.alloc(EquationElem::new(content.clone()).pack());
- }
-
- if let Some(realized) = realize(self.vt, content, styles)? {
- let stored = self.scratch.content.alloc(realized);
- return self.accept(stored, styles);
- }
-
- if let Some((elem, local)) = content.to_styled() {
- return self.styled(elem, local, styles);
- }
-
- if let Some(children) = content.to_sequence() {
- for elem in children {
- self.accept(elem, styles)?;
- }
- return Ok(());
- }
-
- if self.cites.accept(content, styles) {
- return Ok(());
- }
-
- self.interrupt_cites()?;
-
- if self.list.accept(content, styles) {
- return Ok(());
- }
-
- self.interrupt_list()?;
-
- if self.list.accept(content, styles) {
- return Ok(());
- }
-
- if self.par.accept(content, styles) {
- return Ok(());
- }
-
- self.interrupt_par()?;
-
- if self.flow.accept(content, styles) {
- return Ok(());
- }
-
- let keep = content
- .to::<PagebreakElem>()
- .map_or(false, |pagebreak| !pagebreak.weak(styles));
-
- self.interrupt_page(keep.then_some(styles), false)?;
-
- if let Some(doc) = &mut self.doc {
- if doc.accept(content, styles) {
- return Ok(());
- }
- }
-
- if content.is::<PagebreakElem>() {
- bail!(content.span(), "pagebreaks are not allowed inside of containers");
- } else {
- bail!(content.span(), "{} is not allowed here", content.func().name());
- }
- }
-
- fn styled(
- &mut self,
- elem: &'a Content,
- map: &'a Styles,
- styles: StyleChain<'a>,
- ) -> SourceResult<()> {
- let stored = self.scratch.styles.alloc(styles);
- let styles = stored.chain(map);
- self.interrupt_style(map, None)?;
- self.accept(elem, styles)?;
- self.interrupt_style(map, Some(styles))?;
- Ok(())
- }
-
- fn interrupt_style(
- &mut self,
- local: &Styles,
- outer: Option<StyleChain<'a>>,
- ) -> SourceResult<()> {
- if let Some(Some(span)) = local.interruption::<DocumentElem>() {
- if self.doc.is_none() {
- bail!(span, "document set rules are not allowed inside of containers");
- }
- if outer.is_none()
- && (!self.flow.0.is_empty()
- || !self.par.0.is_empty()
- || !self.list.items.is_empty())
- {
- bail!(span, "document set rules must appear before any content");
- }
- } else if let Some(Some(span)) = local.interruption::<PageElem>() {
- if self.doc.is_none() {
- bail!(span, "page configuration is not allowed inside of containers");
- }
- self.interrupt_page(outer, false)?;
- } else if local.interruption::<ParElem>().is_some()
- || local.interruption::<AlignElem>().is_some()
- {
- self.interrupt_par()?;
- } else if local.interruption::<ListElem>().is_some()
- || local.interruption::<EnumElem>().is_some()
- || local.interruption::<TermsElem>().is_some()
- {
- self.interrupt_list()?;
- }
- Ok(())
- }
-
- fn interrupt_cites(&mut self) -> SourceResult<()> {
- if !self.cites.items.is_empty() {
- let staged = mem::take(&mut self.cites.staged);
- let (group, styles) = mem::take(&mut self.cites).finish();
- let stored = self.scratch.content.alloc(group);
- self.accept(stored, styles)?;
- for (content, styles) in staged {
- self.accept(content, styles)?;
- }
- }
- Ok(())
- }
-
- fn interrupt_list(&mut self) -> SourceResult<()> {
- self.interrupt_cites()?;
- if !self.list.items.is_empty() {
- let staged = mem::take(&mut self.list.staged);
- let (list, styles) = mem::take(&mut self.list).finish();
- let stored = self.scratch.content.alloc(list);
- self.accept(stored, styles)?;
- for (content, styles) in staged {
- self.accept(content, styles)?;
- }
- }
- Ok(())
- }
-
- fn interrupt_par(&mut self) -> SourceResult<()> {
- self.interrupt_list()?;
- if !self.par.0.is_empty() {
- let (par, styles) = mem::take(&mut self.par).finish();
- let stored = self.scratch.content.alloc(par);
- self.accept(stored, styles)?;
- }
-
- Ok(())
- }
-
- fn interrupt_page(
- &mut self,
- styles: Option<StyleChain<'a>>,
- last: bool,
- ) -> SourceResult<()> {
- self.interrupt_par()?;
- let Some(doc) = &mut self.doc else { return Ok(()) };
- if (doc.keep_next && styles.is_some()) || self.flow.0.has_strong_elements(last) {
- let (flow, shared) = mem::take(&mut self.flow).0.finish();
- let styles = if shared == StyleChain::default() {
- styles.unwrap_or_default()
- } else {
- shared
- };
- let page = PageElem::new(FlowElem::new(flow.to_vec()).pack());
- let stored = self.scratch.content.alloc(page.pack());
- self.accept(stored, styles)?;
- }
- Ok(())
- }
-}
-
-/// Accepts pagebreaks and pages.
-struct DocBuilder<'a> {
- /// The page runs built so far.
- pages: StyleVecBuilder<'a, Cow<'a, Content>>,
- /// Whether to keep a following page even if it is empty.
- keep_next: bool,
- /// Whether the next page should be cleared to an even or odd number.
- clear_next: Option<Parity>,
-}
-
-impl<'a> DocBuilder<'a> {
- fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool {
- if let Some(pagebreak) = content.to::<PagebreakElem>() {
- self.keep_next = !pagebreak.weak(styles);
- self.clear_next = pagebreak.to(styles);
- return true;
- }
-
- if let Some(page) = content.to::<PageElem>() {
- let elem = if let Some(clear_to) = self.clear_next.take() {
- let mut page = page.clone();
- page.push_clear_to(Some(clear_to));
- Cow::Owned(page.pack())
- } else {
- Cow::Borrowed(content)
- };
-
- self.pages.push(elem, styles);
- self.keep_next = false;
- return true;
- }
-
- false
- }
-}
-
-impl Default for DocBuilder<'_> {
- fn default() -> Self {
- Self {
- pages: StyleVecBuilder::new(),
- keep_next: true,
- clear_next: None,
- }
- }
-}
-
-/// Accepts flow content.
-#[derive(Default)]
-struct FlowBuilder<'a>(BehavedBuilder<'a>, bool);
-
-impl<'a> FlowBuilder<'a> {
- fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool {
- if content.is::<ParbreakElem>() {
- self.1 = true;
- return true;
- }
-
- let last_was_parbreak = self.1;
- self.1 = false;
-
- if content.is::<VElem>()
- || content.is::<ColbreakElem>()
- || content.is::<MetaElem>()
- || content.is::<PlaceElem>()
- {
- self.0.push(Cow::Borrowed(content), styles);
- return true;
- }
-
- if content.can::<dyn Layout>() || content.is::<ParElem>() {
- let is_tight_list = if let Some(elem) = content.to::<ListElem>() {
- elem.tight(styles)
- } else if let Some(elem) = content.to::<EnumElem>() {
- elem.tight(styles)
- } else if let Some(elem) = content.to::<TermsElem>() {
- elem.tight(styles)
- } else {
- false
- };
-
- if !last_was_parbreak && is_tight_list {
- let leading = ParElem::leading_in(styles);
- let spacing = VElem::list_attach(leading.into());
- self.0.push(Cow::Owned(spacing.pack()), styles);
- }
-
- let (above, below) = if let Some(block) = content.to::<BlockElem>() {
- (block.above(styles), block.below(styles))
- } else {
- (BlockElem::above_in(styles), BlockElem::below_in(styles))
- };
-
- self.0.push(Cow::Owned(above.pack()), styles);
- self.0.push(Cow::Borrowed(content), styles);
- self.0.push(Cow::Owned(below.pack()), styles);
- return true;
- }
-
- false
- }
-}
-
-/// Accepts paragraph content.
-#[derive(Default)]
-struct ParBuilder<'a>(BehavedBuilder<'a>);
-
-impl<'a> ParBuilder<'a> {
- fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool {
- if content.is::<MetaElem>() {
- if self.0.has_strong_elements(false) {
- self.0.push(Cow::Borrowed(content), styles);
- return true;
- }
- } else if content.is::<SpaceElem>()
- || content.is::<TextElem>()
- || content.is::<HElem>()
- || content.is::<LinebreakElem>()
- || content.is::<SmartquoteElem>()
- || content.to::<EquationElem>().map_or(false, |elem| !elem.block(styles))
- || content.is::<BoxElem>()
- {
- self.0.push(Cow::Borrowed(content), styles);
- return true;
- }
-
- false
- }
-
- fn finish(self) -> (Content, StyleChain<'a>) {
- let (children, shared) = self.0.finish();
-
- // Find the first span that isn't detached.
- let span = children
- .iter()
- .map(|(cnt, _)| cnt.span())
- .find(|span| !span.is_detached())
- .unwrap_or_else(Span::detached);
-
- (ParElem::new(children.to_vec()).spanned(span).pack(), shared)
- }
-}
-
-/// Accepts list / enum items, spaces, paragraph breaks.
-struct ListBuilder<'a> {
- /// The list items collected so far.
- items: StyleVecBuilder<'a, Cow<'a, Content>>,
- /// Whether the list contains no paragraph breaks.
- tight: bool,
- /// Trailing content for which it is unclear whether it is part of the list.
- staged: Vec<(&'a Content, StyleChain<'a>)>,
-}
-
-impl<'a> ListBuilder<'a> {
- fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool {
- if !self.items.is_empty()
- && (content.is::<SpaceElem>() || content.is::<ParbreakElem>())
- {
- self.staged.push((content, styles));
- return true;
- }
-
- if (content.is::<ListItem>()
- || content.is::<EnumItem>()
- || content.is::<TermItem>())
- && self
- .items
- .elems()
- .next()
- .map_or(true, |first| first.func() == content.func())
- {
- self.items.push(Cow::Borrowed(content), styles);
- self.tight &= self.staged.drain(..).all(|(t, _)| !t.is::<ParbreakElem>());
- return true;
- }
-
- false
- }
-
- fn finish(self) -> (Content, StyleChain<'a>) {
- let (items, shared) = self.items.finish();
- let item = items.items().next().unwrap();
- let output = if item.is::<ListItem>() {
- ListElem::new(
- items
- .iter()
- .map(|(item, local)| {
- let item = item.to::<ListItem>().unwrap();
- item.clone()
- .with_body(item.body().clone().styled_with_map(local.clone()))
- })
- .collect::<Vec<_>>(),
- )
- .with_tight(self.tight)
- .pack()
- } else if item.is::<EnumItem>() {
- EnumElem::new(
- items
- .iter()
- .map(|(item, local)| {
- let item = item.to::<EnumItem>().unwrap();
- item.clone()
- .with_body(item.body().clone().styled_with_map(local.clone()))
- })
- .collect::<Vec<_>>(),
- )
- .with_tight(self.tight)
- .pack()
- } else if item.is::<TermItem>() {
- TermsElem::new(
- items
- .iter()
- .map(|(item, local)| {
- let item = item.to::<TermItem>().unwrap();
- item.clone()
- .with_term(item.term().clone().styled_with_map(local.clone()))
- .with_description(
- item.description().clone().styled_with_map(local.clone()),
- )
- })
- .collect::<Vec<_>>(),
- )
- .with_tight(self.tight)
- .pack()
- } else {
- unreachable!()
- };
- (output, shared)
- }
-}
-
-impl Default for ListBuilder<'_> {
- fn default() -> Self {
- Self {
- items: StyleVecBuilder::default(),
- tight: true,
- staged: vec![],
- }
- }
-}
-
-/// Accepts citations.
-#[derive(Default)]
-struct CiteGroupBuilder<'a> {
- /// The styles.
- styles: StyleChain<'a>,
- /// The citations.
- items: Vec<CiteElem>,
- /// Trailing content for which it is unclear whether it is part of the list.
- staged: Vec<(&'a Content, StyleChain<'a>)>,
-}
-
-impl<'a> CiteGroupBuilder<'a> {
- fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool {
- if !self.items.is_empty()
- && (content.is::<SpaceElem>() || content.is::<MetaElem>())
- {
- self.staged.push((content, styles));
- return true;
- }
-
- if let Some(citation) = content.to::<CiteElem>() {
- if self.items.is_empty() {
- self.styles = styles;
- }
- self.staged.retain(|(elem, _)| !elem.is::<SpaceElem>());
- self.items.push(citation.clone());
- return true;
- }
-
- false
- }
-
- fn finish(self) -> (Content, StyleChain<'a>) {
- let span = self.items.first().map(|cite| cite.span()).unwrap_or(Span::detached());
- (CiteGroup::new(self.items).pack().spanned(span), self.styles)
- }
-}
diff --git a/crates/typst-library/src/layout/pad.rs b/crates/typst-library/src/layout/pad.rs
deleted file mode 100644
index d1b0cb1f..00000000
--- a/crates/typst-library/src/layout/pad.rs
+++ /dev/null
@@ -1,121 +0,0 @@
-use crate::prelude::*;
-
-/// Adds spacing around content.
-///
-/// The spacing can be specified for each side individually, or for all sides at
-/// once by specifying a positional argument.
-///
-/// # Example
-/// ```example
-/// #set align(center)
-///
-/// #pad(x: 16pt, image("typing.jpg"))
-/// _Typing speeds can be
-/// measured in words per minute._
-/// ```
-#[elem(title = "Padding", Layout)]
-pub struct PadElem {
- /// The padding at the left side.
- #[parse(
- let all = args.named("rest")?.or(args.find()?);
- let x = args.named("x")?.or(all);
- let y = args.named("y")?.or(all);
- args.named("left")?.or(x)
- )]
- pub left: Rel<Length>,
-
- /// The padding at the top side.
- #[parse(args.named("top")?.or(y))]
- pub top: Rel<Length>,
-
- /// The padding at the right side.
- #[parse(args.named("right")?.or(x))]
- pub right: Rel<Length>,
-
- /// The padding at the bottom side.
- #[parse(args.named("bottom")?.or(y))]
- pub bottom: Rel<Length>,
-
- /// The horizontal padding. Both `left` and `right` take precedence over
- /// this.
- #[external]
- pub x: Rel<Length>,
-
- /// The vertical padding. Both `top` and `bottom` take precedence over this.
- #[external]
- pub y: Rel<Length>,
-
- /// The padding for all sides. All other parameters take precedence over
- /// this.
- #[external]
- pub rest: Rel<Length>,
-
- /// The content to pad at the sides.
- #[required]
- pub body: Content,
-}
-
-impl Layout for PadElem {
- #[tracing::instrument(name = "PadElem::layout", skip_all)]
- fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- let sides = Sides::new(
- self.left(styles),
- self.top(styles),
- self.right(styles),
- self.bottom(styles),
- );
-
- // Layout child into padded regions.
- let mut backlog = vec![];
- let padding = sides.resolve(styles);
- let pod = regions.map(&mut backlog, |size| shrink(size, padding));
- let mut fragment = self.body().layout(vt, styles, pod)?;
-
- for frame in &mut fragment {
- // Apply the padding inversely such that the grown size padded
- // yields the frame's size.
- let padded = grow(frame.size(), padding);
- let padding = padding.relative_to(padded);
- let offset = Point::new(padding.left, padding.top);
-
- // Grow the frame and translate everything in the frame inwards.
- frame.set_size(padded);
- frame.translate(offset);
- }
-
- Ok(fragment)
- }
-}
-
-/// Shrink a size by padding relative to the size itself.
-fn shrink(size: Size, padding: Sides<Rel<Abs>>) -> Size {
- size - padding.relative_to(size).sum_by_axis()
-}
-
-/// Grow a size by padding relative to the grown size.
-/// This is the inverse operation to `shrink()`.
-///
-/// For the horizontal axis the derivation looks as follows.
-/// (Vertical axis is analogous.)
-///
-/// Let w be the grown target width,
-/// s be the given width,
-/// l be the left padding,
-/// r be the right padding,
-/// p = l + r.
-///
-/// We want that: w - l.resolve(w) - r.resolve(w) = s
-///
-/// Thus: w - l.resolve(w) - r.resolve(w) = s
-/// <=> w - p.resolve(w) = s
-/// <=> w - p.rel * w - p.abs = s
-/// <=> (1 - p.rel) * w = s + p.abs
-/// <=> w = (s + p.abs) / (1 - p.rel)
-fn grow(size: Size, padding: Sides<Rel<Abs>>) -> Size {
- size.zip_map(padding.sum_by_axis(), |s, p| (s + p.abs).safe_div(1.0 - p.rel.get()))
-}
diff --git a/crates/typst-library/src/layout/page.rs b/crates/typst-library/src/layout/page.rs
deleted file mode 100644
index 53646c7d..00000000
--- a/crates/typst-library/src/layout/page.rs
+++ /dev/null
@@ -1,949 +0,0 @@
-use std::borrow::Cow;
-use std::ptr;
-use std::str::FromStr;
-
-use typst::eval::AutoValue;
-
-use crate::layout::{AlignElem, ColumnsElem};
-use crate::meta::{Counter, CounterKey, ManualPageCounter, Numbering};
-use crate::prelude::*;
-use crate::text::TextElem;
-
-/// Layouts its child onto one or multiple pages.
-///
-/// Although this function is primarily used in set rules to affect page
-/// properties, it can also be used to explicitly render its argument onto
-/// a set of pages of its own.
-///
-/// Pages can be set to use `{auto}` as their width or height. In this case, the
-/// pages will grow to fit their content on the respective axis.
-///
-/// The [Guide for Page Setup]($guides/page-setup-guide) explains how to use
-/// this and related functions to set up a document with many examples.
-///
-/// # Example
-/// ```example
-/// >>> #set page(margin: auto)
-/// #set page("us-letter")
-///
-/// There you go, US friends!
-/// ```
-#[elem]
-pub struct PageElem {
- /// A standard paper size to set width and height.
- #[external]
- #[default(Paper::A4)]
- pub paper: Paper,
-
- /// The width of the page.
- ///
- /// ```example
- /// #set page(
- /// width: 3cm,
- /// margin: (x: 0cm),
- /// )
- ///
- /// #for i in range(3) {
- /// box(square(width: 1cm))
- /// }
- /// ```
- #[resolve]
- #[parse(
- let paper = args.named_or_find::<Paper>("paper")?;
- args.named("width")?
- .or_else(|| paper.map(|paper| Smart::Custom(paper.width().into())))
- )]
- #[default(Smart::Custom(Paper::A4.width().into()))]
- pub width: Smart<Length>,
-
- /// The height of the page.
- ///
- /// If this is set to `{auto}`, page breaks can only be triggered manually
- /// by inserting a [page break]($pagebreak). Most examples throughout this
- /// documentation use `{auto}` for the height of the page to dynamically
- /// grow and shrink to fit their content.
- #[resolve]
- #[parse(
- args.named("height")?
- .or_else(|| paper.map(|paper| Smart::Custom(paper.height().into())))
- )]
- #[default(Smart::Custom(Paper::A4.height().into()))]
- pub height: Smart<Length>,
-
- /// Whether the page is flipped into landscape orientation.
- ///
- /// ```example
- /// #set page(
- /// "us-business-card",
- /// flipped: true,
- /// fill: rgb("f2e5dd"),
- /// )
- ///
- /// #set align(bottom + end)
- /// #text(14pt)[*Sam H. Richards*] \
- /// _Procurement Manager_
- ///
- /// #set text(10pt)
- /// 17 Main Street \
- /// New York, NY 10001 \
- /// +1 555 555 5555
- /// ```
- #[default(false)]
- pub flipped: bool,
-
- /// The page's margins.
- ///
- /// - `{auto}`: The margins are set automatically to 2.5/21 times the smaller
- /// dimension of the page. This results in 2.5cm margins for an A4 page.
- /// - A single length: The same margin on all sides.
- /// - A dictionary: With a dictionary, the margins can be set individually.
- /// The dictionary can contain the following keys in order of precedence:
- /// - `top`: The top margin.
- /// - `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]($page.binding) is).
- /// - `outside`: The margin at the outer side of the page (opposite to the
- /// [binding]($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,
- /// height: 4cm,
- /// margin: (x: 8pt, y: 4pt),
- /// )
- ///
- /// #rect(
- /// width: 100%,
- /// height: 100%,
- /// fill: aqua,
- /// )
- /// ```
- #[fold]
- pub margin: Margin,
-
- /// On which side the pages will be bound.
- ///
- /// - `{auto}`: Equivalent to `left` if the [text direction]($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.
- ///
- /// If you need to insert columns into a page or other container, you can
- /// also use the [`columns` function]($columns).
- ///
- /// ```example:single
- /// #set page(columns: 2, height: 4.8cm)
- /// Climate change is one of the most
- /// pressing issues of our time, with
- /// the potential to devastate
- /// communities, ecosystems, and
- /// economies around the world. It's
- /// clear that we need to take urgent
- /// action to reduce our carbon
- /// emissions and mitigate the impacts
- /// of a rapidly changing climate.
- /// ```
- #[default(NonZeroUsize::ONE)]
- pub columns: NonZeroUsize,
-
- /// The page's background color.
- ///
- /// This instructs the printer to color the complete page with the given
- /// color. If you are considering larger production runs, it may be more
- /// environmentally friendly and cost-effective to source pre-dyed pages and
- /// not set this property.
- ///
- /// ```example
- /// #set page(fill: rgb("444352"))
- /// #set text(fill: rgb("fdfdfd"))
- /// *Dark mode enabled.*
- /// ```
- #[borrowed]
- pub fill: Option<Paint>,
-
- /// How to [number]($numbering) the pages.
- ///
- /// If an explicit `footer` (or `header` for top-aligned numbering) is
- /// given, the numbering is ignored.
- ///
- /// ```example
- /// #set page(
- /// height: 100pt,
- /// margin: (top: 16pt, bottom: 24pt),
- /// numbering: "1 / 1",
- /// )
- ///
- /// #lorem(48)
- /// ```
- #[borrowed]
- pub numbering: Option<Numbering>,
-
- /// The alignment of the page numbering.
- ///
- /// If the vertical component is `top`, the numbering is placed into the
- /// header and if it is `bottom`, it is placed in the footer. Horizon
- /// alignment is forbidden. If an explicit matching `header` or `footer` is
- /// given, the numbering is ignored.
- ///
- /// ```example
- /// #set page(
- /// margin: (top: 16pt, bottom: 24pt),
- /// numbering: "1",
- /// number-align: right,
- /// )
- ///
- /// #lorem(30)
- /// ```
- #[default(HAlign::Center + VAlign::Bottom)]
- #[parse({
- let option: Option<Spanned<Align>> = args.named("number-align")?;
- if let Some(Spanned { v: align, span }) = option {
- if align.y() == Some(VAlign::Horizon) {
- bail!(span, "page number cannot be `horizon`-aligned");
- }
- }
- option.map(|spanned| spanned.v)
- })]
- pub number_align: Align,
-
- /// The page's header. Fills the top margin of each page.
- ///
- /// ```example
- /// #set par(justify: true)
- /// #set page(
- /// margin: (top: 32pt, bottom: 20pt),
- /// header: [
- /// #set text(8pt)
- /// #smallcaps[Typst Academcy]
- /// #h(1fr) _Exercise Sheet 3_
- /// ],
- /// )
- ///
- /// #lorem(19)
- /// ```
- #[borrowed]
- pub header: Option<Content>,
-
- /// The amount the header is raised into the top margin.
- #[resolve]
- #[default(Ratio::new(0.3).into())]
- pub header_ascent: Rel<Length>,
-
- /// The page's footer. Fills the bottom margin of each page.
- ///
- /// For just a page number, the `numbering` property, typically suffices. If
- /// you want to create a custom footer, but still display the page number,
- /// you can directly access the [page counter]($counter).
- ///
- /// ```example
- /// #set par(justify: true)
- /// #set page(
- /// height: 100pt,
- /// margin: 20pt,
- /// footer: [
- /// #set align(right)
- /// #set text(8pt)
- /// #counter(page).display(
- /// "1 of I",
- /// both: true,
- /// )
- /// ]
- /// )
- ///
- /// #lorem(48)
- /// ```
- #[borrowed]
- pub footer: Option<Content>,
-
- /// The amount the footer is lowered into the bottom margin.
- #[resolve]
- #[default(Ratio::new(0.3).into())]
- pub footer_descent: Rel<Length>,
-
- /// Content in the page's background.
- ///
- /// This content will be placed behind the page's body. It can be
- /// used to place a background image or a watermark.
- ///
- /// ```example
- /// #set page(background: rotate(24deg,
- /// text(18pt, fill: rgb("FFCBC4"))[
- /// *CONFIDENTIAL*
- /// ]
- /// ))
- ///
- /// = Typst's secret plans
- /// In the year 2023, we plan to take
- /// over the world (of typesetting).
- /// ```
- #[borrowed]
- pub background: Option<Content>,
-
- /// Content in the page's foreground.
- ///
- /// This content will overlay the page's body.
- ///
- /// ```example
- /// #set page(foreground: text(24pt)[🥸])
- ///
- /// Reviewer 2 has marked our paper
- /// "Weak Reject" because they did
- /// not understand our approach...
- /// ```
- #[borrowed]
- pub foreground: Option<Content>,
-
- /// The contents of the page(s).
- ///
- /// Multiple pages will be created if the content does not fit on a single
- /// page. A new page with the page properties prior to the function invocation
- /// will be created after the body has been typeset.
- #[required]
- pub body: Content,
-
- /// Whether the page should be aligned to an even or odd page.
- #[internal]
- pub clear_to: Option<Parity>,
-}
-
-impl PageElem {
- /// A document can consist of multiple `PageElem`s, one per run of pages
- /// with equal properties (not one per actual output page!). The `number` is
- /// the physical page number of the first page of this run. It is mutated
- /// while we post-process the pages in this function. This function returns
- /// a fragment consisting of multiple frames, one per output page of this
- /// page run.
- #[tracing::instrument(skip_all)]
- pub fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- page_counter: &mut ManualPageCounter,
- extend_to: Option<Parity>,
- ) -> SourceResult<Fragment> {
- tracing::info!("Page layout");
-
- // When one of the lengths is infinite the page fits its content along
- // that axis.
- let width = self.width(styles).unwrap_or(Abs::inf());
- let height = self.height(styles).unwrap_or(Abs::inf());
- let mut size = Size::new(width, height);
- if self.flipped(styles) {
- std::mem::swap(&mut size.x, &mut size.y);
- }
-
- let mut min = width.min(height);
- if !min.is_finite() {
- min = Paper::A4.width();
- }
-
- // Determine the margins.
- let default = Rel::<Length>::from((2.5 / 21.0) * 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().clone();
- let columns = self.columns(styles);
- if columns.get() > 1 {
- child = ColumnsElem::new(child).with_count(columns).pack();
- }
-
- let area = size - margin.sum_by_axis();
- let mut regions = Regions::repeat(area, area.map(Abs::is_finite));
- regions.root = true;
-
- // Layout the child.
- let mut frames = child.layout(vt, styles, regions)?.into_frames();
-
- // Align the child to the pagebreak's parity.
- // Check for page count after adding the pending frames
- if extend_to
- .is_some_and(|p| !p.matches(page_counter.physical().get() + frames.len()))
- {
- // Insert empty page after the current pages.
- let size = area.map(Abs::is_finite).select(area, Size::zero());
- frames.push(Frame::hard(size));
- }
-
- let fill = self.fill(styles);
- let foreground = Cow::Borrowed(self.foreground(styles));
- let background = Cow::Borrowed(self.background(styles));
- let header_ascent = self.header_ascent(styles);
- let footer_descent = self.footer_descent(styles);
- let numbering = self.numbering(styles);
- let numbering_meta = Meta::PageNumbering(numbering.clone().into_value());
- let number_align = self.number_align(styles);
- let mut header = Cow::Borrowed(self.header(styles));
- let mut footer = Cow::Borrowed(self.footer(styles));
-
- // Construct the numbering (for header or footer).
- let numbering_marginal = Cow::Owned(numbering.as_ref().map(|numbering| {
- let both = match numbering {
- Numbering::Pattern(pattern) => pattern.pieces() >= 2,
- Numbering::Func(_) => true,
- };
-
- let mut counter =
- Counter::new(CounterKey::Page).display(Some(numbering.clone()), both);
-
- // We interpret the Y alignment as selecting header or footer
- // and then ignore it for aligning the actual number.
- if let Some(x) = number_align.x() {
- counter = counter.aligned(x.into());
- }
-
- counter
- }));
-
- if matches!(number_align.y(), Some(VAlign::Top)) {
- header = if header.is_some() { header } else { numbering_marginal };
- } else {
- footer = if footer.is_some() { footer } else { numbering_marginal };
- }
-
- // Post-process pages.
- for frame in frames.iter_mut() {
- tracing::info!("Layouting page #{}", page_counter.physical());
-
- // 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(page_counter.physical()) {
- 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));
- frame.push_positionless_meta(numbering_meta.clone());
-
- // The page size with margins.
- let size = frame.size();
-
- // Realize overlays.
- for (name, marginal) in [
- ("header", &header),
- ("footer", &footer),
- ("background", &background),
- ("foreground", &foreground),
- ] {
- tracing::info!("Layouting {name}");
-
- let Some(content) = &**marginal else { continue };
-
- let (pos, area, align);
- if ptr::eq(marginal, &header) {
- let ascent = header_ascent.relative_to(margin.top);
- pos = Point::with_x(margin.left);
- area = Size::new(pw, margin.top - ascent);
- align = Align::BOTTOM;
- } else if ptr::eq(marginal, &footer) {
- let descent = footer_descent.relative_to(margin.bottom);
- pos = Point::new(margin.left, size.y - margin.bottom + descent);
- area = Size::new(pw, margin.bottom - descent);
- align = Align::TOP;
- } else {
- pos = Point::zero();
- area = size;
- align = HAlign::Center + VAlign::Horizon;
- };
-
- let pod = Regions::one(area, Axes::splat(true));
- let sub = content
- .clone()
- .styled(AlignElem::set_alignment(align))
- .layout(vt, styles, pod)?
- .into_frame();
-
- if ptr::eq(marginal, &header) || ptr::eq(marginal, &background) {
- frame.prepend_frame(pos, sub);
- } else {
- frame.push_frame(pos, sub);
- }
- }
-
- if let Some(fill) = fill {
- frame.fill(fill.clone());
- }
-
- page_counter.visit(vt, frame)?;
-
- // Add a PDF page label if there is a numbering.
- if let Some(num) = numbering {
- if let Some(page_label) = num.apply_pdf(page_counter.logical()) {
- frame.push_positionless_meta(Meta::PdfPageLabel(page_label));
- }
- }
-
- page_counter.step();
- }
-
- Ok(Fragment::frames(frames))
- }
-}
-
-/// 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 => Align::LEFT.into_value(),
- Self::Right => Align::RIGHT.into_value(),
- },
- v: Align => match v {
- Align::LEFT => Self::Left,
- Align::RIGHT => Self::Right,
- _ => bail!("must be `left` or `right`"),
- },
-}
-
-/// A header, footer, foreground or background definition.
-#[derive(Debug, Clone, Hash)]
-pub enum Marginal {
- /// Bare content.
- Content(Content),
- /// A closure mapping from a page number to content.
- Func(Func),
-}
-
-impl Marginal {
- /// Resolve the marginal based on the page number.
- pub fn resolve(&self, vt: &mut Vt, page: usize) -> SourceResult<Cow<'_, Content>> {
- Ok(match self {
- Self::Content(content) => Cow::Borrowed(content),
- Self::Func(func) => Cow::Owned(func.call_vt(vt, [page])?.display()),
- })
- }
-}
-
-cast! {
- Marginal,
- self => match self {
- Self::Content(v) => v.into_value(),
- Self::Func(v) => v.into_value(),
- },
- v: Content => Self::Content(v),
- v: Func => Self::Func(v),
-}
-
-/// A manual page break.
-///
-/// Must not be used inside any containers.
-///
-/// # Example
-/// ```example
-/// The next page contains
-/// more details on compound theory.
-/// #pagebreak()
-///
-/// == Compound Theory
-/// In 1984, the first ...
-/// ```
-#[elem(title = "Page Break")]
-pub struct PagebreakElem {
- /// If `{true}`, the page break is skipped if the current page is already
- /// empty.
- #[default(false)]
- pub weak: bool,
-
- /// If given, ensures that the next page will be an even/odd page, with an
- /// empty page in between if necessary.
- ///
- /// ```example
- /// #set page(height: 30pt)
- ///
- /// First.
- /// #pagebreak(to: "odd")
- /// Third.
- /// ```
- pub to: Option<Parity>,
-}
-
-/// Whether something should be even or odd.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
-pub enum Parity {
- /// Next page will be an even page.
- Even,
- /// Next page will be an odd page.
- Odd,
-}
-
-impl Parity {
- /// Whether the given number matches the parity.
- fn matches(self, number: usize) -> bool {
- match self {
- Self::Even => number % 2 == 0,
- Self::Odd => number % 2 == 1,
- }
- }
-}
-
-/// Specification of a paper.
-#[derive(Debug, Copy, Clone, Hash)]
-pub struct Paper {
- /// The name of the paper.
- name: &'static str,
- /// The width of the paper in millimeters.
- width: Scalar,
- /// The height of the paper in millimeters.
- height: Scalar,
-}
-
-impl Paper {
- /// The width of the paper.
- pub fn width(self) -> Abs {
- Abs::mm(self.width.get())
- }
-
- /// The height of the paper.
- pub fn height(self) -> Abs {
- Abs::mm(self.height.get())
- }
-}
-
-/// Defines paper constants and a paper parsing implementation.
-macro_rules! papers {
- ($(($var:ident: $width:expr, $height: expr, $name:literal))*) => {
- /// Predefined papers.
- ///
- /// Each paper is parsable from its name in kebab-case.
- impl Paper {
- $(pub const $var: Self = Self {
- name: $name,
- width: Scalar::new($width),
- height: Scalar::new($height),
- };)*
- }
-
- impl FromStr for Paper {
- type Err = &'static str;
-
- fn from_str(name: &str) -> Result<Self, Self::Err> {
- match name.to_lowercase().as_str() {
- $($name => Ok(Self::$var),)*
- _ => Err("unknown paper size"),
- }
- }
- }
-
- cast! {
- Paper,
- self => self.name.into_value(),
- $(
- /// Produces a paper of the respective size.
- $name => Self::$var,
- )*
- }
- };
-}
-
-// All paper sizes in mm.
-//
-// Resources:
-// - https://papersizes.io/
-// - https://en.wikipedia.org/wiki/Paper_size
-// - https://www.theedkins.co.uk/jo/units/oldunits/print.htm
-// - https://vintagepaper.co/blogs/news/traditional-paper-sizes
-papers! {
- // ---------------------------------------------------------------------- //
- // ISO 216 A Series
- (A0: 841.0, 1189.0, "a0")
- (A1: 594.0, 841.0, "a1")
- (A2: 420.0, 594.0, "a2")
- (A3: 297.0, 420.0, "a3")
- (A4: 210.0, 297.0, "a4")
- (A5: 148.0, 210.0, "a5")
- (A6: 105.0, 148.0, "a6")
- (A7: 74.0, 105.0, "a7")
- (A8: 52.0, 74.0, "a8")
- (A9: 37.0, 52.0, "a9")
- (A10: 26.0, 37.0, "a10")
- (A11: 18.0, 26.0, "a11")
-
- // ISO 216 B Series
- (ISO_B1: 707.0, 1000.0, "iso-b1")
- (ISO_B2: 500.0, 707.0, "iso-b2")
- (ISO_B3: 353.0, 500.0, "iso-b3")
- (ISO_B4: 250.0, 353.0, "iso-b4")
- (ISO_B5: 176.0, 250.0, "iso-b5")
- (ISO_B6: 125.0, 176.0, "iso-b6")
- (ISO_B7: 88.0, 125.0, "iso-b7")
- (ISO_B8: 62.0, 88.0, "iso-b8")
-
- // ISO 216 C Series
- (ISO_C3: 324.0, 458.0, "iso-c3")
- (ISO_C4: 229.0, 324.0, "iso-c4")
- (ISO_C5: 162.0, 229.0, "iso-c5")
- (ISO_C6: 114.0, 162.0, "iso-c6")
- (ISO_C7: 81.0, 114.0, "iso-c7")
- (ISO_C8: 57.0, 81.0, "iso-c8")
-
- // DIN D Series (extension to ISO)
- (DIN_D3: 272.0, 385.0, "din-d3")
- (DIN_D4: 192.0, 272.0, "din-d4")
- (DIN_D5: 136.0, 192.0, "din-d5")
- (DIN_D6: 96.0, 136.0, "din-d6")
- (DIN_D7: 68.0, 96.0, "din-d7")
- (DIN_D8: 48.0, 68.0, "din-d8")
-
- // SIS (used in academia)
- (SIS_G5: 169.0, 239.0, "sis-g5")
- (SIS_E5: 115.0, 220.0, "sis-e5")
-
- // ANSI Extensions
- (ANSI_A: 216.0, 279.0, "ansi-a")
- (ANSI_B: 279.0, 432.0, "ansi-b")
- (ANSI_C: 432.0, 559.0, "ansi-c")
- (ANSI_D: 559.0, 864.0, "ansi-d")
- (ANSI_E: 864.0, 1118.0, "ansi-e")
-
- // ANSI Architectural Paper
- (ARCH_A: 229.0, 305.0, "arch-a")
- (ARCH_B: 305.0, 457.0, "arch-b")
- (ARCH_C: 457.0, 610.0, "arch-c")
- (ARCH_D: 610.0, 914.0, "arch-d")
- (ARCH_E1: 762.0, 1067.0, "arch-e1")
- (ARCH_E: 914.0, 1219.0, "arch-e")
-
- // JIS B Series
- (JIS_B0: 1030.0, 1456.0, "jis-b0")
- (JIS_B1: 728.0, 1030.0, "jis-b1")
- (JIS_B2: 515.0, 728.0, "jis-b2")
- (JIS_B3: 364.0, 515.0, "jis-b3")
- (JIS_B4: 257.0, 364.0, "jis-b4")
- (JIS_B5: 182.0, 257.0, "jis-b5")
- (JIS_B6: 128.0, 182.0, "jis-b6")
- (JIS_B7: 91.0, 128.0, "jis-b7")
- (JIS_B8: 64.0, 91.0, "jis-b8")
- (JIS_B9: 45.0, 64.0, "jis-b9")
- (JIS_B10: 32.0, 45.0, "jis-b10")
- (JIS_B11: 22.0, 32.0, "jis-b11")
-
- // SAC D Series
- (SAC_D0: 764.0, 1064.0, "sac-d0")
- (SAC_D1: 532.0, 760.0, "sac-d1")
- (SAC_D2: 380.0, 528.0, "sac-d2")
- (SAC_D3: 264.0, 376.0, "sac-d3")
- (SAC_D4: 188.0, 260.0, "sac-d4")
- (SAC_D5: 130.0, 184.0, "sac-d5")
- (SAC_D6: 92.0, 126.0, "sac-d6")
-
- // ISO 7810 ID
- (ISO_ID_1: 85.6, 53.98, "iso-id-1")
- (ISO_ID_2: 74.0, 105.0, "iso-id-2")
- (ISO_ID_3: 88.0, 125.0, "iso-id-3")
-
- // ---------------------------------------------------------------------- //
- // Asia
- (ASIA_F4: 210.0, 330.0, "asia-f4")
-
- // Japan
- (JP_SHIROKU_BAN_4: 264.0, 379.0, "jp-shiroku-ban-4")
- (JP_SHIROKU_BAN_5: 189.0, 262.0, "jp-shiroku-ban-5")
- (JP_SHIROKU_BAN_6: 127.0, 188.0, "jp-shiroku-ban-6")
- (JP_KIKU_4: 227.0, 306.0, "jp-kiku-4")
- (JP_KIKU_5: 151.0, 227.0, "jp-kiku-5")
- (JP_BUSINESS_CARD: 91.0, 55.0, "jp-business-card")
-
- // China
- (CN_BUSINESS_CARD: 90.0, 54.0, "cn-business-card")
-
- // Europe
- (EU_BUSINESS_CARD: 85.0, 55.0, "eu-business-card")
-
- // French Traditional (AFNOR)
- (FR_TELLIERE: 340.0, 440.0, "fr-tellière")
- (FR_COURONNE_ECRITURE: 360.0, 460.0, "fr-couronne-écriture")
- (FR_COURONNE_EDITION: 370.0, 470.0, "fr-couronne-édition")
- (FR_RAISIN: 500.0, 650.0, "fr-raisin")
- (FR_CARRE: 450.0, 560.0, "fr-carré")
- (FR_JESUS: 560.0, 760.0, "fr-jésus")
-
- // United Kingdom Imperial
- (UK_BRIEF: 406.4, 342.9, "uk-brief")
- (UK_DRAFT: 254.0, 406.4, "uk-draft")
- (UK_FOOLSCAP: 203.2, 330.2, "uk-foolscap")
- (UK_QUARTO: 203.2, 254.0, "uk-quarto")
- (UK_CROWN: 508.0, 381.0, "uk-crown")
- (UK_BOOK_A: 111.0, 178.0, "uk-book-a")
- (UK_BOOK_B: 129.0, 198.0, "uk-book-b")
-
- // Unites States
- (US_LETTER: 215.9, 279.4, "us-letter")
- (US_LEGAL: 215.9, 355.6, "us-legal")
- (US_TABLOID: 279.4, 431.8, "us-tabloid")
- (US_EXECUTIVE: 84.15, 266.7, "us-executive")
- (US_FOOLSCAP_FOLIO: 215.9, 342.9, "us-foolscap-folio")
- (US_STATEMENT: 139.7, 215.9, "us-statement")
- (US_LEDGER: 431.8, 279.4, "us-ledger")
- (US_OFICIO: 215.9, 340.36, "us-oficio")
- (US_GOV_LETTER: 203.2, 266.7, "us-gov-letter")
- (US_GOV_LEGAL: 215.9, 330.2, "us-gov-legal")
- (US_BUSINESS_CARD: 88.9, 50.8, "us-business-card")
- (US_DIGEST: 139.7, 215.9, "us-digest")
- (US_TRADE: 152.4, 228.6, "us-trade")
-
- // ---------------------------------------------------------------------- //
- // Other
- (NEWSPAPER_COMPACT: 280.0, 430.0, "newspaper-compact")
- (NEWSPAPER_BERLINER: 315.0, 470.0, "newspaper-berliner")
- (NEWSPAPER_BROADSHEET: 381.0, 578.0, "newspaper-broadsheet")
- (PRESENTATION_16_9: 297.0, 167.0625, "presentation-16-9")
- (PRESENTATION_4_3: 280.0, 210.0, "presentation-4-3")
-}
diff --git a/crates/typst-library/src/layout/par.rs b/crates/typst-library/src/layout/par.rs
deleted file mode 100644
index 2d7c9080..00000000
--- a/crates/typst-library/src/layout/par.rs
+++ /dev/null
@@ -1,1520 +0,0 @@
-use comemo::Prehashed;
-use typst::eval::Tracer;
-use typst::model::DelayedErrors;
-use unicode_bidi::{BidiInfo, Level as BidiLevel};
-use unicode_script::{Script, UnicodeScript};
-
-use crate::layout::{AlignElem, BoxElem, HElem, Sizing, Spacing};
-use crate::math::EquationElem;
-use crate::prelude::*;
-use crate::text::{
- breakpoints, char_is_cjk_script, is_gb_style, shape, Breakpoint, LinebreakElem,
- Quoter, Quotes, ShapedGlyph, ShapedText, SmartquoteElem, SpaceElem, TextElem,
- BEGIN_PUNCT_PAT, END_PUNCT_PAT,
-};
-
-/// Arranges text, spacing and inline-level elements into a paragraph.
-///
-/// Although this function is primarily used in set rules to affect paragraph
-/// properties, it can also be used to explicitly render its argument onto a
-/// paragraph of its own.
-///
-/// # Example
-/// ```example
-/// #show par: set block(spacing: 0.65em)
-/// #set par(
-/// first-line-indent: 1em,
-/// justify: true,
-/// )
-///
-/// We proceed by contradiction.
-/// Suppose that there exists a set
-/// of positive integers $a$, $b$, and
-/// $c$ that satisfies the equation
-/// $a^n + b^n = c^n$ for some
-/// integer value of $n > 2$.
-///
-/// Without loss of generality,
-/// let $a$ be the smallest of the
-/// three integers. Then, we ...
-/// ```
-#[elem(title = "Paragraph", Construct)]
-pub struct ParElem {
- /// The spacing between lines.
- #[resolve]
- #[ghost]
- #[default(Em::new(0.65).into())]
- pub leading: Length,
-
- /// Whether to justify text in its line.
- ///
- /// Hyphenation will be enabled for justified paragraphs if the
- /// [text function's `hyphenate` property]($text.hyphenate) is set to
- /// `{auto}` and the current language is known.
- ///
- /// Note that the current [alignment]($align) still has an effect on the
- /// placement of the last line except if it ends with a
- /// [justified line break]($linebreak.justify).
- #[ghost]
- #[default(false)]
- pub justify: bool,
-
- /// How to determine line breaks.
- ///
- /// When this property is set to `{auto}`, its default value, optimized line
- /// breaks will be used for justified paragraphs. Enabling optimized line
- /// breaks for ragged paragraphs may also be worthwhile to improve the
- /// appearance of the text.
- ///
- /// ```example
- /// #set page(width: 207pt)
- /// #set par(linebreaks: "simple")
- /// Some texts feature many longer
- /// words. Those are often exceedingly
- /// challenging to break in a visually
- /// pleasing way.
- ///
- /// #set par(linebreaks: "optimized")
- /// Some texts feature many longer
- /// words. Those are often exceedingly
- /// challenging to break in a visually
- /// pleasing way.
- /// ```
- #[ghost]
- pub linebreaks: Smart<Linebreaks>,
-
- /// The indent the first line of a paragraph should have.
- ///
- /// Only the first line of a consecutive paragraph will be indented (not
- /// the first one in a block or on the page).
- ///
- /// By typographic convention, paragraph breaks are indicated either by some
- /// space between paragraphs or by indented first lines. Consider reducing
- /// the [paragraph spacing]($block.spacing) to the [`leading`] when
- /// using this property (e.g. using
- /// `[#show par: set block(spacing: 0.65em)]`).
- #[ghost]
- pub first_line_indent: Length,
-
- /// The indent all but the first line of a paragraph should have.
- #[ghost]
- #[resolve]
- pub hanging_indent: Length,
-
- /// The contents of the paragraph.
- #[external]
- #[required]
- pub body: Content,
-
- /// The paragraph's children.
- #[internal]
- #[variadic]
- pub children: Vec<Prehashed<Content>>,
-}
-
-impl Construct for ParElem {
- fn construct(vm: &mut Vm, args: &mut Args) -> SourceResult<Content> {
- // The paragraph constructor is special: It doesn't create a paragraph
- // element. Instead, it just ensures that the passed content lives in a
- // separate paragraph and styles it.
- let styles = Self::set(vm, args)?;
- let body = args.expect::<Content>("body")?;
- Ok(Content::sequence([
- ParbreakElem::new().pack(),
- body.styled_with_map(styles),
- ParbreakElem::new().pack(),
- ]))
- }
-}
-
-impl ParElem {
- /// Layout the paragraph into a collection of lines.
- #[tracing::instrument(name = "ParElement::layout", skip_all)]
- pub fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- consecutive: bool,
- region: Size,
- expand: bool,
- ) -> SourceResult<Fragment> {
- #[comemo::memoize]
- #[allow(clippy::too_many_arguments)]
- fn cached(
- par: &ParElem,
- world: Tracked<dyn World + '_>,
- introspector: Tracked<Introspector>,
- locator: Tracked<Locator>,
- delayed: TrackedMut<DelayedErrors>,
- tracer: TrackedMut<Tracer>,
- styles: StyleChain,
- consecutive: bool,
- region: Size,
- expand: bool,
- ) -> SourceResult<Fragment> {
- let mut locator = Locator::chained(locator);
- let mut vt = Vt {
- world,
- introspector,
- locator: &mut locator,
- delayed,
- tracer,
- };
- let children = par.children();
-
- // Collect all text into one string for BiDi analysis.
- let (text, segments, spans) = collect(children, &styles, consecutive)?;
-
- // Perform BiDi analysis and then prepare paragraph layout by building a
- // representation on which we can do line breaking without layouting
- // each and every line from scratch.
- let p = prepare(&mut vt, children, &text, segments, spans, styles, region)?;
-
- // Break the paragraph into lines.
- let lines = linebreak(&vt, &p, region.x - p.hang);
-
- // Stack the lines into one frame per region.
- finalize(&mut vt, &p, &lines, region, expand)
- }
-
- let fragment = cached(
- self,
- vt.world,
- vt.introspector,
- vt.locator.track(),
- TrackedMut::reborrow_mut(&mut vt.delayed),
- TrackedMut::reborrow_mut(&mut vt.tracer),
- styles,
- consecutive,
- region,
- expand,
- )?;
-
- vt.locator.visit_frames(&fragment);
- Ok(fragment)
- }
-}
-
-/// How to determine line breaks in a paragraph.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
-pub enum Linebreaks {
- /// Determine the line breaks in a simple first-fit style.
- Simple,
- /// Optimize the line breaks for the whole paragraph.
- ///
- /// Typst will try to produce more evenly filled lines of text by
- /// considering the whole paragraph when calculating line breaks.
- Optimized,
-}
-
-/// A paragraph break.
-///
-/// This starts a new paragraph. Especially useful when used within code like
-/// [for loops]($scripting/#loops). Multiple consecutive
-/// paragraph breaks collapse into a single one.
-///
-/// # Example
-/// ```example
-/// #for i in range(3) {
-/// [Blind text #i: ]
-/// lorem(5)
-/// parbreak()
-/// }
-/// ```
-///
-/// # Syntax
-/// Instead of calling this function, you can insert a blank line into your
-/// markup to create a paragraph break.
-#[elem(title = "Paragraph Break", Unlabellable)]
-pub struct ParbreakElem {}
-
-impl Unlabellable for ParbreakElem {}
-
-/// Range of a substring of text.
-type Range = std::ops::Range<usize>;
-
-// The characters by which spacing, inline content and pins are replaced in the
-// paragraph's full text.
-const SPACING_REPLACE: char = ' '; // Space
-const OBJ_REPLACE: char = '\u{FFFC}'; // Object Replacement Character
-
-/// A paragraph representation in which children are already layouted and text
-/// is already preshaped.
-///
-/// In many cases, we can directly reuse these results when constructing a line.
-/// Only when a line break falls onto a text index that is not safe-to-break per
-/// rustybuzz, we have to reshape that portion.
-pub(crate) struct Preparation<'a> {
- /// Bidirectional text embedding levels for the paragraph.
- pub bidi: BidiInfo<'a>,
- /// Text runs, spacing and layouted elements.
- pub items: Vec<Item<'a>>,
- /// The span mapper.
- pub spans: SpanMapper,
- /// Whether to hyphenate if it's the same for all children.
- pub hyphenate: Option<bool>,
- /// The text language if it's the same for all children.
- pub lang: Option<Lang>,
- /// The paragraph's resolved horizontal alignment.
- pub align: FixedAlign,
- /// Whether to justify the paragraph.
- pub justify: bool,
- /// The paragraph's hanging indent.
- pub hang: Abs,
- /// Whether to add spacing between CJK and Latin characters.
- pub cjk_latin_spacing: bool,
- /// Whether font fallback is enabled for this paragraph.
- pub fallback: bool,
- /// The leading of the paragraph.
- pub leading: Abs,
- /// How to determine line breaks.
- pub linebreaks: Smart<Linebreaks>,
- /// The text size.
- pub size: Abs,
-}
-
-impl<'a> Preparation<'a> {
- /// Find the item that contains the given `text_offset`.
- pub fn find(&self, text_offset: usize) -> Option<&Item<'a>> {
- let mut cursor = 0;
- for item in &self.items {
- let end = cursor + item.len();
- if (cursor..end).contains(&text_offset) {
- return Some(item);
- }
- cursor = end;
- }
- None
- }
-
- /// Return the items that intersect the given `text_range`.
- ///
- /// Returns the expanded range around the items and the items.
- pub fn slice(&self, text_range: Range) -> (Range, &[Item<'a>]) {
- let mut cursor = 0;
- let mut start = 0;
- let mut end = 0;
- let mut expanded = text_range.clone();
-
- for (i, item) in self.items.iter().enumerate() {
- if cursor <= text_range.start {
- start = i;
- expanded.start = cursor;
- }
-
- let len = item.len();
- if cursor < text_range.end || cursor + len <= text_range.end {
- end = i + 1;
- expanded.end = cursor + len;
- } else {
- break;
- }
-
- cursor += len;
- }
-
- (expanded, &self.items[start..end])
- }
-}
-
-/// A segment of one or multiple collapsed children.
-#[derive(Debug, Copy, Clone)]
-enum Segment<'a> {
- /// One or multiple collapsed text or text-equivalent children. Stores how
- /// long the segment is (in bytes of the full text string).
- Text(usize),
- /// Horizontal spacing between other segments.
- Spacing(Spacing),
- /// A mathematical equation.
- Equation(&'a EquationElem),
- /// A box with arbitrary content.
- Box(&'a BoxElem, bool),
- /// Metadata.
- Meta,
-}
-
-impl Segment<'_> {
- /// The text length of the item.
- fn len(&self) -> usize {
- match *self {
- Self::Text(len) => len,
- Self::Spacing(_) => SPACING_REPLACE.len_utf8(),
- Self::Box(_, true) => SPACING_REPLACE.len_utf8(),
- Self::Equation(_) | Self::Box(_, _) => OBJ_REPLACE.len_utf8(),
- Self::Meta => 0,
- }
- }
-}
-
-/// A prepared item in a paragraph layout.
-#[derive(Debug)]
-pub(crate) enum Item<'a> {
- /// A shaped text run with consistent style and direction.
- Text(ShapedText<'a>),
- /// Absolute spacing between other items.
- Absolute(Abs),
- /// Fractional spacing between other items.
- Fractional(Fr, Option<(&'a BoxElem, StyleChain<'a>)>),
- /// Layouted inline-level content.
- Frame(Frame),
- /// Metadata.
- Meta(Frame),
-}
-
-impl<'a> Item<'a> {
- /// If this a text item, return it.
- pub fn text(&self) -> Option<&ShapedText<'a>> {
- match self {
- Self::Text(shaped) => Some(shaped),
- _ => None,
- }
- }
-
- pub fn text_mut(&mut self) -> Option<&mut ShapedText<'a>> {
- match self {
- Self::Text(shaped) => Some(shaped),
- _ => None,
- }
- }
-
- /// The text length of the item.
- pub fn len(&self) -> usize {
- match self {
- Self::Text(shaped) => shaped.text.len(),
- Self::Absolute(_) | Self::Fractional(_, _) => SPACING_REPLACE.len_utf8(),
- Self::Frame(_) => OBJ_REPLACE.len_utf8(),
- Self::Meta(_) => 0,
- }
- }
-
- /// The natural layouted width of the item.
- pub fn width(&self) -> Abs {
- match self {
- Self::Text(shaped) => shaped.width,
- Self::Absolute(v) => *v,
- Self::Frame(frame) => frame.width(),
- Self::Fractional(_, _) | Self::Meta(_) => Abs::zero(),
- }
- }
-}
-
-/// Maps byte offsets back to spans.
-#[derive(Default)]
-pub struct SpanMapper(Vec<(usize, Span)>);
-
-impl SpanMapper {
- /// Create a new span mapper.
- pub fn new() -> Self {
- Self::default()
- }
-
- /// Push a span for a segment with the given length.
- pub fn push(&mut self, len: usize, span: Span) {
- self.0.push((len, span));
- }
-
- /// Determine the span at the given byte offset.
- ///
- /// May return a detached span.
- pub fn span_at(&self, offset: usize) -> (Span, u16) {
- let mut cursor = 0;
- for &(len, span) in &self.0 {
- if (cursor..=cursor + len).contains(&offset) {
- return (span, u16::try_from(offset - cursor).unwrap_or(0));
- }
- cursor += len;
- }
- (Span::detached(), 0)
- }
-}
-
-/// A layouted line, consisting of a sequence of layouted paragraph items that
-/// are mostly borrowed from the preparation phase. This type enables you to
-/// measure the size of a line in a range before committing to building the
-/// line's frame.
-///
-/// At most two paragraph items must be created individually for this line: The
-/// first and last one since they may be broken apart by the start or end of the
-/// line, respectively. But even those can partially reuse previous results when
-/// the break index is safe-to-break per rustybuzz.
-struct Line<'a> {
- /// Bidi information about the paragraph.
- bidi: &'a BidiInfo<'a>,
- /// The trimmed range the line spans in the paragraph.
- trimmed: Range,
- /// The untrimmed end where the line ends.
- end: usize,
- /// A reshaped text item if the line sliced up a text item at the start.
- first: Option<Item<'a>>,
- /// Inner items which don't need to be reprocessed.
- inner: &'a [Item<'a>],
- /// A reshaped text item if the line sliced up a text item at the end. If
- /// there is only one text item, this takes precedence over `first`.
- last: Option<Item<'a>>,
- /// The width of the line.
- width: Abs,
- /// Whether the line should be justified.
- justify: bool,
- /// Whether the line ends with a hyphen or dash, either naturally or through
- /// hyphenation.
- dash: bool,
-}
-
-impl<'a> Line<'a> {
- /// Iterate over the line's items.
- fn items(&self) -> impl Iterator<Item = &Item<'a>> {
- self.first.iter().chain(self.inner).chain(&self.last)
- }
-
- /// Return items that intersect the given `text_range`.
- fn slice(&self, text_range: Range) -> impl Iterator<Item = &Item<'a>> {
- let mut cursor = self.trimmed.start;
- let mut start = 0;
- let mut end = 0;
-
- for (i, item) in self.items().enumerate() {
- if cursor <= text_range.start {
- start = i;
- }
-
- let len = item.len();
- if cursor < text_range.end || cursor + len <= text_range.end {
- end = i + 1;
- } else {
- break;
- }
-
- cursor += len;
- }
-
- self.items().skip(start).take(end - start)
- }
-
- /// How many glyphs are in the text where we can insert additional
- /// space when encountering underfull lines.
- fn justifiables(&self) -> usize {
- let mut count = 0;
- for shaped in self.items().filter_map(Item::text) {
- count += shaped.justifiables();
- }
- // CJK character at line end should not be adjusted.
- if self
- .items()
- .last()
- .and_then(Item::text)
- .map(|s| s.cjk_justifiable_at_last())
- .unwrap_or(false)
- {
- count -= 1;
- }
-
- count
- }
-
- /// How much can the line stretch
- fn stretchability(&self) -> Abs {
- self.items().filter_map(Item::text).map(|s| s.stretchability()).sum()
- }
-
- /// How much can the line shrink
- fn shrinkability(&self) -> Abs {
- self.items().filter_map(Item::text).map(|s| s.shrinkability()).sum()
- }
-
- /// The sum of fractions in the line.
- fn fr(&self) -> Fr {
- self.items()
- .filter_map(|item| match item {
- Item::Fractional(fr, _) => Some(*fr),
- _ => None,
- })
- .sum()
- }
-}
-
-/// Collect all text of the paragraph into one string. This also performs
-/// string-level preprocessing like case transformations.
-#[allow(clippy::type_complexity)]
-fn collect<'a>(
- children: &'a [Prehashed<Content>],
- styles: &'a StyleChain<'a>,
- consecutive: bool,
-) -> SourceResult<(String, Vec<(Segment<'a>, StyleChain<'a>)>, SpanMapper)> {
- let mut full = String::new();
- let mut quoter = Quoter::new();
- let mut segments = Vec::with_capacity(2 + children.len());
- let mut spans = SpanMapper::new();
- let mut iter = children.iter().map(|c| &**c).peekable();
-
- let first_line_indent = ParElem::first_line_indent_in(*styles);
- if !first_line_indent.is_zero()
- && consecutive
- && AlignElem::alignment_in(*styles).resolve(*styles).x
- == TextElem::dir_in(*styles).start().into()
- {
- full.push(SPACING_REPLACE);
- segments.push((Segment::Spacing(first_line_indent.into()), *styles));
- }
-
- let hang = ParElem::hanging_indent_in(*styles);
- if !hang.is_zero() {
- full.push(SPACING_REPLACE);
- segments.push((Segment::Spacing((-hang).into()), *styles));
- }
-
- while let Some(mut child) = iter.next() {
- let outer = styles;
- let mut styles = *styles;
- if let Some((elem, local)) = child.to_styled() {
- child = elem;
- styles = outer.chain(local);
- }
-
- let segment = if child.is::<SpaceElem>() {
- full.push(' ');
- Segment::Text(1)
- } else if let Some(elem) = child.to::<TextElem>() {
- let prev = full.len();
- if let Some(case) = TextElem::case_in(styles) {
- full.push_str(&case.apply(elem.text()));
- } else {
- full.push_str(elem.text());
- }
- Segment::Text(full.len() - prev)
- } else if let Some(elem) = child.to::<HElem>() {
- if elem.amount().is_zero() {
- continue;
- }
-
- full.push(SPACING_REPLACE);
- Segment::Spacing(*elem.amount())
- } else if let Some(elem) = child.to::<LinebreakElem>() {
- let c = if elem.justify(styles) { '\u{2028}' } else { '\n' };
- full.push(c);
- Segment::Text(c.len_utf8())
- } else if let Some(elem) = child.to::<SmartquoteElem>() {
- let prev = full.len();
- if SmartquoteElem::enabled_in(styles) {
- let quotes = SmartquoteElem::quotes_in(styles);
- let lang = TextElem::lang_in(styles);
- let region = TextElem::region_in(styles);
- let quotes = Quotes::new(
- quotes,
- lang,
- region,
- SmartquoteElem::alternative_in(styles),
- );
- let peeked = iter.peek().and_then(|child| {
- let child = if let Some((child, _)) = child.to_styled() {
- child
- } else {
- child
- };
- if let Some(elem) = child.to::<TextElem>() {
- elem.text().chars().next()
- } else if child.is::<SmartquoteElem>() {
- Some('"')
- } else if child.is::<SpaceElem>()
- || child.is::<HElem>()
- || child.is::<LinebreakElem>()
- {
- Some(SPACING_REPLACE)
- } else {
- Some(OBJ_REPLACE)
- }
- });
-
- full.push_str(quoter.quote(&quotes, elem.double(styles), peeked));
- } else {
- full.push(if elem.double(styles) { '"' } else { '\'' });
- }
- Segment::Text(full.len() - prev)
- } else if let Some(elem) = child.to::<EquationElem>() {
- full.push(OBJ_REPLACE);
- Segment::Equation(elem)
- } else if let Some(elem) = child.to::<BoxElem>() {
- let frac = elem.width(styles).is_fractional();
- full.push(if frac { SPACING_REPLACE } else { OBJ_REPLACE });
- Segment::Box(elem, frac)
- } else if child.is::<MetaElem>() {
- Segment::Meta
- } else {
- bail!(child.span(), "unexpected paragraph child");
- };
-
- if let Some(last) = full.chars().last() {
- quoter.last(last, child.is::<SmartquoteElem>());
- }
-
- spans.push(segment.len(), child.span());
-
- if let (Some((Segment::Text(last_len), last_styles)), Segment::Text(len)) =
- (segments.last_mut(), segment)
- {
- if *last_styles == styles {
- *last_len += len;
- continue;
- }
- }
-
- segments.push((segment, styles));
- }
-
- Ok((full, segments, spans))
-}
-
-/// Prepare paragraph layout by shaping the whole paragraph and layouting all
-/// contained inline-level content.
-fn prepare<'a>(
- vt: &mut Vt,
- children: &'a [Prehashed<Content>],
- text: &'a str,
- segments: Vec<(Segment<'a>, StyleChain<'a>)>,
- spans: SpanMapper,
- styles: StyleChain<'a>,
- region: Size,
-) -> SourceResult<Preparation<'a>> {
- let dir = TextElem::dir_in(styles);
- let bidi = BidiInfo::new(
- text,
- match dir {
- Dir::LTR => Some(BidiLevel::ltr()),
- Dir::RTL => Some(BidiLevel::rtl()),
- _ => None,
- },
- );
-
- let mut cursor = 0;
- let mut items = Vec::with_capacity(segments.len());
-
- // Shape / layout the children and collect them into items.
- for (segment, styles) in segments {
- let end = cursor + segment.len();
- match segment {
- Segment::Text(_) => {
- shape_range(&mut items, vt, &bidi, cursor..end, &spans, styles);
- }
- Segment::Spacing(spacing) => match spacing {
- Spacing::Rel(v) => {
- let resolved = v.resolve(styles).relative_to(region.x);
- items.push(Item::Absolute(resolved));
- }
- Spacing::Fr(v) => {
- items.push(Item::Fractional(v, None));
- }
- },
- Segment::Equation(equation) => {
- let pod = Regions::one(region, Axes::splat(false));
- let mut frame = equation.layout(vt, styles, pod)?.into_frame();
- frame.translate(Point::with_y(TextElem::baseline_in(styles)));
- items.push(Item::Frame(frame));
- }
- Segment::Box(elem, _) => {
- if let Sizing::Fr(v) = elem.width(styles) {
- items.push(Item::Fractional(v, Some((elem, styles))));
- } else {
- let pod = Regions::one(region, Axes::splat(false));
- let mut frame = elem.layout(vt, styles, pod)?.into_frame();
- frame.translate(Point::with_y(TextElem::baseline_in(styles)));
- items.push(Item::Frame(frame));
- }
- }
- Segment::Meta => {
- let mut frame = Frame::soft(Size::zero());
- frame.meta(styles, true);
- items.push(Item::Meta(frame));
- }
- }
-
- cursor = end;
- }
-
- let cjk_latin_spacing = TextElem::cjk_latin_spacing_in(styles).is_auto();
- if cjk_latin_spacing {
- add_cjk_latin_spacing(&mut items);
- }
-
- Ok(Preparation {
- bidi,
- items,
- spans,
- hyphenate: shared_get(styles, children, TextElem::hyphenate_in),
- lang: shared_get(styles, children, TextElem::lang_in),
- align: AlignElem::alignment_in(styles).resolve(styles).x,
- justify: ParElem::justify_in(styles),
- hang: ParElem::hanging_indent_in(styles),
- cjk_latin_spacing,
- fallback: TextElem::fallback_in(styles),
- leading: ParElem::leading_in(styles),
- linebreaks: ParElem::linebreaks_in(styles),
- size: TextElem::size_in(styles),
- })
-}
-
-/// Add some spacing between Han characters and western characters.
-/// See Requirements for Chinese Text Layout, Section 3.2.2 Mixed Text Composition in Horizontal
-/// Written Mode
-fn add_cjk_latin_spacing(items: &mut [Item]) {
- let mut items = items.iter_mut().filter(|x| !matches!(x, Item::Meta(_))).peekable();
- let mut prev: Option<&ShapedGlyph> = None;
- while let Some(item) = items.next() {
- let Some(text) = item.text_mut() else {
- prev = None;
- continue;
- };
-
- // Since we only call this function in [`prepare`], we can assume
- // that the Cow is owned, and `to_mut` can be called without overhead.
- debug_assert!(matches!(text.glyphs, std::borrow::Cow::Owned(_)));
- let mut glyphs = text.glyphs.to_mut().iter_mut().peekable();
-
- while let Some(glyph) = glyphs.next() {
- let next = glyphs.peek().map(|n| n as _).or_else(|| {
- items
- .peek()
- .and_then(|i| i.text())
- .and_then(|shaped| shaped.glyphs.first())
- });
-
- // Case 1: CJK followed by a Latin character
- if glyph.is_cjk_script() && next.map_or(false, |g| g.is_letter_or_number()) {
- // The spacing is default to 1/4 em, and can be shrunk to 1/8 em.
- glyph.x_advance += Em::new(0.25);
- glyph.adjustability.shrinkability.1 += Em::new(0.125);
- text.width += Em::new(0.25).at(text.size);
- }
-
- // Case 2: Latin followed by a CJK character
- if glyph.is_cjk_script() && prev.map_or(false, |g| g.is_letter_or_number()) {
- glyph.x_advance += Em::new(0.25);
- glyph.x_offset += Em::new(0.25);
- glyph.adjustability.shrinkability.0 += Em::new(0.125);
- text.width += Em::new(0.25).at(text.size);
- }
-
- prev = Some(glyph);
- }
- }
-}
-
-/// Group a range of text by BiDi level and script, shape the runs and generate
-/// items for them.
-fn shape_range<'a>(
- items: &mut Vec<Item<'a>>,
- vt: &Vt,
- bidi: &BidiInfo<'a>,
- range: Range,
- spans: &SpanMapper,
- styles: StyleChain<'a>,
-) {
- let script = TextElem::script_in(styles);
- let lang = TextElem::lang_in(styles);
- let region = TextElem::region_in(styles);
- let mut process = |range: Range, level: BidiLevel| {
- let dir = if level.is_ltr() { Dir::LTR } else { Dir::RTL };
- let shaped =
- shape(vt, range.start, &bidi.text[range], spans, styles, dir, lang, region);
- items.push(Item::Text(shaped));
- };
-
- let mut prev_level = BidiLevel::ltr();
- let mut prev_script = Script::Unknown;
- let mut cursor = range.start;
-
- // Group by embedding level and script. If the text's script is explicitly
- // set (rather than inferred from the glyphs), we keep the script at an
- // unchanging `Script::Unknown` so that only level changes cause breaks.
- for i in range.clone() {
- if !bidi.text.is_char_boundary(i) {
- continue;
- }
-
- let level = bidi.levels[i];
- let curr_script = match script {
- Smart::Auto => {
- bidi.text[i..].chars().next().map_or(Script::Unknown, |c| c.script())
- }
- Smart::Custom(_) => Script::Unknown,
- };
-
- if level != prev_level || !is_compatible(curr_script, prev_script) {
- if cursor < i {
- process(cursor..i, prev_level);
- }
- cursor = i;
- prev_level = level;
- prev_script = curr_script;
- } else if is_generic_script(prev_script) {
- prev_script = curr_script;
- }
- }
-
- process(cursor..range.end, prev_level);
-}
-
-/// Whether this is not a specific script.
-fn is_generic_script(script: Script) -> bool {
- matches!(script, Script::Unknown | Script::Common | Script::Inherited)
-}
-
-/// Whether these script can be part of the same shape run.
-fn is_compatible(a: Script, b: Script) -> bool {
- is_generic_script(a) || is_generic_script(b) || a == b
-}
-
-/// Get a style property, but only if it is the same for all children of the
-/// paragraph.
-fn shared_get<T: PartialEq>(
- styles: StyleChain<'_>,
- children: &[Prehashed<Content>],
- getter: fn(StyleChain) -> T,
-) -> Option<T> {
- let value = getter(styles);
- children
- .iter()
- .filter_map(|child| child.to_styled())
- .all(|(_, local)| getter(styles.chain(local)) == value)
- .then_some(value)
-}
-
-/// Find suitable linebreaks.
-fn linebreak<'a>(vt: &Vt, p: &'a Preparation<'a>, width: Abs) -> Vec<Line<'a>> {
- let linebreaks = p.linebreaks.unwrap_or_else(|| {
- if p.justify {
- Linebreaks::Optimized
- } else {
- Linebreaks::Simple
- }
- });
-
- match linebreaks {
- Linebreaks::Simple => linebreak_simple(vt, p, width),
- Linebreaks::Optimized => linebreak_optimized(vt, p, width),
- }
-}
-
-/// Perform line breaking in simple first-fit style. This means that we build
-/// lines greedily, always taking the longest possible line. This may lead to
-/// very unbalanced line, but is fast and simple.
-fn linebreak_simple<'a>(vt: &Vt, p: &'a Preparation<'a>, width: Abs) -> Vec<Line<'a>> {
- let mut lines = Vec::with_capacity(16);
- let mut start = 0;
- let mut last = None;
-
- breakpoints(p, |end, breakpoint| {
- // Compute the line and its size.
- let mut attempt = line(vt, p, start..end, breakpoint);
-
- // If the line doesn't fit anymore, we push the last fitting attempt
- // into the stack and rebuild the line from the attempt's end. The
- // resulting line cannot be broken up further.
- if !width.fits(attempt.width) {
- if let Some((last_attempt, last_end)) = last.take() {
- lines.push(last_attempt);
- start = last_end;
- attempt = line(vt, p, start..end, breakpoint);
- }
- }
-
- // Finish the current line if there is a mandatory line break (i.e.
- // due to "\n") or if the line doesn't fit horizontally already
- // since then no shorter line will be possible.
- if breakpoint == Breakpoint::Mandatory || !width.fits(attempt.width) {
- lines.push(attempt);
- start = end;
- last = None;
- } else {
- last = Some((attempt, end));
- }
- });
-
- if let Some((line, _)) = last {
- lines.push(line);
- }
-
- lines
-}
-
-/// Perform line breaking in optimized Knuth-Plass style. Here, we use more
-/// context to determine the line breaks than in the simple first-fit style. For
-/// example, we might choose to cut a line short even though there is still a
-/// bit of space to improve the fit of one of the following lines. The
-/// Knuth-Plass algorithm is based on the idea of "cost". A line which has a
-/// very tight or very loose fit has a higher cost than one that is just right.
-/// Ending a line with a hyphen incurs extra cost and endings two successive
-/// lines with hyphens even more.
-///
-/// To find the layout with the minimal total cost the algorithm uses dynamic
-/// programming: For each possible breakpoint it determines the optimal
-/// paragraph layout _up to that point_. It walks over all possible start points
-/// for a line ending at that point and finds the one for which the cost of the
-/// line plus the cost of the optimal paragraph up to the start point (already
-/// computed and stored in dynamic programming table) is minimal. The final
-/// result is simply the layout determined for the last breakpoint at the end of
-/// text.
-fn linebreak_optimized<'a>(vt: &Vt, p: &'a Preparation<'a>, width: Abs) -> Vec<Line<'a>> {
- /// The cost of a line or paragraph layout.
- type Cost = f64;
-
- /// An entry in the dynamic programming table.
- struct Entry<'a> {
- pred: usize,
- total: Cost,
- line: Line<'a>,
- }
-
- // Cost parameters.
- const HYPH_COST: Cost = 0.5;
- const RUNT_COST: Cost = 0.5;
- const CONSECUTIVE_DASH_COST: Cost = 0.3;
- const MAX_COST: Cost = 1_000_000.0;
- const MIN_RATIO: f64 = -1.0;
-
- // Dynamic programming table.
- let mut active = 0;
- let mut table = vec![Entry {
- pred: 0,
- total: 0.0,
- line: line(vt, p, 0..0, Breakpoint::Mandatory),
- }];
-
- let em = p.size;
- let mut lines = Vec::with_capacity(16);
- breakpoints(p, |end, breakpoint| {
- let k = table.len();
- let eof = end == p.bidi.text.len();
- let mut best: Option<Entry> = None;
-
- // Find the optimal predecessor.
- for (i, pred) in table.iter().enumerate().skip(active) {
- // Layout the line.
- let start = pred.line.end;
-
- let attempt = line(vt, p, start..end, breakpoint);
-
- // Determine how much the line's spaces would need to be stretched
- // to make it the desired width.
- let delta = width - attempt.width;
- // Determine how much stretch are permitted.
- let adjust = if delta >= Abs::zero() {
- attempt.stretchability()
- } else {
- attempt.shrinkability()
- };
- // Ideally, the ratio should between -1.0 and 1.0, but sometimes a value above 1.0
- // is possible, in which case the line is underfull.
- let mut ratio = delta / adjust;
- if ratio.is_nan() {
- // The line is not stretchable, but it just fits.
- // This often happens with monospace fonts and CJK texts.
- ratio = 0.0;
- }
- if ratio > 1.0 {
- // We should stretch the line above its stretchability. Now
- // calculate the extra amount. Also, don't divide by zero.
- let extra_stretch =
- (delta - adjust) / attempt.justifiables().max(1) as f64;
- // Normalize the amount by half Em size.
- ratio = 1.0 + extra_stretch / (em / 2.0);
- }
-
- // Determine the cost of the line.
- let min_ratio = if p.justify { MIN_RATIO } else { 0.0 };
- let mut cost = if ratio < min_ratio {
- // The line is overfull. This is the case if
- // - justification is on, but we'd need to shrink too much
- // - justification is off and the line just doesn't fit
- //
- // If this is the earliest breakpoint in the active set
- // (active == i), remove it from the active set. If there is an
- // earlier one (active < i), then the logically shorter line was
- // in fact longer (can happen with negative spacing) and we
- // can't trim the active set just yet.
- if active == i {
- active += 1;
- }
- MAX_COST
- } else if breakpoint == Breakpoint::Mandatory || eof {
- // This is a mandatory break and the line is not overfull, so
- // all breakpoints before this one become inactive since no line
- // can span above the mandatory break.
- active = k;
- // If ratio > 0, we need to stretch the line only when justify is needed.
- // If ratio < 0, we always need to shrink the line.
- if (ratio > 0.0 && attempt.justify) || ratio < 0.0 {
- ratio.powi(3).abs()
- } else {
- 0.0
- }
- } else {
- // Normal line with cost of |ratio^3|.
- ratio.powi(3).abs()
- };
-
- // Penalize runts.
- if k == i + 1 && eof {
- cost += RUNT_COST;
- }
-
- // Penalize hyphens.
- if breakpoint == Breakpoint::Hyphen {
- cost += HYPH_COST;
- }
-
- // In Knuth paper, cost = (1 + 100|r|^3 + p)^2 + a,
- // where r is the ratio, p=50 is the penalty, and a=3000 is consecutive the penalty.
- // We divide the whole formula by 10, resulting (0.01 + |r|^3 + p)^2 + a,
- // where p=0.5 and a=0.3
- cost = (0.01 + cost).powi(2);
-
- // Penalize two consecutive dashes (not necessarily hyphens) extra.
- if attempt.dash && pred.line.dash {
- cost += CONSECUTIVE_DASH_COST;
- }
-
- // The total cost of this line and its chain of predecessors.
- let total = pred.total + cost;
-
- // If this attempt is better than what we had before, take it!
- if best.as_ref().map_or(true, |best| best.total >= total) {
- best = Some(Entry { pred: i, total, line: attempt });
- }
- }
-
- table.push(best.unwrap());
- });
-
- // Retrace the best path.
- let mut idx = table.len() - 1;
- while idx != 0 {
- table.truncate(idx + 1);
- let entry = table.pop().unwrap();
- lines.push(entry.line);
- idx = entry.pred;
- }
-
- lines.reverse();
- lines
-}
-
-/// Create a line which spans the given range.
-fn line<'a>(
- vt: &Vt,
- p: &'a Preparation,
- mut range: Range,
- breakpoint: Breakpoint,
-) -> Line<'a> {
- let end = range.end;
- let mut justify =
- p.justify && end < p.bidi.text.len() && breakpoint != Breakpoint::Mandatory;
-
- if range.is_empty() {
- return Line {
- bidi: &p.bidi,
- end,
- trimmed: range,
- first: None,
- inner: &[],
- last: None,
- width: Abs::zero(),
- justify,
- dash: false,
- };
- }
-
- // Slice out the relevant items.
- let (expanded, mut inner) = p.slice(range.clone());
- let mut width = Abs::zero();
-
- // Reshape the last item if it's split in half or hyphenated.
- let mut last = None;
- let mut dash = false;
- if let Some((Item::Text(shaped), before)) = inner.split_last() {
- // Compute the range we want to shape, trimming whitespace at the
- // end of the line.
- let base = expanded.end - shaped.text.len();
- let start = range.start.max(base);
- let text = &p.bidi.text[start..range.end];
- // U+200B ZERO WIDTH SPACE is used to provide a line break opportunity,
- // we want to trim it too.
- let trimmed = text.trim_end().trim_end_matches('\u{200B}');
- range.end = start + trimmed.len();
-
- // Deal with hyphens, dashes and justification.
- let shy = trimmed.ends_with('\u{ad}');
- let hyphen = breakpoint == Breakpoint::Hyphen;
- dash = hyphen || shy || trimmed.ends_with(['-', '–', '—']);
- justify |= text.ends_with('\u{2028}');
-
- // Deal with CJK punctuation at line ends.
- let gb_style = is_gb_style(shaped.lang, shaped.region);
- let maybe_adjust_last_glyph = trimmed.ends_with(END_PUNCT_PAT)
- || (p.cjk_latin_spacing && trimmed.ends_with(char_is_cjk_script));
-
- // Usually, we don't want to shape an empty string because:
- // - We don't want the height of trimmed whitespace in a different
- // font to be considered for the line height.
- // - Even if it's in the same font, its unnecessary.
- //
- // There is one exception though. When the whole line is empty, we
- // need the shaped empty string to make the line the appropriate
- // height. That is the case exactly if the string is empty and there
- // are no other items in the line.
- if hyphen || start + shaped.text.len() > range.end || maybe_adjust_last_glyph {
- if hyphen || start < range.end || before.is_empty() {
- let mut reshaped = shaped.reshape(vt, &p.spans, start..range.end);
- if hyphen || shy {
- reshaped.push_hyphen(vt, p.fallback);
- }
-
- if let Some(last_glyph) = reshaped.glyphs.last() {
- if last_glyph.is_cjk_left_aligned_punctuation(gb_style) {
- // If the last glyph is a CJK punctuation, we want to shrink it.
- // See Requirements for Chinese Text Layout, Section 3.1.6.3
- // Compression of punctuation marks at line start or line end
- let shrink_amount = last_glyph.shrinkability().1;
- let punct = reshaped.glyphs.to_mut().last_mut().unwrap();
- punct.shrink_right(shrink_amount);
- reshaped.width -= shrink_amount.at(reshaped.size);
- } else if p.cjk_latin_spacing
- && last_glyph.is_cjk_script()
- && (last_glyph.x_advance - last_glyph.x_offset) > Em::one()
- {
- // If the last glyph is a CJK character adjusted by [`add_cjk_latin_spacing`],
- // restore the original width.
- let shrink_amount =
- last_glyph.x_advance - last_glyph.x_offset - Em::one();
- let glyph = reshaped.glyphs.to_mut().last_mut().unwrap();
- glyph.x_advance -= shrink_amount;
- glyph.adjustability.shrinkability.1 = Em::zero();
- reshaped.width -= shrink_amount.at(reshaped.size);
- }
- }
-
- width += reshaped.width;
- last = Some(Item::Text(reshaped));
- }
-
- inner = before;
- }
- }
-
- // Deal with CJK characters at line starts.
- let text = &p.bidi.text[range.start..end];
- let maybe_adjust_first_glyph = text.starts_with(BEGIN_PUNCT_PAT)
- || (p.cjk_latin_spacing && text.starts_with(char_is_cjk_script));
-
- // Reshape the start item if it's split in half.
- let mut first = None;
- if let Some((Item::Text(shaped), after)) = inner.split_first() {
- // Compute the range we want to shape.
- let base = expanded.start;
- let end = range.end.min(base + shaped.text.len());
-
- // Reshape if necessary.
- if range.start + shaped.text.len() > end || maybe_adjust_first_glyph {
- // If the range is empty, we don't want to push an empty text item.
- if range.start < end {
- let reshaped = shaped.reshape(vt, &p.spans, range.start..end);
- width += reshaped.width;
- first = Some(Item::Text(reshaped));
- }
-
- inner = after;
- }
- }
-
- if maybe_adjust_first_glyph {
- let reshaped = first.as_mut().or(last.as_mut()).and_then(Item::text_mut);
- if let Some(reshaped) = reshaped {
- if let Some(first_glyph) = reshaped.glyphs.first() {
- if first_glyph.is_cjk_right_aligned_punctuation() {
- // If the first glyph is a CJK punctuation, we want to shrink it.
- let shrink_amount = first_glyph.shrinkability().0;
- let glyph = reshaped.glyphs.to_mut().first_mut().unwrap();
- glyph.shrink_left(shrink_amount);
- let amount_abs = shrink_amount.at(reshaped.size);
- reshaped.width -= amount_abs;
- width -= amount_abs;
- } else if p.cjk_latin_spacing
- && first_glyph.is_cjk_script()
- && first_glyph.x_offset > Em::zero()
- {
- // If the first glyph is a CJK character adjusted by [`add_cjk_latin_spacing`],
- // restore the original width.
- let shrink_amount = first_glyph.x_offset;
- let glyph = reshaped.glyphs.to_mut().first_mut().unwrap();
- glyph.x_advance -= shrink_amount;
- glyph.x_offset = Em::zero();
- glyph.adjustability.shrinkability.0 = Em::zero();
- let amount_abs = shrink_amount.at(reshaped.size);
- reshaped.width -= amount_abs;
- width -= amount_abs;
- }
- }
- }
- }
-
- // Measure the inner items.
- for item in inner {
- width += item.width();
- }
-
- Line {
- bidi: &p.bidi,
- trimmed: range,
- end,
- first,
- inner,
- last,
- width,
- justify,
- dash,
- }
-}
-
-/// Combine layouted lines into one frame per region.
-fn finalize(
- vt: &mut Vt,
- p: &Preparation,
- lines: &[Line],
- region: Size,
- expand: bool,
-) -> SourceResult<Fragment> {
- // Determine the paragraph's width: Full width of the region if we
- // should expand or there's fractional spacing, fit-to-width otherwise.
- let width = if !region.x.is_finite()
- || (!expand && lines.iter().all(|line| line.fr().is_zero()))
- {
- region
- .x
- .min(p.hang + lines.iter().map(|line| line.width).max().unwrap_or_default())
- } else {
- region.x
- };
-
- // Stack the lines into one frame per region.
- let mut frames: Vec<Frame> = lines
- .iter()
- .map(|line| commit(vt, p, line, width, region.y))
- .collect::<SourceResult<_>>()?;
-
- // Prevent orphans.
- if frames.len() >= 2 && !frames[1].is_empty() {
- let second = frames.remove(1);
- let first = &mut frames[0];
- merge(first, second, p.leading);
- }
-
- // Prevent widows.
- let len = frames.len();
- if len >= 2 && !frames[len - 2].is_empty() {
- let second = frames.pop().unwrap();
- let first = frames.last_mut().unwrap();
- merge(first, second, p.leading);
- }
-
- Ok(Fragment::frames(frames))
-}
-
-/// Merge two line frames
-fn merge(first: &mut Frame, second: Frame, leading: Abs) {
- let offset = first.height() + leading;
- let total = offset + second.height();
- first.push_frame(Point::with_y(offset), second);
- first.size_mut().y = total;
-}
-
-/// Commit to a line and build its frame.
-fn commit(
- vt: &mut Vt,
- p: &Preparation,
- line: &Line,
- width: Abs,
- full: Abs,
-) -> SourceResult<Frame> {
- let mut remaining = width - line.width - p.hang;
- let mut offset = Abs::zero();
-
- // Reorder the line from logical to visual order.
- let (reordered, starts_rtl) = reorder(line);
- if !starts_rtl {
- offset += p.hang;
- }
-
- // Handle hanging punctuation to the left.
- if let Some(Item::Text(text)) = reordered.first() {
- if let Some(glyph) = text.glyphs.first() {
- if !text.dir.is_positive()
- && TextElem::overhang_in(text.styles)
- && (reordered.len() > 1 || text.glyphs.len() > 1)
- {
- let amount = overhang(glyph.c) * glyph.x_advance.at(text.size);
- offset -= amount;
- remaining += amount;
- }
- }
- }
-
- // Handle hanging punctuation to the right.
- if let Some(Item::Text(text)) = reordered.last() {
- if let Some(glyph) = text.glyphs.last() {
- if text.dir.is_positive()
- && TextElem::overhang_in(text.styles)
- && (reordered.len() > 1 || text.glyphs.len() > 1)
- {
- let amount = overhang(glyph.c) * glyph.x_advance.at(text.size);
- remaining += amount;
- }
- }
- }
-
- // Determine how much additional space is needed.
- // The justicication_ratio is for the first step justification,
- // extra_justification is for the last step.
- // For more info on multi-step justification, see Procedures for Inter-
- // Character Space Expansion in W3C document Chinese Layout Requirements.
- let fr = line.fr();
- let mut justification_ratio = 0.0;
- let mut extra_justification = Abs::zero();
-
- let shrink = line.shrinkability();
- let stretch = line.stretchability();
- if remaining < Abs::zero() && shrink > Abs::zero() {
- // Attempt to reduce the length of the line, using shrinkability.
- justification_ratio = (remaining / shrink).max(-1.0);
- remaining = (remaining + shrink).min(Abs::zero());
- } else if line.justify && fr.is_zero() {
- // Attempt to increase the length of the line, using stretchability.
- if stretch > Abs::zero() {
- justification_ratio = (remaining / stretch).min(1.0);
- remaining = (remaining - stretch).max(Abs::zero());
- }
-
- let justifiables = line.justifiables();
- if justifiables > 0 && remaining > Abs::zero() {
- // Underfull line, distribute the extra space.
- extra_justification = remaining / justifiables as f64;
- remaining = Abs::zero();
- }
- }
-
- let mut top = Abs::zero();
- let mut bottom = Abs::zero();
-
- // Build the frames and determine the height and baseline.
- let mut frames = vec![];
- for item in reordered {
- let mut push = |offset: &mut Abs, frame: Frame| {
- let width = frame.width();
- top.set_max(frame.baseline());
- bottom.set_max(frame.size().y - frame.baseline());
- frames.push((*offset, frame));
- *offset += width;
- };
-
- match item {
- Item::Absolute(v) => {
- offset += *v;
- }
- Item::Fractional(v, elem) => {
- let amount = v.share(fr, remaining);
- if let Some((elem, styles)) = elem {
- let region = Size::new(amount, full);
- let pod = Regions::one(region, Axes::new(true, false));
- let mut frame = elem.layout(vt, *styles, pod)?.into_frame();
- frame.translate(Point::with_y(TextElem::baseline_in(*styles)));
- push(&mut offset, frame);
- } else {
- offset += amount;
- }
- }
- Item::Text(shaped) => {
- let frame = shaped.build(vt, justification_ratio, extra_justification);
- push(&mut offset, frame);
- }
- Item::Frame(frame) | Item::Meta(frame) => {
- push(&mut offset, frame.clone());
- }
- }
- }
-
- // Remaining space is distributed now.
- if !fr.is_zero() {
- remaining = Abs::zero();
- }
-
- let size = Size::new(width, top + bottom);
- let mut output = Frame::soft(size);
- output.set_baseline(top);
-
- // Construct the line's frame.
- for (offset, frame) in frames {
- let x = offset + p.align.position(remaining);
- let y = top - frame.baseline();
- output.push_frame(Point::new(x, y), frame);
- }
-
- Ok(output)
-}
-
-/// Return a line's items in visual order.
-fn reorder<'a>(line: &'a Line<'a>) -> (Vec<&Item<'a>>, bool) {
- let mut reordered = vec![];
-
- // The bidi crate doesn't like empty lines.
- if line.trimmed.is_empty() {
- return (line.slice(line.trimmed.clone()).collect(), false);
- }
-
- // Find the paragraph that contains the line.
- let para = line
- .bidi
- .paragraphs
- .iter()
- .find(|para| para.range.contains(&line.trimmed.start))
- .unwrap();
-
- // Compute the reordered ranges in visual order (left to right).
- let (levels, runs) = line.bidi.visual_runs(para, line.trimmed.clone());
- let starts_rtl = levels.first().map_or(false, |level| level.is_rtl());
-
- // Collect the reordered items.
- for run in runs {
- // Skip reset L1 runs because handling them would require reshaping
- // again in some cases.
- if line.bidi.levels[run.start] != levels[run.start] {
- continue;
- }
-
- let prev = reordered.len();
- reordered.extend(line.slice(run.clone()));
-
- if levels[run.start].is_rtl() {
- reordered[prev..].reverse();
- }
- }
-
- (reordered, starts_rtl)
-}
-
-/// How much a character should hang into the end margin.
-///
-/// For more discussion, see:
-/// https://recoveringphysicist.com/21/
-fn overhang(c: char) -> f64 {
- match c {
- // Dashes.
- '–' | '—' => 0.2,
- '-' => 0.55,
-
- // Punctuation.
- '.' | ',' => 0.8,
- ':' | ';' => 0.3,
-
- // Arabic
- '\u{60C}' | '\u{6D4}' => 0.4,
-
- _ => 0.0,
- }
-}
diff --git a/crates/typst-library/src/layout/place.rs b/crates/typst-library/src/layout/place.rs
deleted file mode 100644
index c8e83383..00000000
--- a/crates/typst-library/src/layout/place.rs
+++ /dev/null
@@ -1,124 +0,0 @@
-use crate::prelude::*;
-
-/// Places content at an absolute position.
-///
-/// Placed content will not affect the position of other content. Place is
-/// always relative to its parent container and will be in the foreground of all
-/// other content in the container. Page margins will be respected.
-///
-///
-/// # Example
-/// ```example
-/// #set page(height: 60pt)
-/// Hello, world!
-///
-/// #place(
-/// top + right,
-/// square(
-/// width: 20pt,
-/// stroke: 2pt + blue
-/// ),
-/// )
-/// ```
-#[elem(Layout, Behave)]
-pub struct PlaceElem {
- /// Relative to which position in the parent container to place the content.
- ///
- /// Cannot be `{auto}` if `float` is `{false}` and must be either
- /// `{auto}`, `{top}`, or `{bottom}` if `float` is `{true}`.
- ///
- /// When an axis of the page is `{auto}` sized, all alignments relative to
- /// that axis will be ignored, instead, the item will be placed in the
- /// origin of the axis.
- #[positional]
- #[default(Smart::Custom(Align::START))]
- pub alignment: Smart<Align>,
-
- /// Whether the placed element has floating layout.
- ///
- /// Floating elements are positioned at the top or bottom of the page,
- /// displacing in-flow content.
- ///
- /// ```example
- /// #set page(height: 150pt)
- /// #let note(where, body) = place(
- /// center + where,
- /// float: true,
- /// clearance: 6pt,
- /// rect(body),
- /// )
- ///
- /// #lorem(10)
- /// #note(bottom)[Bottom 1]
- /// #note(bottom)[Bottom 2]
- /// #lorem(40)
- /// #note(top)[Top]
- /// #lorem(10)
- /// ```
- pub float: bool,
-
- /// The amount of clearance the placed element has in a floating layout.
- #[default(Em::new(1.5).into())]
- #[resolve]
- pub clearance: Length,
-
- /// The horizontal displacement of the placed content.
- ///
- /// ```example
- /// #set page(height: 100pt)
- /// #for i in range(16) {
- /// let amount = i * 4pt
- /// place(center, dx: amount - 32pt, dy: amount)[A]
- /// }
- /// ```
- pub dx: Rel<Length>,
-
- /// The vertical displacement of the placed content.
- pub dy: Rel<Length>,
-
- /// The content to place.
- #[required]
- pub body: Content,
-}
-
-impl Layout for PlaceElem {
- #[tracing::instrument(name = "PlaceElem::layout", skip_all)]
- fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- // The pod is the base area of the region because for absolute
- // placement we don't really care about the already used area.
- let base = regions.base();
- let float = self.float(styles);
- let alignment = self.alignment(styles);
-
- if float
- && alignment
- .map_or(false, |align| matches!(align.y(), None | Some(VAlign::Horizon)))
- {
- bail!(self.span(), "floating placement must be `auto`, `top`, or `bottom`");
- } else if !float && alignment.is_auto() {
- return Err("automatic positioning is only available for floating placement")
- .hint("you can enable floating placement with `place(float: true, ..)`")
- .at(self.span());
- }
-
- let child = self
- .body()
- .clone()
- .aligned(alignment.unwrap_or_else(|| Align::CENTER));
-
- let pod = Regions::one(base, Axes::splat(false));
- let frame = child.layout(vt, styles, pod)?.into_frame();
- Ok(Fragment::frame(frame))
- }
-}
-
-impl Behave for PlaceElem {
- fn behaviour(&self) -> Behaviour {
- Behaviour::Ignorant
- }
-}
diff --git a/crates/typst-library/src/layout/regions.rs b/crates/typst-library/src/layout/regions.rs
deleted file mode 100644
index 6dd549b0..00000000
--- a/crates/typst-library/src/layout/regions.rs
+++ /dev/null
@@ -1,144 +0,0 @@
-use std::fmt::{self, Debug, Formatter};
-
-use typst::geom::{Abs, Axes, Size};
-
-/// A sequence of regions to layout into.
-#[derive(Copy, Clone, Hash)]
-pub struct Regions<'a> {
- /// The remaining size of the first region.
- pub size: Size,
- /// The full height of the region for relative sizing.
- pub full: Abs,
- /// The height of followup regions. The width is the same for all regions.
- pub backlog: &'a [Abs],
- /// The height of the final region that is repeated once the backlog is
- /// drained. The width is the same for all regions.
- pub last: Option<Abs>,
- /// Whether elements should expand to fill the regions instead of shrinking
- /// to fit the content.
- pub expand: Axes<bool>,
- /// Whether these are the root regions or direct descendants.
- ///
- /// True for the padded page regions and columns directly in the page,
- /// false otherwise.
- pub root: bool,
-}
-
-impl Regions<'_> {
- /// Create a new region sequence with exactly one region.
- pub fn one(size: Size, expand: Axes<bool>) -> Self {
- Self {
- size,
- full: size.y,
- backlog: &[],
- last: None,
- expand,
- root: false,
- }
- }
-
- /// Create a new sequence of same-size regions that repeats indefinitely.
- pub fn repeat(size: Size, expand: Axes<bool>) -> Self {
- Self {
- size,
- full: size.y,
- backlog: &[],
- last: Some(size.y),
- expand,
- root: false,
- }
- }
-
- /// The base size, which doesn't take into account that the regions is
- /// already partially used up.
- ///
- /// This is also used for relative sizing.
- pub fn base(&self) -> Size {
- Size::new(self.size.x, self.full)
- }
-
- /// Create new regions where all sizes are mapped with `f`.
- ///
- /// Note that since all regions must have the same width, the width returned
- /// by `f` is ignored for the backlog and the final region.
- pub fn map<'v, F>(&self, backlog: &'v mut Vec<Abs>, mut f: F) -> Regions<'v>
- where
- F: FnMut(Size) -> Size,
- {
- let x = self.size.x;
- backlog.clear();
- backlog.extend(self.backlog.iter().map(|&y| f(Size::new(x, y)).y));
- Regions {
- size: f(self.size),
- full: f(Size::new(x, self.full)).y,
- backlog,
- last: self.last.map(|y| f(Size::new(x, y)).y),
- expand: self.expand,
- root: false,
- }
- }
-
- /// Whether the first region is full and a region break is called for.
- pub fn is_full(&self) -> bool {
- Abs::zero().fits(self.size.y) && !self.in_last()
- }
-
- /// Whether the first region is the last usable region.
- ///
- /// If this is true, calling `next()` will have no effect.
- pub fn in_last(&self) -> bool {
- self.backlog.is_empty() && self.last.map_or(true, |height| self.size.y == height)
- }
-
- /// The same regions, but with different `root` configuration.
- pub fn with_root(self, root: bool) -> Self {
- Self { root, ..self }
- }
-
- /// Advance to the next region if there is any.
- pub fn next(&mut self) {
- if let Some(height) = self
- .backlog
- .split_first()
- .map(|(first, tail)| {
- self.backlog = tail;
- *first
- })
- .or(self.last)
- {
- self.size.y = height;
- self.full = height;
- }
- }
-
- /// An iterator that returns the sizes of the first and all following
- /// regions, equivalently to what would be produced by calling
- /// [`next()`](Self::next) repeatedly until all regions are exhausted.
- /// This iterator may be infinite.
- pub fn iter(&self) -> impl Iterator<Item = Size> + '_ {
- let first = std::iter::once(self.size);
- let backlog = self.backlog.iter();
- let last = self.last.iter().cycle();
- first.chain(backlog.chain(last).map(|&h| Size::new(self.size.x, h)))
- }
-}
-
-impl Debug for Regions<'_> {
- fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
- f.write_str("Regions ")?;
- let mut list = f.debug_list();
- let mut prev = self.size.y;
- list.entry(&self.size);
- for &height in self.backlog {
- list.entry(&Size::new(self.size.x, height));
- prev = height;
- }
- if let Some(last) = self.last {
- if last != prev {
- list.entry(&Size::new(self.size.x, last));
- }
- list.entry(&(..));
- }
- list.finish()
- }
-}
diff --git a/crates/typst-library/src/layout/repeat.rs b/crates/typst-library/src/layout/repeat.rs
deleted file mode 100644
index ce31164e..00000000
--- a/crates/typst-library/src/layout/repeat.rs
+++ /dev/null
@@ -1,75 +0,0 @@
-use crate::layout::AlignElem;
-use crate::prelude::*;
-
-/// Repeats content to the available space.
-///
-/// This can be useful when implementing a custom index, reference, or outline.
-///
-/// Space may be inserted between the instances of the body parameter, so be
-/// sure to include negative space if you need the instances to overlap.
-///
-/// Errors if there no bounds on the available space, as it would create
-/// infinite content.
-///
-/// # Example
-/// ```example
-/// Sign on the dotted line:
-/// #box(width: 1fr, repeat[.])
-///
-/// #set text(10pt)
-/// #v(8pt, weak: true)
-/// #align(right)[
-/// Berlin, the 22nd of December, 2022
-/// ]
-/// ```
-#[elem(Layout)]
-pub struct RepeatElem {
- /// The content to repeat.
- #[required]
- pub body: Content,
-}
-
-impl Layout for RepeatElem {
- #[tracing::instrument(name = "RepeatElem::layout", skip_all)]
- fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- let pod = Regions::one(regions.size, Axes::new(false, false));
- let piece = self.body().layout(vt, styles, pod)?.into_frame();
- let align = AlignElem::alignment_in(styles).resolve(styles);
-
- let fill = regions.size.x;
- let width = piece.width();
- let count = (fill / width).floor();
- let remaining = fill % width;
- let apart = remaining / (count - 1.0);
-
- let size = Size::new(regions.size.x, piece.height());
-
- if !size.is_finite() {
- bail!(self.span(), "repeat with no size restrictions");
- }
-
- let mut frame = Frame::soft(size);
- if piece.has_baseline() {
- frame.set_baseline(piece.baseline());
- }
-
- let mut offset = Abs::zero();
- if count == 1.0 {
- offset += align.x.position(remaining);
- }
-
- if width > Abs::zero() {
- for _ in 0..(count as usize).min(1000) {
- frame.push_frame(Point::with_x(offset), piece.clone());
- offset += piece.width() + apart;
- }
- }
-
- Ok(Fragment::frame(frame))
- }
-}
diff --git a/crates/typst-library/src/layout/spacing.rs b/crates/typst-library/src/layout/spacing.rs
deleted file mode 100644
index 88b6e2cd..00000000
--- a/crates/typst-library/src/layout/spacing.rs
+++ /dev/null
@@ -1,262 +0,0 @@
-use std::borrow::Cow;
-
-use crate::prelude::*;
-
-/// Inserts horizontal spacing into a paragraph.
-///
-/// The spacing can be absolute, relative, or fractional. In the last case, the
-/// remaining space on the line is distributed among all fractional spacings
-/// according to their relative fractions.
-///
-/// # Example
-/// ```example
-/// First #h(1cm) Second \
-/// First #h(30%) Second \
-/// First #h(2fr) Second #h(1fr) Third
-/// ```
-///
-/// # Mathematical Spacing { #math-spacing }
-/// In [mathematical formulas]($category/math), you can additionally use these
-/// constants to add spacing between elements: `thin`, `med`, `thick`, `quad`, `wide`.
-#[elem(title = "Spacing (H)", Behave)]
-pub struct HElem {
- /// How much spacing to insert.
- #[required]
- pub amount: Spacing,
-
- /// If `{true}`, the spacing collapses at the start or end of a paragraph.
- /// Moreover, from multiple adjacent weak spacings all but the largest one
- /// collapse.
- ///
- /// Weak spacing in markup also causes all adjacent markup spaces to be
- /// removed, regardless of the amount of spacing inserted. To force a space
- /// next to weak spacing, you can explicitly write `[#" "]` (for a normal
- /// space) or `[~]` (for a non-breaking space). The latter can be useful to
- /// create a construct that always attaches to the preceding word with one
- /// non-breaking space, independently of whether a markup space existed in
- /// front or not.
- ///
- /// ```example
- /// #h(1cm, weak: true)
- /// We identified a group of _weak_
- /// specimens that fail to manifest
- /// in most cases. However, when
- /// #h(8pt, weak: true) supported
- /// #h(8pt, weak: true) on both sides,
- /// they do show up.
- ///
- /// Further #h(0pt, weak: true) more,
- /// even the smallest of them swallow
- /// adjacent markup spaces.
- /// ```
- #[default(false)]
- pub weak: bool,
-}
-
-impl HElem {
- /// Zero-width horizontal weak spacing that eats surrounding spaces.
- pub fn hole() -> Self {
- Self::new(Abs::zero().into()).with_weak(true)
- }
-}
-
-impl Behave for HElem {
- fn behaviour(&self) -> Behaviour {
- if self.amount().is_fractional() {
- Behaviour::Destructive
- } else if self.weak(StyleChain::default()) {
- Behaviour::Weak(1)
- } else {
- Behaviour::Invisible
- }
- }
-
- fn larger(
- &self,
- prev: &(Cow<Content>, Behaviour, StyleChain),
- styles: StyleChain,
- ) -> bool {
- let Some(other) = prev.0.to::<Self>() else { return false };
- match (self.amount(), other.amount()) {
- (Spacing::Fr(this), Spacing::Fr(other)) => this > other,
- (Spacing::Rel(this), Spacing::Rel(other)) => {
- this.resolve(styles) > other.resolve(prev.2)
- }
- _ => false,
- }
- }
-}
-
-/// Inserts vertical spacing into a flow of blocks.
-///
-/// The spacing can be absolute, relative, or fractional. In the last case,
-/// the remaining space on the page is distributed among all fractional spacings
-/// according to their relative fractions.
-///
-/// # Example
-/// ```example
-/// #grid(
-/// rows: 3cm,
-/// columns: 6,
-/// gutter: 1fr,
-/// [A #parbreak() B],
-/// [A #v(0pt) B],
-/// [A #v(10pt) B],
-/// [A #v(0pt, weak: true) B],
-/// [A #v(40%, weak: true) B],
-/// [A #v(1fr) B],
-/// )
-/// ```
-#[elem(title = "Spacing (V)", Behave)]
-pub struct VElem {
- /// How much spacing to insert.
- #[required]
- pub amount: Spacing,
-
- /// If `{true}`, the spacing collapses at the start or end of a flow.
- /// Moreover, from multiple adjacent weak spacings all but the largest one
- /// collapse. Weak spacings will always collapse adjacent paragraph spacing,
- /// even if the paragraph spacing is larger.
- ///
- /// ```example
- /// The following theorem is
- /// foundational to the field:
- /// #v(4pt, weak: true)
- /// $ x^2 + y^2 = r^2 $
- /// #v(4pt, weak: true)
- /// The proof is simple:
- /// ```
- #[external]
- pub weak: bool,
-
- /// The element's weakness level, see also [`Behaviour`].
- #[internal]
- #[parse(args.named("weak")?.map(|v: bool| v as usize))]
- pub weakness: usize,
-}
-
-impl VElem {
- /// Normal strong spacing.
- pub fn strong(amount: Spacing) -> Self {
- Self::new(amount).with_weakness(0)
- }
-
- /// User-created weak spacing.
- pub fn weak(amount: Spacing) -> Self {
- Self::new(amount).with_weakness(1)
- }
-
- /// Weak spacing with list attach weakness.
- pub fn list_attach(amount: Spacing) -> Self {
- Self::new(amount).with_weakness(2)
- }
-
- /// Weak spacing with BlockElem::ABOVE/BELOW weakness.
- pub fn block_around(amount: Spacing) -> Self {
- Self::new(amount).with_weakness(3)
- }
-
- /// Weak spacing with BlockElem::SPACING weakness.
- pub fn block_spacing(amount: Spacing) -> Self {
- Self::new(amount).with_weakness(4)
- }
-}
-
-impl Behave for VElem {
- fn behaviour(&self) -> Behaviour {
- if self.amount().is_fractional() {
- Behaviour::Destructive
- } else if self.weakness(StyleChain::default()) > 0 {
- Behaviour::Weak(self.weakness(StyleChain::default()))
- } else {
- Behaviour::Invisible
- }
- }
-
- fn larger(
- &self,
- prev: &(Cow<Content>, Behaviour, StyleChain),
- styles: StyleChain,
- ) -> bool {
- let Some(other) = prev.0.to::<Self>() else { return false };
- match (self.amount(), other.amount()) {
- (Spacing::Fr(this), Spacing::Fr(other)) => this > other,
- (Spacing::Rel(this), Spacing::Rel(other)) => {
- this.resolve(styles) > other.resolve(prev.2)
- }
- _ => false,
- }
- }
-}
-
-cast! {
- VElem,
- v: Content => v.to::<Self>().cloned().ok_or("expected `v` element")?,
-}
-
-/// Kinds of spacing.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub enum Spacing {
- /// Spacing specified in absolute terms and relative to the parent's size.
- Rel(Rel<Length>),
- /// Spacing specified as a fraction of the remaining free space in the
- /// parent.
- Fr(Fr),
-}
-
-impl Spacing {
- /// Whether this is fractional spacing.
- pub fn is_fractional(self) -> bool {
- matches!(self, Self::Fr(_))
- }
-
- /// Whether the spacing is actually no spacing.
- pub fn is_zero(&self) -> bool {
- match self {
- Self::Rel(rel) => rel.is_zero(),
- Self::Fr(fr) => fr.is_zero(),
- }
- }
-}
-
-impl From<Abs> for Spacing {
- fn from(abs: Abs) -> Self {
- Self::Rel(abs.into())
- }
-}
-
-impl From<Em> for Spacing {
- fn from(em: Em) -> Self {
- Self::Rel(Rel::new(Ratio::zero(), em.into()))
- }
-}
-
-impl From<Length> for Spacing {
- fn from(length: Length) -> Self {
- Self::Rel(length.into())
- }
-}
-
-impl From<Fr> for Spacing {
- fn from(fr: Fr) -> Self {
- Self::Fr(fr)
- }
-}
-
-cast! {
- Spacing,
- self => match self {
- Self::Rel(rel) => {
- if rel.rel.is_zero() {
- rel.abs.into_value()
- } else if rel.abs.is_zero() {
- rel.rel.into_value()
- } else {
- rel.into_value()
- }
- }
- Self::Fr(fr) => fr.into_value(),
- },
- v: Rel<Length> => Self::Rel(v),
- v: Fr => Self::Fr(v),
-}
diff --git a/crates/typst-library/src/layout/stack.rs b/crates/typst-library/src/layout/stack.rs
deleted file mode 100644
index c12d2048..00000000
--- a/crates/typst-library/src/layout/stack.rs
+++ /dev/null
@@ -1,345 +0,0 @@
-use crate::layout::{AlignElem, Spacing};
-use crate::prelude::*;
-
-/// Arranges content and spacing horizontally or vertically.
-///
-/// The stack places a list of items along an axis, with optional spacing
-/// between each item.
-///
-/// # Example
-/// ```example
-/// #stack(
-/// dir: ttb,
-/// rect(width: 40pt),
-/// rect(width: 120pt),
-/// rect(width: 90pt),
-/// )
-/// ```
-#[elem(Layout)]
-pub struct StackElem {
- /// The direction along which the items are stacked. Possible values are:
- ///
- /// - `{ltr}`: Left to right.
- /// - `{rtl}`: Right to left.
- /// - `{ttb}`: Top to bottom.
- /// - `{btt}`: Bottom to top.
- ///
- /// You can use the `start` and `end` methods to obtain the initial and
- /// final points (respectively) of a direction, as `alignment`. You can also
- /// use the `axis` method to determine whether a direction is
- /// `{"horizontal"}` or `{"vertical"}`. The `inv` method returns a
- /// direction's inverse direction.
- ///
- /// For example, `{ttb.start()}` is `top`, `{ttb.end()}` is `bottom`,
- /// `{ttb.axis()}` is `{"vertical"}` and `{ttb.inv()}` is equal to `btt`.
- #[default(Dir::TTB)]
- pub dir: Dir,
-
- /// Spacing to insert between items where no explicit spacing was provided.
- pub spacing: Option<Spacing>,
-
- /// The children to stack along the axis.
- #[variadic]
- pub children: Vec<StackChild>,
-}
-
-impl Layout for StackElem {
- #[tracing::instrument(name = "StackElem::layout", skip_all)]
- fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- let mut layouter = StackLayouter::new(self.dir(styles), regions, styles);
-
- // Spacing to insert before the next block.
- let spacing = self.spacing(styles);
- let mut deferred = None;
-
- for child in self.children() {
- match child {
- StackChild::Spacing(kind) => {
- layouter.layout_spacing(*kind);
- deferred = None;
- }
- StackChild::Block(block) => {
- if let Some(kind) = deferred {
- layouter.layout_spacing(kind);
- }
-
- layouter.layout_block(vt, block, styles)?;
- deferred = spacing;
- }
- }
- }
-
- Ok(layouter.finish())
- }
-}
-
-/// A child of a stack element.
-#[derive(Clone, PartialEq, Hash)]
-pub enum StackChild {
- /// Spacing between other children.
- Spacing(Spacing),
- /// Arbitrary block-level content.
- Block(Content),
-}
-
-impl Debug for StackChild {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- match self {
- Self::Spacing(kind) => kind.fmt(f),
- Self::Block(block) => block.fmt(f),
- }
- }
-}
-
-cast! {
- StackChild,
- self => match self {
- Self::Spacing(spacing) => spacing.into_value(),
- Self::Block(content) => content.into_value(),
- },
- v: Spacing => Self::Spacing(v),
- v: Content => Self::Block(v),
-}
-
-/// Performs stack layout.
-struct StackLayouter<'a> {
- /// The stacking direction.
- dir: Dir,
- /// The axis of the stacking direction.
- axis: Axis,
- /// The regions to layout children into.
- regions: Regions<'a>,
- /// The inherited styles.
- styles: StyleChain<'a>,
- /// Whether the stack itself should expand to fill the region.
- expand: Axes<bool>,
- /// The initial size of the current region before we started subtracting.
- initial: Size,
- /// The generic size used by the frames for the current region.
- used: Gen<Abs>,
- /// The sum of fractions in the current region.
- fr: Fr,
- /// Already layouted items whose exact positions are not yet known due to
- /// fractional spacing.
- items: Vec<StackItem>,
- /// Finished frames for previous regions.
- finished: Vec<Frame>,
-}
-
-/// A prepared item in a stack layout.
-enum StackItem {
- /// Absolute spacing between other items.
- Absolute(Abs),
- /// Fractional spacing between other items.
- Fractional(Fr),
- /// A frame for a layouted block.
- Frame(Frame, Axes<FixedAlign>),
-}
-
-impl<'a> StackLayouter<'a> {
- /// Create a new stack layouter.
- fn new(dir: Dir, mut regions: Regions<'a>, styles: StyleChain<'a>) -> Self {
- let axis = dir.axis();
- let expand = regions.expand;
-
- // Disable expansion along the block axis for children.
- regions.expand.set(axis, false);
-
- Self {
- dir,
- axis,
- regions,
- styles,
- expand,
- initial: regions.size,
- used: Gen::zero(),
- fr: Fr::zero(),
- items: vec![],
- finished: vec![],
- }
- }
-
- /// Add spacing along the spacing direction.
- #[tracing::instrument(name = "StackLayouter::layout_spacing", skip_all)]
- fn layout_spacing(&mut self, spacing: Spacing) {
- match spacing {
- Spacing::Rel(v) => {
- // Resolve the spacing and limit it to the remaining space.
- let resolved = v
- .resolve(self.styles)
- .relative_to(self.regions.base().get(self.axis));
- let remaining = self.regions.size.get_mut(self.axis);
- let limited = resolved.min(*remaining);
- if self.dir.axis() == Axis::Y {
- *remaining -= limited;
- }
- self.used.main += limited;
- self.items.push(StackItem::Absolute(resolved));
- }
- Spacing::Fr(v) => {
- self.fr += v;
- self.items.push(StackItem::Fractional(v));
- }
- }
- }
-
- /// Layout an arbitrary block.
- #[tracing::instrument(name = "StackLayouter::layout_block", skip_all)]
- fn layout_block(
- &mut self,
- vt: &mut Vt,
- block: &Content,
- styles: StyleChain,
- ) -> SourceResult<()> {
- if self.regions.is_full() {
- self.finish_region();
- }
-
- // Block-axis alignment of the `AlignElement` is respected by stacks.
- let align = if let Some(align) = block.to::<AlignElem>() {
- align.alignment(styles)
- } else if let Some((_, local)) = block.to_styled() {
- AlignElem::alignment_in(styles.chain(local))
- } else {
- AlignElem::alignment_in(styles)
- }
- .resolve(styles);
-
- let fragment = block.layout(vt, styles, self.regions)?;
- let len = fragment.len();
- for (i, frame) in fragment.into_iter().enumerate() {
- // Grow our size, shrink the region and save the frame for later.
- let size = frame.size();
- if self.dir.axis() == Axis::Y {
- self.regions.size.y -= size.y;
- }
-
- let gen = match self.axis {
- Axis::X => Gen::new(size.y, size.x),
- Axis::Y => Gen::new(size.x, size.y),
- };
-
- self.used.main += gen.main;
- self.used.cross.set_max(gen.cross);
-
- self.items.push(StackItem::Frame(frame, align));
-
- if i + 1 < len {
- self.finish_region();
- }
- }
-
- Ok(())
- }
-
- /// Advance to the next region.
- fn finish_region(&mut self) {
- // Determine the size of the stack in this region depending on whether
- // the region expands.
- let mut size = self
- .expand
- .select(self.initial, self.used.into_axes(self.axis))
- .min(self.initial);
-
- // Expand fully if there are fr spacings.
- let full = self.initial.get(self.axis);
- let remaining = full - self.used.main;
- if self.fr.get() > 0.0 && full.is_finite() {
- self.used.main = full;
- size.set(self.axis, full);
- }
-
- let mut output = Frame::hard(size);
- let mut cursor = Abs::zero();
- let mut ruler: FixedAlign = self.dir.start().into();
-
- // Place all frames.
- for item in self.items.drain(..) {
- match item {
- StackItem::Absolute(v) => cursor += v,
- StackItem::Fractional(v) => cursor += v.share(self.fr, remaining),
- StackItem::Frame(frame, align) => {
- if self.dir.is_positive() {
- ruler = ruler.max(align.get(self.axis));
- } else {
- ruler = ruler.min(align.get(self.axis));
- }
-
- // Align along the main axis.
- let parent = size.get(self.axis);
- let child = frame.size().get(self.axis);
- let main = ruler.position(parent - self.used.main)
- + if self.dir.is_positive() {
- cursor
- } else {
- self.used.main - child - cursor
- };
-
- // Align along the cross axis.
- let other = self.axis.other();
- let cross = align
- .get(other)
- .position(size.get(other) - frame.size().get(other));
-
- let pos = Gen::new(cross, main).to_point(self.axis);
- cursor += child;
- output.push_frame(pos, frame);
- }
- }
- }
-
- // Advance to the next region.
- self.regions.next();
- self.initial = self.regions.size;
- self.used = Gen::zero();
- self.fr = Fr::zero();
- self.finished.push(output);
- }
-
- /// Finish layouting and return the resulting frames.
- fn finish(mut self) -> Fragment {
- self.finish_region();
- Fragment::frames(self.finished)
- }
-}
-
-/// A container with a main and cross component.
-#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)]
-struct Gen<T> {
- /// The main component.
- pub cross: T,
- /// The cross component.
- pub main: T,
-}
-
-impl<T> Gen<T> {
- /// Create a new instance from the two components.
- const fn new(cross: T, main: T) -> Self {
- Self { cross, main }
- }
-
- /// Convert to the specific representation, given the current main axis.
- fn into_axes(self, main: Axis) -> Axes<T> {
- match main {
- Axis::X => Axes::new(self.main, self.cross),
- Axis::Y => Axes::new(self.cross, self.main),
- }
- }
-}
-
-impl Gen<Abs> {
- /// The zero value.
- fn zero() -> Self {
- Self { cross: Abs::zero(), main: Abs::zero() }
- }
-
- /// Convert to a point.
- fn to_point(self, main: Axis) -> Point {
- self.into_axes(main).to_point()
- }
-}
diff --git a/crates/typst-library/src/layout/table.rs b/crates/typst-library/src/layout/table.rs
deleted file mode 100644
index 0eff7a44..00000000
--- a/crates/typst-library/src/layout/table.rs
+++ /dev/null
@@ -1,361 +0,0 @@
-use typst::eval::{CastInfo, Reflect};
-
-use crate::layout::{AlignElem, GridLayouter, TrackSizings};
-use crate::meta::Figurable;
-use crate::prelude::*;
-
-/// A table of items.
-///
-/// Tables are used to arrange content in cells. Cells can contain arbitrary
-/// content, including multiple paragraphs and are specified in row-major order.
-/// Because tables are just grids with configurable cell properties, refer to
-/// the [grid documentation]($grid) for more information on how to size the
-/// table tracks.
-///
-/// To give a table a caption and make it [referenceable]($ref), put it into a
-/// [figure]($figure).
-///
-/// # Example
-/// ```example
-/// #table(
-/// columns: (1fr, auto, auto),
-/// inset: 10pt,
-/// align: horizon,
-/// [], [*Area*], [*Parameters*],
-/// image("cylinder.svg"),
-/// $ pi h (D^2 - d^2) / 4 $,
-/// [
-/// $h$: height \
-/// $D$: outer radius \
-/// $d$: inner radius
-/// ],
-/// image("tetrahedron.svg"),
-/// $ sqrt(2) / 12 a^3 $,
-/// [$a$: edge length]
-/// )
-/// ```
-#[elem(Layout, LocalName, Figurable)]
-pub struct TableElem {
- /// The column sizes. See the [grid documentation]($grid) for more
- /// information on track sizing.
- #[borrowed]
- pub columns: TrackSizings,
-
- /// The row sizes. See the [grid documentation]($grid) for more information
- /// on track sizing.
- #[borrowed]
- pub rows: TrackSizings,
-
- /// The gaps between rows & columns. See the [grid documentation]($grid) for
- /// more information on gutters.
- #[external]
- pub gutter: TrackSizings,
-
- /// The gaps between columns. Takes precedence over `gutter`. See the
- /// [grid documentation]($grid) for more information on gutters.
- #[borrowed]
- #[parse(
- let gutter = args.named("gutter")?;
- args.named("column-gutter")?.or_else(|| gutter.clone())
- )]
- pub column_gutter: TrackSizings,
-
- /// The gaps between rows. Takes precedence over `gutter`. See the
- /// [grid documentation]($grid) for more information on gutters.
- #[parse(args.named("row-gutter")?.or_else(|| gutter.clone()))]
- #[borrowed]
- pub row_gutter: TrackSizings,
-
- /// How to fill the cells.
- ///
- /// This can be a color or a function that returns a color. The function is
- /// passed the cells' column and row index, starting at zero. This can be
- /// used to implement striped tables.
- ///
- /// ```example
- /// #table(
- /// fill: (col, _) => if calc.odd(col) { luma(240) } else { white },
- /// align: (col, row) =>
- /// if row == 0 { center }
- /// else if col == 0 { left }
- /// else { right },
- /// columns: 4,
- /// [], [*Q1*], [*Q2*], [*Q3*],
- /// [Revenue:], [1000 €], [2000 €], [3000 €],
- /// [Expenses:], [500 €], [1000 €], [1500 €],
- /// [Profit:], [500 €], [1000 €], [1500 €],
- /// )
- /// ```
- #[borrowed]
- pub fill: Celled<Option<Paint>>,
-
- /// How to align the cells' content.
- ///
- /// This can either be a single alignment, an array of alignments
- /// (corresponding to each column) or a function that returns an alignment.
- /// The function is passed the cells' column and row index, starting at zero.
- /// If set to `{auto}`, the outer alignment is used.
- ///
- /// ```example
- /// #table(
- /// columns: 3,
- /// align: (x, y) => (left, center, right).at(x),
- /// [Hello], [Hello], [Hello],
- /// [A], [B], [C],
- /// )
- /// ```
- #[borrowed]
- pub align: Celled<Smart<Align>>,
-
- /// How to [stroke]($stroke) the cells.
- ///
- /// Strokes can be disabled by setting this to `{none}`.
- ///
- /// _Note:_ Richer stroke customization for individual cells is not yet
- /// implemented, but will be in the future. In the meantime, you can use the
- /// third-party [tablex library](https://github.com/PgBiel/typst-tablex/).
- #[resolve]
- #[fold]
- #[default(Some(Stroke::default()))]
- pub stroke: Option<Stroke>,
-
- /// How much to pad the cells' content.
- ///
- /// ```example
- /// #table(
- /// inset: 10pt,
- /// [Hello],
- /// [World],
- /// )
- ///
- /// #table(
- /// columns: 2,
- /// inset: (
- /// x: 20pt,
- /// y: 10pt,
- /// ),
- /// [Hello],
- /// [World],
- /// )
- /// ```
- #[fold]
- #[default(Sides::splat(Abs::pt(5.0).into()))]
- pub inset: Sides<Option<Rel<Length>>>,
-
- /// The contents of the table cells.
- #[variadic]
- pub children: Vec<Content>,
-}
-
-impl Layout for TableElem {
- #[tracing::instrument(name = "TableElem::layout", skip_all)]
- fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- let inset = self.inset(styles);
- let align = self.align(styles);
- let columns = self.columns(styles);
- let rows = self.rows(styles);
- let column_gutter = self.column_gutter(styles);
- let row_gutter = self.row_gutter(styles);
-
- let tracks = Axes::new(columns.0.as_slice(), rows.0.as_slice());
- let gutter = Axes::new(column_gutter.0.as_slice(), row_gutter.0.as_slice());
- let cols = tracks.x.len().max(1);
- let cells: Vec<_> = self
- .children()
- .iter()
- .enumerate()
- .map(|(i, child)| {
- let mut child = child.clone().padded(inset);
-
- let x = i % cols;
- let y = i / cols;
- if let Smart::Custom(alignment) = align.resolve(vt, x, y)? {
- child = child.styled(AlignElem::set_alignment(alignment));
- }
-
- Ok(child)
- })
- .collect::<SourceResult<_>>()?;
-
- let fill = self.fill(styles);
- let stroke = self.stroke(styles).map(Stroke::unwrap_or_default);
-
- // Prepare grid layout by unifying content and gutter tracks.
- let layouter =
- GridLayouter::new(tracks, gutter, &cells, regions, styles, self.span());
-
- // Measure the columns and layout the grid row-by-row.
- let mut layout = layouter.layout(vt)?;
-
- // Add lines and backgrounds.
- for (frame, rows) in layout.fragment.iter_mut().zip(&layout.rows) {
- if layout.cols.is_empty() || rows.is_empty() {
- continue;
- }
-
- // Render table lines.
- if let Some(stroke) = &stroke {
- let thickness = stroke.thickness;
- let half = thickness / 2.0;
-
- // Render horizontal lines.
- for offset in points(rows.iter().map(|piece| piece.height)) {
- let target = Point::with_x(frame.width() + thickness);
- let hline = Geometry::Line(target).stroked(stroke.clone());
- frame.prepend(
- Point::new(-half, offset),
- FrameItem::Shape(hline, self.span()),
- );
- }
-
- // Render vertical lines.
- for offset in points(layout.cols.iter().copied()) {
- let target = Point::with_y(frame.height() + thickness);
- let vline = Geometry::Line(target).stroked(stroke.clone());
- frame.prepend(
- Point::new(offset, -half),
- FrameItem::Shape(vline, self.span()),
- );
- }
- }
-
- // Render cell backgrounds.
- let mut dx = Abs::zero();
- for (x, &col) in layout.cols.iter().enumerate() {
- let mut dy = Abs::zero();
- for row in rows {
- if let Some(fill) = fill.resolve(vt, x, row.y)? {
- let pos = Point::new(dx, dy);
- let size = Size::new(col, row.height);
- let rect = Geometry::Rect(size).filled(fill);
- frame.prepend(pos, FrameItem::Shape(rect, self.span()));
- }
- dy += row.height;
- }
- dx += col;
- }
- }
-
- Ok(layout.fragment)
- }
-}
-
-/// Turn an iterator of extents into an iterator of offsets before, in between,
-/// and after the extents, e.g. [10mm, 5mm] -> [0mm, 10mm, 15mm].
-fn points(extents: impl IntoIterator<Item = Abs>) -> impl Iterator<Item = Abs> {
- let mut offset = Abs::zero();
- std::iter::once(Abs::zero()).chain(extents).map(move |extent| {
- offset += extent;
- offset
- })
-}
-
-/// A value that can be configured per cell.
-#[derive(Debug, Clone, PartialEq, Hash)]
-pub enum Celled<T> {
- /// A bare value, the same for all cells.
- Value(T),
- /// A closure mapping from cell coordinates to a value.
- Func(Func),
- /// An array of alignment values corresponding to each column.
- Array(Vec<T>),
-}
-
-impl<T: Default + Clone + FromValue> Celled<T> {
- /// Resolve the value based on the cell position.
- pub fn resolve(&self, vt: &mut Vt, x: usize, y: usize) -> SourceResult<T> {
- Ok(match self {
- Self::Value(value) => value.clone(),
- Self::Func(func) => func.call_vt(vt, [x, y])?.cast().at(func.span())?,
- Self::Array(array) => x
- .checked_rem(array.len())
- .and_then(|i| array.get(i))
- .cloned()
- .unwrap_or_default(),
- })
- }
-}
-
-impl<T: Default> Default for Celled<T> {
- fn default() -> Self {
- Self::Value(T::default())
- }
-}
-
-impl<T: Reflect> Reflect for Celled<T> {
- fn input() -> CastInfo {
- T::input() + Array::input() + Func::input()
- }
-
- fn output() -> CastInfo {
- T::output() + Array::output() + Func::output()
- }
-
- fn castable(value: &Value) -> bool {
- Array::castable(value) || Func::castable(value) || T::castable(value)
- }
-}
-
-impl<T: IntoValue> IntoValue for Celled<T> {
- fn into_value(self) -> Value {
- match self {
- Self::Value(value) => value.into_value(),
- Self::Func(func) => func.into_value(),
- Self::Array(arr) => arr.into_value(),
- }
- }
-}
-
-impl<T: FromValue> FromValue for Celled<T> {
- fn from_value(value: Value) -> StrResult<Self> {
- match value {
- Value::Func(v) => Ok(Self::Func(v)),
- Value::Array(array) => Ok(Self::Array(
- array.into_iter().map(T::from_value).collect::<StrResult<_>>()?,
- )),
- v if T::castable(&v) => Ok(Self::Value(T::from_value(v)?)),
- v => Err(Self::error(&v)),
- }
- }
-}
-
-impl LocalName for TableElem {
- fn local_name(lang: Lang, _: Option<Region>) -> &'static str {
- match lang {
- Lang::ALBANIAN => "Tabel",
- Lang::ARABIC => "جدول",
- Lang::BOKMÅL => "Tabell",
- Lang::CHINESE => "表",
- Lang::CZECH => "Tabulka",
- Lang::DANISH => "Tabel",
- Lang::DUTCH => "Tabel",
- Lang::FILIPINO => "Talaan",
- Lang::FINNISH => "Taulukko",
- Lang::FRENCH => "Tableau",
- Lang::GERMAN => "Tabelle",
- Lang::GREEK => "Πίνακας",
- Lang::HUNGARIAN => "Táblázat",
- Lang::ITALIAN => "Tabella",
- Lang::NYNORSK => "Tabell",
- Lang::POLISH => "Tabela",
- Lang::PORTUGUESE => "Tabela",
- Lang::ROMANIAN => "Tabelul",
- Lang::RUSSIAN => "Таблица",
- Lang::SLOVENIAN => "Tabela",
- Lang::SPANISH => "Tabla",
- Lang::SWEDISH => "Tabell",
- Lang::TURKISH => "Tablo",
- Lang::UKRAINIAN => "Таблиця",
- Lang::VIETNAMESE => "Bảng",
- Lang::JAPANESE => "表",
- Lang::ENGLISH | _ => "Table",
- }
- }
-}
-
-impl Figurable for TableElem {}
diff --git a/crates/typst-library/src/layout/terms.rs b/crates/typst-library/src/layout/terms.rs
deleted file mode 100644
index d373768d..00000000
--- a/crates/typst-library/src/layout/terms.rs
+++ /dev/null
@@ -1,162 +0,0 @@
-use crate::layout::{BlockElem, HElem, ParElem, Spacing, VElem};
-use crate::prelude::*;
-
-/// A list of terms and their descriptions.
-///
-/// Displays a sequence of terms and their descriptions vertically. When the
-/// descriptions span over multiple lines, they use hanging indent to
-/// communicate the visual hierarchy.
-///
-/// # Example
-/// ```example
-/// / Ligature: A merged glyph.
-/// / Kerning: A spacing adjustment
-/// between two adjacent letters.
-/// ```
-///
-/// # Syntax
-/// This function also has dedicated syntax: Starting a line with a slash,
-/// followed by a term, a colon and a description creates a term list item.
-#[elem(scope, title = "Term List", Layout)]
-pub struct TermsElem {
- /// If this is `{false}`, the items are spaced apart with
- /// [term list spacing]($terms.spacing). If it is `{true}`, they use normal
- /// [leading]($par.leading) instead. This makes the term list more compact,
- /// which can look better if the items are short.
- ///
- /// In markup mode, the value of this parameter is determined based on
- /// whether items are separated with a blank line. If items directly follow
- /// each other, this is set to `{true}`; if items are separated by a blank
- /// line, this is set to `{false}`.
- ///
- /// ```example
- /// / Fact: If a term list has a lot
- /// of text, and maybe other inline
- /// content, it should not be tight
- /// anymore.
- ///
- /// / Tip: To make it wide, simply
- /// insert a blank line between the
- /// items.
- /// ```
- #[default(true)]
- pub tight: bool,
-
- /// The separator between the item and the description.
- ///
- /// If you want to just separate them with a certain amount of space, use
- /// `{h(2cm, weak: true)}` as the separator and replace `{2cm}` with your
- /// desired amount of space.
- ///
- /// ```example
- /// #set terms(separator: [: ])
- ///
- /// / Colon: A nice separator symbol.
- /// ```
- #[default(HElem::new(Em::new(0.6).into()).with_weak(true).pack())]
- #[borrowed]
- pub separator: Content,
-
- /// The indentation of each item.
- pub indent: Length,
-
- /// The hanging indent of the description.
- ///
- /// This is in addition to the whole item's `indent`.
- ///
- /// ```example
- /// #set terms(hanging-indent: 0pt)
- /// / Term: This term list does not
- /// make use of hanging indents.
- /// ```
- #[default(Em::new(2.0).into())]
- pub hanging_indent: Length,
-
- /// The spacing between the items of a wide (non-tight) term list.
- ///
- /// If set to `{auto}`, uses the spacing [below blocks]($block.below).
- pub spacing: Smart<Spacing>,
-
- /// The term list's children.
- ///
- /// When using the term list syntax, adjacent items are automatically
- /// collected into term lists, even through constructs like for loops.
- ///
- /// ```example
- /// #for (year, product) in (
- /// "1978": "TeX",
- /// "1984": "LaTeX",
- /// "2019": "Typst",
- /// ) [/ #product: Born in #year.]
- /// ```
- #[variadic]
- pub children: Vec<TermItem>,
-}
-
-#[scope]
-impl TermsElem {
- #[elem]
- type TermItem;
-}
-
-impl Layout for TermsElem {
- #[tracing::instrument(name = "TermsElem::layout", skip_all)]
- fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- let separator = self.separator(styles);
- let indent = self.indent(styles);
- let hanging_indent = self.hanging_indent(styles);
- let gutter = if self.tight(styles) {
- ParElem::leading_in(styles).into()
- } else {
- self.spacing(styles)
- .unwrap_or_else(|| *BlockElem::below_in(styles).amount())
- };
-
- let mut seq = vec![];
- for (i, child) in self.children().iter().enumerate() {
- if i > 0 {
- seq.push(VElem::new(gutter).with_weakness(1).pack());
- }
- if !indent.is_zero() {
- seq.push(HElem::new(indent.into()).pack());
- }
- seq.push(child.term().clone().strong());
- seq.push((*separator).clone());
- seq.push(child.description().clone());
- }
-
- Content::sequence(seq)
- .styled(ParElem::set_hanging_indent(hanging_indent + indent))
- .layout(vt, styles, regions)
- }
-}
-
-/// A term list item.
-#[elem(name = "item", title = "Term List Item")]
-pub struct TermItem {
- /// The term described by the list item.
- #[required]
- pub term: Content,
-
- /// The description of the term.
- #[required]
- pub description: Content,
-}
-
-cast! {
- TermItem,
- array: Array => {
- let mut iter = array.into_iter();
- let (term, description) = match (iter.next(), iter.next(), iter.next()) {
- (Some(a), Some(b), None) => (a.cast()?, b.cast()?),
- _ => bail!("array must contain exactly two entries"),
- };
- Self::new(term, description)
- },
- v: Content => v.to::<Self>().cloned().ok_or("expected term item or array")?,
-}
diff --git a/crates/typst-library/src/layout/transform.rs b/crates/typst-library/src/layout/transform.rs
deleted file mode 100644
index 012a146d..00000000
--- a/crates/typst-library/src/layout/transform.rs
+++ /dev/null
@@ -1,187 +0,0 @@
-use typst::geom::Transform;
-
-use crate::prelude::*;
-
-/// Moves content without affecting layout.
-///
-/// The `move` function allows you to move content while the layout still 'sees'
-/// it at the original positions. Containers will still be sized as if the
-/// content was not moved.
-///
-/// # Example
-/// ```example
-/// #rect(inset: 0pt, move(
-/// dx: 6pt, dy: 6pt,
-/// rect(
-/// inset: 8pt,
-/// fill: white,
-/// stroke: black,
-/// [Abra cadabra]
-/// )
-/// ))
-/// ```
-#[elem(Layout)]
-pub struct MoveElem {
- /// The horizontal displacement of the content.
- pub dx: Rel<Length>,
-
- /// The vertical displacement of the content.
- pub dy: Rel<Length>,
-
- /// The content to move.
- #[required]
- pub body: Content,
-}
-
-impl Layout for MoveElem {
- #[tracing::instrument(name = "MoveElem::layout", skip_all)]
- fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- let pod = Regions::one(regions.base(), Axes::splat(false));
- let mut frame = self.body().layout(vt, styles, pod)?.into_frame();
- let delta = Axes::new(self.dx(styles), self.dy(styles)).resolve(styles);
- let delta = delta.zip_map(regions.base(), Rel::relative_to);
- frame.translate(delta.to_point());
- Ok(Fragment::frame(frame))
- }
-}
-
-/// Rotates content without affecting layout.
-///
-/// Rotates an element by a given angle. The layout will act as if the element
-/// was not rotated.
-///
-/// # Example
-/// ```example
-/// #stack(
-/// dir: ltr,
-/// spacing: 1fr,
-/// ..range(16)
-/// .map(i => rotate(24deg * i)[X]),
-/// )
-/// ```
-#[elem(Layout)]
-pub struct RotateElem {
- /// The amount of rotation.
- ///
- /// ```example
- /// #rotate(-1.571rad)[Space!]
- /// ```
- ///
- #[positional]
- pub angle: Angle,
-
- /// The origin of the rotation.
- ///
- /// If, for instance, you wanted the bottom left corner of the rotated
- /// element to stay aligned with the baseline, you would set it to `bottom +
- /// left` instead.
- ///
- /// ```example
- /// #set text(spacing: 8pt)
- /// #let square = square.with(width: 8pt)
- ///
- /// #box(square())
- /// #box(rotate(30deg, origin: center, square()))
- /// #box(rotate(30deg, origin: top + left, square()))
- /// #box(rotate(30deg, origin: bottom + right, square()))
- /// ```
- #[fold]
- #[default(HAlign::Center + VAlign::Horizon)]
- pub origin: Align,
-
- /// The content to rotate.
- #[required]
- pub body: Content,
-}
-
-impl Layout for RotateElem {
- #[tracing::instrument(name = "RotateElem::layout", skip_all)]
- fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- let pod = Regions::one(regions.base(), Axes::splat(false));
- let mut frame = self.body().layout(vt, styles, pod)?.into_frame();
- let Axes { x, y } = self
- .origin(styles)
- .resolve(styles)
- .zip_map(frame.size(), FixedAlign::position);
- let ts = Transform::translate(x, y)
- .pre_concat(Transform::rotate(self.angle(styles)))
- .pre_concat(Transform::translate(-x, -y));
- frame.transform(ts);
- Ok(Fragment::frame(frame))
- }
-}
-
-/// Scales content without affecting layout.
-///
-/// Lets you mirror content by specifying a negative scale on a single axis.
-///
-/// # Example
-/// ```example
-/// #set align(center)
-/// #scale(x: -100%)[This is mirrored.]
-/// ```
-#[elem(Layout)]
-pub struct ScaleElem {
- /// The horizontal scaling factor.
- ///
- /// The body will be mirrored horizontally if the parameter is negative.
- #[parse(
- let all = args.find()?;
- args.named("x")?.or(all)
- )]
- #[default(Ratio::one())]
- pub x: Ratio,
-
- /// The vertical scaling factor.
- ///
- /// The body will be mirrored vertically if the parameter is negative.
- #[parse(args.named("y")?.or(all))]
- #[default(Ratio::one())]
- pub y: Ratio,
-
- /// The origin of the transformation.
- ///
- /// ```example
- /// A#box(scale(75%)[A])A \
- /// B#box(scale(75%, origin: bottom + left)[B])B
- /// ```
- #[fold]
- #[default(HAlign::Center + VAlign::Horizon)]
- pub origin: Align,
-
- /// The content to scale.
- #[required]
- pub body: Content,
-}
-
-impl Layout for ScaleElem {
- #[tracing::instrument(name = "ScaleElem::layout", skip_all)]
- fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- let pod = Regions::one(regions.base(), Axes::splat(false));
- let mut frame = self.body().layout(vt, styles, pod)?.into_frame();
- let Axes { x, y } = self
- .origin(styles)
- .resolve(styles)
- .zip_map(frame.size(), FixedAlign::position);
- let transform = Transform::translate(x, y)
- .pre_concat(Transform::scale(self.x(styles), self.y(styles)))
- .pre_concat(Transform::translate(-x, -y));
- frame.transform(transform);
- Ok(Fragment::frame(frame))
- }
-}