summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--crates/typst/src/introspection/counter.rs11
-rw-r--r--crates/typst/src/layout/columns.rs52
-rw-r--r--crates/typst/src/layout/container.rs390
-rw-r--r--crates/typst/src/layout/flow/collect.rs639
-rw-r--r--crates/typst/src/layout/flow/compose.rs843
-rw-r--r--crates/typst/src/layout/flow/distribute.rs512
-rw-r--r--crates/typst/src/layout/flow/mod.rs1265
-rw-r--r--crates/typst/src/layout/frame.rs6
-rw-r--r--crates/typst/src/layout/inline/line.rs11
-rw-r--r--crates/typst/src/layout/pages/mod.rs2
-rw-r--r--crates/typst/src/layout/pages/run.rs13
-rw-r--r--crates/typst/src/layout/place.rs27
-rw-r--r--crates/typst/src/layout/regions.rs13
-rw-r--r--crates/typst/src/math/fragment.rs5
-rw-r--r--crates/typst/src/model/figure.rs13
-rw-r--r--crates/typst/src/model/footnote.rs3
-rw-r--r--crates/typst/src/model/par.rs6
-rw-r--r--crates/typst/src/visualize/image/mod.rs6
-rw-r--r--crates/typst/src/visualize/shape.rs14
-rw-r--r--docs/guides/page-setup.md61
-rw-r--r--tests/ref/block-fr-height-auto-width.pngbin0 -> 1069 bytes
-rw-r--r--tests/ref/block-fr-height-first-child.pngbin0 -> 149 bytes
-rw-r--r--tests/ref/block-fr-height-multiple.pngbin0 -> 152 bytes
-rw-r--r--tests/ref/block-fr-height.pngbin0 -> 167 bytes
-rw-r--r--tests/ref/block-sticky-alone.pngbin0 -> 166 bytes
-rw-r--r--tests/ref/block-sticky-colbreak.pngbin0 -> 283 bytes
-rw-r--r--tests/ref/block-sticky-many.pngbin0 -> 570 bytes
-rw-r--r--tests/ref/block-sticky.pngbin0 -> 469 bytes
-rw-r--r--tests/ref/box-fr-width.png (renamed from tests/ref/box-width-fr.png)bin464 -> 464 bytes
-rw-r--r--tests/ref/colbreak-weak.pngbin0 -> 197 bytes
-rw-r--r--tests/ref/figure-placement.pngbin0 -> 2302 bytes
-rw-r--r--tests/ref/footnote-block-at-end.pngbin0 -> 620 bytes
-rw-r--r--tests/ref/footnote-break-across-pages-block.pngbin0 -> 1296 bytes
-rw-r--r--tests/ref/footnote-break-across-pages-float.pngbin0 -> 1399 bytes
-rw-r--r--tests/ref/footnote-break-across-pages-nested.pngbin0 -> 1320 bytes
-rw-r--r--tests/ref/footnote-break-across-pages.pngbin5424 -> 5425 bytes
-rw-r--r--tests/ref/footnote-float-priority.pngbin0 -> 1393 bytes
-rw-r--r--tests/ref/footnote-in-list.pngbin0 -> 2513 bytes
-rw-r--r--tests/ref/footnote-in-place.pngbin0 -> 1127 bytes
-rw-r--r--tests/ref/footnote-in-table.pngbin12311 -> 12423 bytes
-rw-r--r--tests/ref/footnote-multiple-in-one-line.pngbin0 -> 693 bytes
-rw-r--r--tests/ref/footnote-nested-same-frame.pngbin743 -> 0 bytes
-rw-r--r--tests/ref/footnote-nested.pngbin2469 -> 2581 bytes
-rw-r--r--tests/ref/issue-2213-align-fr.pngbin0 -> 291 bytes
-rw-r--r--tests/ref/issue-3481-cite-location.pngbin602 -> 504 bytes
-rw-r--r--tests/ref/issue-3641-float-loop.pngbin704 -> 678 bytes
-rw-r--r--tests/ref/issue-3866-block-migration.pngbin0 -> 777 bytes
-rw-r--r--tests/ref/issue-footnotes-skip-first-page.pngbin571 -> 512 bytes
-rw-r--r--tests/ref/issue-multiple-footnote-in-one-line.pngbin704 -> 0 bytes
-rw-r--r--tests/ref/place-float-block-backlog.pngbin0 -> 826 bytes
-rw-r--r--tests/ref/place-float-clearance-empty.pngbin0 -> 1235 bytes
-rw-r--r--tests/ref/place-float-column-align-auto.pngbin0 -> 932 bytes
-rw-r--r--tests/ref/place-float-column-queued.png (renamed from tests/ref/place-float-in-column-queued.png)bin638 -> 638 bytes
-rw-r--r--tests/ref/place-float-counter.pngbin0 -> 674 bytes
-rw-r--r--tests/ref/place-float-delta.pngbin0 -> 317 bytes
-rw-r--r--tests/ref/place-float-flow-size-alone.pngbin0 -> 125 bytes
-rw-r--r--tests/ref/place-float-flow-size.pngbin0 -> 347 bytes
-rw-r--r--tests/ref/place-float-fr.pngbin0 -> 507 bytes
-rw-r--r--tests/ref/place-float-in-column-align-auto.pngbin843 -> 0 bytes
-rw-r--r--tests/ref/place-float-rel-sizing.pngbin0 -> 335 bytes
-rw-r--r--tests/ref/place-float-threecolumn-block-backlog.pngbin0 -> 707 bytes
-rw-r--r--tests/ref/place-float-threecolumn.pngbin0 -> 1286 bytes
-rw-r--r--tests/ref/place-float-twocolumn-align-auto.pngbin0 -> 719 bytes
-rw-r--r--tests/ref/place-float-twocolumn-fits-not.pngbin0 -> 1043 bytes
-rw-r--r--tests/ref/place-float-twocolumn-fits.pngbin0 -> 1001 bytes
-rw-r--r--tests/ref/place-float-twocolumn-queued.pngbin0 -> 862 bytes
-rw-r--r--tests/ref/place-float-twocolumn.pngbin0 -> 1259 bytes
-rw-r--r--tests/ref/query-running-header.pngbin9017 -> 9231 bytes
-rw-r--r--tests/skip.txt0
-rw-r--r--tests/src/collect.rs13
-rw-r--r--tests/src/tests.rs3
-rw-r--r--tests/suite/introspection/query.typ4
-rw-r--r--tests/suite/layout/align.typ8
-rw-r--r--tests/suite/layout/columns.typ7
-rw-r--r--tests/suite/layout/container.typ61
-rw-r--r--tests/suite/layout/flow/flow.typ13
-rw-r--r--tests/suite/layout/flow/footnote.typ (renamed from tests/suite/model/footnote.typ)114
-rw-r--r--tests/suite/layout/flow/place-float.typ83
-rw-r--r--tests/suite/layout/flow/place-flush.typ29
-rw-r--r--tests/suite/layout/flow/place.typ290
-rw-r--r--tests/suite/model/cite.typ3
-rw-r--r--tests/suite/model/figure.typ36
82 files changed, 2878 insertions, 1678 deletions
diff --git a/crates/typst/src/introspection/counter.rs b/crates/typst/src/introspection/counter.rs
index e7dd2ba0..ba126e18 100644
--- a/crates/typst/src/introspection/counter.rs
+++ b/crates/typst/src/introspection/counter.rs
@@ -304,7 +304,7 @@ impl Counter {
route: Route::extend(route).unnested(),
};
- let mut state = CounterState::init(&self.0);
+ let mut state = CounterState::init(matches!(self.0, CounterKey::Page));
let mut page = NonZeroUsize::ONE;
let mut stops = eco_vec![(state.clone(), page)];
@@ -656,12 +656,9 @@ pub struct CounterState(pub SmallVec<[usize; 3]>);
impl CounterState {
/// Get the initial counter state for the key.
- pub fn init(key: &CounterKey) -> Self {
- Self(match key {
- // special case, because pages always start at one.
- CounterKey::Page => smallvec![1],
- _ => smallvec![0],
- })
+ pub fn init(page: bool) -> Self {
+ // Special case, because pages always start at one.
+ Self(smallvec![usize::from(page)])
}
/// Advance the counter and return the numbers for the given heading.
diff --git a/crates/typst/src/layout/columns.rs b/crates/typst/src/layout/columns.rs
index b46351f8..5812e38a 100644
--- a/crates/typst/src/layout/columns.rs
+++ b/crates/typst/src/layout/columns.rs
@@ -10,34 +10,39 @@ use crate::layout::{
/// 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.
+/// The `column` function lets you separate the interior of any container into
+/// multiple columns. It will currently not balance the height of the columns.
+/// Instead, the columns will take up the height of their container or the
+/// remaining height on the page. Support for balanced columns is planned for
+/// the future.
///
-/// If you need to insert columns across your whole document, you can use the
-/// [`{page}` function's `columns` parameter]($page.columns) instead.
+/// # Page-level columns { #page-level }
+/// If you need to insert columns across your whole document, use the `{page}`
+/// function's [`columns` parameter]($page.columns) instead. This will create
+/// the columns directly at the page-level rather than wrapping all of your
+/// content in a layout container. As a result, things like
+/// [pagebreaks]($pagebreak), [footnotes]($footnote), and [line
+/// numbers]($par.line) will continue to work as expected. For more information,
+/// also read the [relevant part of the page setup
+/// guide]($guides/page-setup/#columns).
///
-/// # Example
-/// ```example
-/// = Towards Advanced Deep Learning
+/// # Breaking out of columns { #breaking-out }
+/// To temporarily break out of columns (e.g. for a paper's title), use
+/// page-scoped floating placement:
+///
+/// ```example:single
+/// #set page(columns: 2, height: 150pt)
///
-/// #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.
-/// ]
+/// #place(
+/// top + center,
+/// scope: "page",
+/// float: true,
+/// text(1.4em, weight: "bold")[
+/// My document
+/// ],
/// )
///
-/// In recent years, deep learning has
-/// increasingly been used to solve a
-/// variety of problems.
+/// #lorem(40)
/// ```
#[elem(Show)]
pub struct ColumnsElem {
@@ -59,7 +64,6 @@ pub struct ColumnsElem {
impl Show for Packed<ColumnsElem> {
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
Ok(BlockElem::multi_layouter(self.clone(), layout_columns)
- .with_rootable(true)
.pack()
.spanned(self.span()))
}
diff --git a/crates/typst/src/layout/container.rs b/crates/typst/src/layout/container.rs
index b5a8f7f8..2ff0f0fd 100644
--- a/crates/typst/src/layout/container.rs
+++ b/crates/typst/src/layout/container.rs
@@ -143,7 +143,7 @@ impl Packed<BoxElem> {
let inset = self.inset(styles).unwrap_or_default();
// Build the pod region.
- let pod = Self::pod(&width, &height, &inset, styles, region);
+ let pod = unbreakable_pod(&width, &height.into(), &inset, styles, region);
// Layout the body.
let mut frame = match self.body(styles) {
@@ -166,14 +166,6 @@ impl Packed<BoxElem> {
crate::layout::grow(&mut frame, &inset);
}
- // Apply baseline shift. Do this after setting the size and applying the
- // inset, so that a relative shift is resolved relative to the final
- // height.
- 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
@@ -196,49 +188,20 @@ impl Packed<BoxElem> {
frame.fill_and_stroke(fill, &stroke, &outset, &radius, self.span());
}
+ // Assign label to the frame.
if let Some(label) = self.label() {
frame.group(|group| group.label = Some(label))
}
- Ok(frame)
- }
-
- /// Builds the pod region for box layout.
- fn pod(
- width: &Sizing,
- height: &Smart<Rel>,
- inset: &Sides<Rel<Abs>>,
- styles: StyleChain,
- region: Size,
- ) -> Region {
- // Resolve the size.
- let mut size = Size::new(
- match width {
- // For auto, the whole region is available.
- Sizing::Auto => region.x,
- // Resolve the relative sizing.
- Sizing::Rel(rel) => rel.resolve(styles).relative_to(region.x),
- // Fr is handled outside and already factored into the `region`,
- // so we can treat it equivalently to 100%.
- Sizing::Fr(_) => region.x,
- },
- match height {
- // See above. Note that fr is not supported on this axis.
- Smart::Auto => region.y,
- Smart::Custom(rel) => rel.resolve(styles).relative_to(region.y),
- },
- );
-
- // Take the inset, if any, into account.
- if !inset.is_zero() {
- size = crate::layout::shrink(size, inset);
+ // Apply baseline shift. Do this after setting the size and applying the
+ // inset, so that a relative shift is resolved relative to the final
+ // height.
+ let shift = self.baseline(styles).relative_to(frame.height());
+ if !shift.is_zero() {
+ frame.set_baseline(frame.baseline() - shift);
}
- // If the child is not auto-sized, the size is forced and we should
- // enable expansion.
- let expand = Axes::new(*width != Sizing::Auto, *height != Smart::Auto);
-
- Region::new(size, expand)
+ Ok(frame)
}
}
@@ -355,7 +318,7 @@ pub struct BlockElem {
/// fill: aqua,
/// )
/// ```
- pub height: Smart<Rel<Length>>,
+ pub height: Sizing,
/// Whether the block can be broken and continue on the next page.
///
@@ -453,20 +416,16 @@ pub struct BlockElem {
#[default(false)]
pub clip: bool,
- /// Whether this block must stick to the following one.
+ /// Whether this block must stick to the following one, with no break in
+ /// between.
///
- /// Use this to prevent page breaks between e.g. a heading and its body.
- #[internal]
+ /// This is, by default, set on heading blocks to prevent orphaned headings
+ /// at the bottom of the page.
+ ///
+ /// Marking a block as sticky makes it unbreakable.
#[default(false)]
- #[parse(None)]
pub sticky: bool,
- /// Whether this block can host footnotes.
- #[internal]
- #[default(false)]
- #[parse(None)]
- pub rootable: bool,
-
/// The contents of the block.
#[positional]
#[borrowed]
@@ -513,9 +472,97 @@ impl BlockElem {
}
impl Packed<BlockElem> {
- /// Layout this block as part of a flow.
+ /// Lay this out as an unbreakable block.
#[typst_macros::time(name = "block", span = self.span())]
- pub fn layout(
+ pub fn layout_single(
+ &self,
+ engine: &mut Engine,
+ locator: Locator,
+ styles: StyleChain,
+ base: Size,
+ ) -> SourceResult<Frame> {
+ // Fetch sizing properties.
+ let width = self.width(styles);
+ let height = self.height(styles);
+ let inset = self.inset(styles).unwrap_or_default();
+
+ // Build the pod regions.
+ let pod = unbreakable_pod(&width.into(), &height, &inset, styles, base);
+
+ // Layout the body.
+ let body = self.body(styles);
+ let mut frame = match body {
+ // If we have no body, just create one frame. Its size will be
+ // adjusted below.
+ None => Frame::hard(Size::zero()),
+
+ // If we have content as our body, just layout it.
+ Some(BlockBody::Content(body)) => {
+ layout_frame(engine, body, locator.relayout(), styles, pod)?
+ }
+
+ // If we have a child that wants to layout with just access to the
+ // base region, give it that.
+ Some(BlockBody::SingleLayouter(callback)) => {
+ callback.call(engine, locator, styles, pod)?
+ }
+
+ // If we have a child that wants to layout with full region access,
+ // we layout it.
+ Some(BlockBody::MultiLayouter(callback)) => {
+ callback.call(engine, locator, styles, pod.into())?.into_frame()
+ }
+ };
+
+ // Explicit blocks are boundaries for gradient relativeness.
+ if matches!(body, None | Some(BlockBody::Content(_))) {
+ frame.set_kind(FrameKind::Hard);
+ }
+
+ // Enforce a correct frame size on the expanded axes. Do this before
+ // applying the inset, since the pod shrunk.
+ frame.set_size(pod.expand.select(pod.size, frame.size()));
+
+ // Apply the inset.
+ if !inset.is_zero() {
+ crate::layout::grow(&mut frame, &inset);
+ }
+
+ // Prepare fill and stroke.
+ let fill = self.fill(styles);
+ let stroke = self
+ .stroke(styles)
+ .unwrap_or_default()
+ .map(|s| s.map(Stroke::unwrap_or_default));
+
+ // Only fetch these if necessary (for clipping or filling/stroking).
+ let outset = Lazy::new(|| self.outset(styles).unwrap_or_default());
+ let radius = Lazy::new(|| self.radius(styles).unwrap_or_default());
+
+ // Clip the contents, if requested.
+ if self.clip(styles) {
+ let size = frame.size() + outset.relative_to(frame.size()).sum_by_axis();
+ frame.clip(clip_rect(size, &radius, &stroke));
+ }
+
+ // Add fill and/or stroke.
+ if fill.is_some() || stroke.iter().any(Option::is_some) {
+ frame.fill_and_stroke(fill, &stroke, &outset, &radius, self.span());
+ }
+
+ // Assign label to each frame in the fragment.
+ if let Some(label) = self.label() {
+ frame.group(|group| group.label = Some(label));
+ }
+
+ Ok(frame)
+ }
+}
+
+impl Packed<BlockElem> {
+ /// Lay this out as a breakable block.
+ #[typst_macros::time(name = "block", span = self.span())]
+ pub fn layout_multiple(
&self,
engine: &mut Engine,
locator: Locator,
@@ -526,14 +573,13 @@ impl Packed<BlockElem> {
let width = self.width(styles);
let height = self.height(styles);
let inset = self.inset(styles).unwrap_or_default();
- let breakable = self.breakable(styles);
// Allocate a small vector for backlogs.
let mut buf = SmallVec::<[Abs; 2]>::new();
// Build the pod regions.
let pod =
- Self::pod(&width, &height, &inset, breakable, styles, regions, &mut buf);
+ breakable_pod(&width.into(), &height, &inset, styles, regions, &mut buf);
// Layout the body.
let body = self.body(styles);
@@ -673,116 +719,6 @@ impl Packed<BlockElem> {
Ok(fragment)
}
-
- /// Builds the pod regions for block layout.
- ///
- /// If `breakable` is `false`, this will only ever return a single region.
- fn pod<'a>(
- width: &Smart<Rel>,
- height: &Smart<Rel>,
- inset: &Sides<Rel<Abs>>,
- breakable: bool,
- styles: StyleChain,
- regions: Regions,
- buf: &'a mut SmallVec<[Abs; 2]>,
- ) -> Regions<'a> {
- let base = regions.base();
-
- // The vertical region sizes we're about to build.
- let first;
- let full;
- let backlog: &mut [Abs];
- let last;
-
- // If the block has a fixed height, things are very different, so we
- // handle that case completely separately.
- match height {
- Smart::Auto => {
- if breakable {
- // If the block automatically sized and breakable, we can
- // just inherit the regions.
- first = regions.size.y;
- buf.extend_from_slice(regions.backlog);
- backlog = buf;
- last = regions.last;
- } else {
- // If the block is automatically sized, but not breakable,
- // we provide the full base height. It doesn't really make
- // sense to provide just the remaining height to an
- // unbreakable block.
- first = regions.full;
- backlog = &mut [];
- last = None;
- }
-
- // Since we're automatically sized, we inherit the base size.
- full = regions.full;
- }
-
- Smart::Custom(rel) => {
- // Resolve the sizing to a concrete size.
- let resolved = rel.resolve(styles).relative_to(base.y);
-
- if breakable {
- // If the block is fixed-height and breakable, distribute
- // the fixed height across a start region and a backlog.
- (first, backlog) = distribute(resolved, regions, buf);
- } else {
- // If the block is fixed-height, but not breakable, the
- // fixed height is all in the first region, and we have no
- // backlog.
- first = resolved;
- backlog = &mut [];
- }
-
- // Since we're manually sized, the resolved size is also the
- // base height.
- full = resolved;
-
- // If the height is manually sized, we don't want a final
- // repeatable region.
- last = None;
- }
- };
-
- // Resolve the horizontal sizing to a concrete width and combine
- // `width` and `first` into `size`.
- let mut size = Size::new(
- match width {
- Smart::Auto => regions.size.x,
- Smart::Custom(rel) => rel.resolve(styles).relative_to(base.x),
- },
- first,
- );
-
- // Take the inset, if any, into account, applying it to the
- // individual region components.
- let (mut full, mut last) = (full, last);
- if !inset.is_zero() {
- crate::layout::shrink_multiple(
- &mut size, &mut full, backlog, &mut last, inset,
- );
- }
-
- // If the child is manually sized along an axis (i.e. not `auto`), then
- // it should expand along that axis. We also ensure that we only expand
- // if the size is finite because it just doesn't make sense to expand
- // into infinite regions.
- let expand = Axes::new(*width != Smart::Auto, *height != Smart::Auto)
- & size.map(Abs::is_finite);
-
- Regions {
- size,
- full,
- backlog,
- last,
- expand,
- // This will only ever be set by the flow if the block is
- // `rootable`. It is important that we propagate this, so that
- // columns can hold footnotes.
- root: regions.root,
- }
- }
}
/// The contents of a block.
@@ -873,6 +809,118 @@ cast! {
v: Fr => Self::Fr(v),
}
+/// Builds the pod region for an unbreakable sized container.
+fn unbreakable_pod(
+ width: &Sizing,
+ height: &Sizing,
+ inset: &Sides<Rel<Abs>>,
+ styles: StyleChain,
+ base: Size,
+) -> Region {
+ // Resolve the size.
+ let mut size = Size::new(
+ match width {
+ // - For auto, the whole region is available.
+ // - Fr is handled outside and already factored into the `region`,
+ // so we can treat it equivalently to 100%.
+ Sizing::Auto | Sizing::Fr(_) => base.x,
+ // Resolve the relative sizing.
+ Sizing::Rel(rel) => rel.resolve(styles).relative_to(base.x),
+ },
+ match height {
+ Sizing::Auto | Sizing::Fr(_) => base.y,
+ Sizing::Rel(rel) => rel.resolve(styles).relative_to(base.y),
+ },
+ );
+
+ // Take the inset, if any, into account.
+ if !inset.is_zero() {
+ size = crate::layout::shrink(size, inset);
+ }
+
+ // If the child is manually, the size is forced and we should enable
+ // expansion.
+ let expand = Axes::new(
+ *width != Sizing::Auto && size.x.is_finite(),
+ *height != Sizing::Auto && size.y.is_finite(),
+ );
+
+ Region::new(size, expand)
+}
+
+/// Builds the pod regions for a breakable sized container.
+fn breakable_pod<'a>(
+ width: &Sizing,
+ height: &Sizing,
+ inset: &Sides<Rel<Abs>>,
+ styles: StyleChain,
+ regions: Regions,
+ buf: &'a mut SmallVec<[Abs; 2]>,
+) -> Regions<'a> {
+ let base = regions.base();
+
+ // The vertical region sizes we're about to build.
+ let first;
+ let full;
+ let backlog: &mut [Abs];
+ let last;
+
+ // If the block has a fixed height, things are very different, so we
+ // handle that case completely separately.
+ match height {
+ Sizing::Auto | Sizing::Fr(_) => {
+ // If the block is automatically sized, we can just inherit the
+ // regions.
+ first = regions.size.y;
+ full = regions.full;
+ buf.extend_from_slice(regions.backlog);
+ backlog = buf;
+ last = regions.last;
+ }
+
+ Sizing::Rel(rel) => {
+ // Resolve the sizing to a concrete size.
+ let resolved = rel.resolve(styles).relative_to(base.y);
+
+ // Since we're manually sized, the resolved size is the base height.
+ full = resolved;
+
+ // Distribute the fixed height across a start region and a backlog.
+ (first, backlog) = distribute(resolved, regions, buf);
+
+ // If the height is manually sized, we don't want a final repeatable
+ // region.
+ last = None;
+ }
+ };
+
+ // Resolve the horizontal sizing to a concrete width and combine
+ // `width` and `first` into `size`.
+ let mut size = Size::new(
+ match width {
+ Sizing::Auto | Sizing::Fr(_) => regions.size.x,
+ Sizing::Rel(rel) => rel.resolve(styles).relative_to(base.x),
+ },
+ first,
+ );
+
+ // Take the inset, if any, into account, applying it to the
+ // individual region components.
+ let (mut full, mut last) = (full, last);
+ if !inset.is_zero() {
+ crate::layout::shrink_multiple(&mut size, &mut full, backlog, &mut last, inset);
+ }
+
+ // If the child is manually, the size is forced and we should enable
+ // expansion.
+ let expand = Axes::new(
+ *width != Sizing::Auto && size.x.is_finite(),
+ *height != Sizing::Auto && size.y.is_finite(),
+ );
+
+ Regions { size, full, backlog, last, expand }
+}
+
/// Distribute a fixed height spread over existing regions into a new first
/// height and a new backlog.
fn distribute<'a>(
diff --git a/crates/typst/src/layout/flow/collect.rs b/crates/typst/src/layout/flow/collect.rs
index 13b49d86..73659ee2 100644
--- a/crates/typst/src/layout/flow/collect.rs
+++ b/crates/typst/src/layout/flow/collect.rs
@@ -1,3 +1,7 @@
+use std::cell::RefCell;
+use std::fmt::{self, Debug, Formatter};
+use std::hash::Hash;
+
use bumpalo::boxed::Box as BumpBox;
use bumpalo::Bump;
use once_cell::unsync::Lazy;
@@ -5,39 +9,18 @@ use once_cell::unsync::Lazy;
use crate::diag::{bail, SourceResult};
use crate::engine::Engine;
use crate::foundations::{Packed, Resolve, Smart, StyleChain};
-use crate::introspection::{Locator, Tag, TagElem};
+use crate::introspection::{Locator, SplitLocator, Tag, TagElem};
use crate::layout::{
layout_frame, Abs, AlignElem, Alignment, Axes, BlockElem, ColbreakElem,
- FixedAlignment, FlushElem, Fr, Fragment, Frame, PagebreakElem, PlaceElem, Ratio,
- Region, Regions, Rel, Size, Spacing, VElem,
+ FixedAlignment, FlushElem, Fr, Fragment, Frame, PagebreakElem, PlaceElem,
+ PlacementScope, Ratio, Region, Regions, Rel, Size, Sizing, Spacing, VElem,
};
use crate::model::ParElem;
use crate::realize::Pair;
use crate::text::TextElem;
-/// A prepared child in flow layout.
-///
-/// The larger variants are bump-boxed to keep the enum size down.
-pub enum Child<'a> {
- /// An introspection tag.
- Tag(&'a Tag),
- /// Relative spacing with a specific weakness.
- Rel(Rel<Abs>, u8),
- /// Fractional spacing.
- Fr(Fr),
- /// An already layouted line of a paragraph.
- Line(BumpBox<'a, LineChild>),
- /// A potentially breakable block.
- Block(BumpBox<'a, BlockChild<'a>>),
- /// An absolutely or floatingly placed element.
- Placed(BumpBox<'a, PlacedChild<'a>>),
- /// A column break.
- Break(bool),
- /// A place flush.
- Flush,
-}
-
-/// Collects all content of the flow into prepared children.
+/// Collects all elements of the flow into prepared children. These are much
+/// simpler to handle than the raw elements.
#[typst_macros::time]
pub fn collect<'a>(
engine: &mut Engine,
@@ -47,218 +30,510 @@ pub fn collect<'a>(
base: Size,
expand: bool,
) -> SourceResult<Vec<Child<'a>>> {
- let mut locator = locator.split();
- let mut output = Vec::with_capacity(children.len());
- let mut last_was_par = false;
-
- for &(child, styles) in children {
- if let Some(elem) = child.to_packed::<TagElem>() {
- output.push(Child::Tag(&elem.tag));
- } else if let Some(elem) = child.to_packed::<VElem>() {
- output.push(match elem.amount {
- Spacing::Rel(rel) => {
- Child::Rel(rel.resolve(styles), elem.weak(styles) as u8)
- }
- Spacing::Fr(fr) => Child::Fr(fr),
- });
- } else if let Some(elem) = child.to_packed::<ColbreakElem>() {
- output.push(Child::Break(elem.weak(styles)));
- } else if let Some(elem) = child.to_packed::<ParElem>() {
- let align = AlignElem::alignment_in(styles).resolve(styles);
- let leading = ParElem::leading_in(styles);
- let spacing = ParElem::spacing_in(styles);
- let costs = TextElem::costs_in(styles);
-
- let lines = crate::layout::layout_inline(
- engine,
- &elem.children,
- locator.next(&elem.span()),
- styles,
- last_was_par,
- base,
- expand,
- )?
- .into_frames();
-
- output.push(Child::Rel(spacing.into(), 4));
-
- // Determine whether to prevent widow and orphans.
- let len = lines.len();
- let prevent_orphans =
- costs.orphan() > Ratio::zero() && len >= 2 && !lines[1].is_empty();
- let prevent_widows =
- costs.widow() > Ratio::zero() && len >= 2 && !lines[len - 2].is_empty();
- let prevent_all = len == 3 && prevent_orphans && prevent_widows;
-
- // Store the heights of lines at the edges because we'll potentially
- // need these later when `lines` is already moved.
- let height_at = |i| lines.get(i).map(Frame::height).unwrap_or_default();
- let front_1 = height_at(0);
- let front_2 = height_at(1);
- let back_2 = height_at(len.saturating_sub(2));
- let back_1 = height_at(len.saturating_sub(1));
-
- for (i, frame) in lines.into_iter().enumerate() {
- if i > 0 {
- output.push(Child::Rel(leading.into(), 5));
- }
-
- // To prevent widows and orphans, we require enough space for
- // - all lines if it's just three
- // - the first two lines if we're at the first line
- // - the last two lines if we're at the second to last line
- let need = if prevent_all && i == 0 {
- front_1 + leading + front_2 + leading + back_1
- } else if prevent_orphans && i == 0 {
- front_1 + leading + front_2
- } else if prevent_widows && i >= 2 && i + 2 == len {
- back_2 + leading + back_1
- } else {
- frame.height()
- };
-
- let child = LineChild { frame, align, need };
- output.push(Child::Line(BumpBox::new_in(child, bump)));
+ Collector {
+ engine,
+ bump,
+ children,
+ locator: locator.split(),
+ base,
+ expand,
+ output: Vec::with_capacity(children.len()),
+ last_was_par: false,
+ }
+ .run()
+}
+
+/// State for collection.
+struct Collector<'a, 'x, 'y> {
+ engine: &'x mut Engine<'y>,
+ bump: &'a Bump,
+ children: &'x [Pair<'a>],
+ base: Size,
+ expand: bool,
+ locator: SplitLocator<'a>,
+ output: Vec<Child<'a>>,
+ last_was_par: bool,
+}
+
+impl<'a> Collector<'a, '_, '_> {
+ /// Perform the collection.
+ fn run(mut self) -> SourceResult<Vec<Child<'a>>> {
+ for (idx, &(child, styles)) in self.children.iter().enumerate() {
+ if let Some(elem) = child.to_packed::<TagElem>() {
+ self.output.push(Child::Tag(&elem.tag));
+ } else if let Some(elem) = child.to_packed::<VElem>() {
+ self.v(elem, styles);
+ } else if let Some(elem) = child.to_packed::<ParElem>() {
+ self.par(elem, styles)?;
+ } else if let Some(elem) = child.to_packed::<BlockElem>() {
+ self.block(elem, styles);
+ } else if let Some(elem) = child.to_packed::<PlaceElem>() {
+ self.place(idx, elem, styles)?;
+ } else if child.is::<FlushElem>() {
+ self.output.push(Child::Flush);
+ } else if let Some(elem) = child.to_packed::<ColbreakElem>() {
+ self.output.push(Child::Break(elem.weak(styles)));
+ } else if child.is::<PagebreakElem>() {
+ bail!(
+ child.span(), "pagebreaks are not allowed inside of containers";
+ hint: "try using a `#colbreak()` instead",
+ );
+ } else {
+ bail!(child.span(), "{} is not allowed here", child.func().name());
}
+ }
- output.push(Child::Rel(spacing.into(), 4));
- last_was_par = true;
- } else if let Some(elem) = child.to_packed::<BlockElem>() {
- let locator = locator.next(&elem.span());
- let align = AlignElem::alignment_in(styles).resolve(styles);
- let sticky = elem.sticky(styles);
- let rootable = elem.rootable(styles);
-
- let fallback = Lazy::new(|| ParElem::spacing_in(styles));
- let spacing = |amount| match amount {
- Smart::Auto => Child::Rel((*fallback).into(), 4),
- Smart::Custom(Spacing::Rel(rel)) => Child::Rel(rel.resolve(styles), 3),
- Smart::Custom(Spacing::Fr(fr)) => Child::Fr(fr),
- };
+ Ok(self.output)
+ }
+
+ /// Collect vertical spacing into a relative or fractional child.
+ fn v(&mut self, elem: &'a Packed<VElem>, styles: StyleChain<'a>) {
+ self.output.push(match elem.amount {
+ Spacing::Rel(rel) => Child::Rel(rel.resolve(styles), elem.weak(styles) as u8),
+ Spacing::Fr(fr) => Child::Fr(fr),
+ });
+ }
- output.push(spacing(elem.above(styles)));
+ /// Collect a paragraph into [`LineChild`]ren. This already performs line
+ /// layout since it is not dependent on the concrete regions.
+ fn par(
+ &mut self,
+ elem: &'a Packed<ParElem>,
+ styles: StyleChain<'a>,
+ ) -> SourceResult<()> {
+ let align = AlignElem::alignment_in(styles).resolve(styles);
+ let leading = ParElem::leading_in(styles);
+ let spacing = ParElem::spacing_in(styles);
+ let costs = TextElem::costs_in(styles);
- let child = BlockChild { align, sticky, rootable, elem, styles, locator };
- output.push(Child::Block(BumpBox::new_in(child, bump)));
+ let lines = crate::layout::layout_inline(
+ self.engine,
+ &elem.children,
+ self.locator.next(&elem.span()),
+ styles,
+ self.last_was_par,
+ self.base,
+ self.expand,
+ )?
+ .into_frames();
- output.push(spacing(elem.below(styles)));
- last_was_par = false;
- } else if let Some(elem) = child.to_packed::<PlaceElem>() {
- let locator = locator.next(&elem.span());
- let float = elem.float(styles);
- let clearance = elem.clearance(styles);
- let delta = Axes::new(elem.dx(styles), elem.dy(styles)).resolve(styles);
+ self.output.push(Child::Rel(spacing.into(), 4));
- let alignment = elem.alignment(styles);
- let align_x = alignment.map_or(FixedAlignment::Center, |align| {
- align.x().unwrap_or_default().resolve(styles)
- });
- let align_y = alignment.map(|align| align.y().map(|y| y.resolve(styles)));
-
- match (float, align_y) {
- (true, Smart::Custom(None | Some(FixedAlignment::Center))) => bail!(
- elem.span(),
- "floating placement must be `auto`, `top`, or `bottom`"
- ),
- (false, Smart::Auto) => bail!(
- elem.span(),
- "automatic positioning is only available for floating placement";
- hint: "you can enable floating placement with `place(float: true, ..)`"
- ),
- _ => {}
+ // Determine whether to prevent widow and orphans.
+ let len = lines.len();
+ let prevent_orphans =
+ costs.orphan() > Ratio::zero() && len >= 2 && !lines[1].is_empty();
+ let prevent_widows =
+ costs.widow() > Ratio::zero() && len >= 2 && !lines[len - 2].is_empty();
+ let prevent_all = len == 3 && prevent_orphans && prevent_widows;
+
+ // Store the heights of lines at the edges because we'll potentially
+ // need these later when `lines` is already moved.
+ let height_at = |i| lines.get(i).map(Frame::height).unwrap_or_default();
+ let front_1 = height_at(0);
+ let front_2 = height_at(1);
+ let back_2 = height_at(len.saturating_sub(2));
+ let back_1 = height_at(len.saturating_sub(1));
+
+ for (i, frame) in lines.into_iter().enumerate() {
+ if i > 0 {
+ self.output.push(Child::Rel(leading.into(), 5));
}
- let child = PlacedChild {
- float,
- clearance,
- delta,
- align_x,
- align_y,
+ // To prevent widows and orphans, we require enough space for
+ // - all lines if it's just three
+ // - the first two lines if we're at the first line
+ // - the last two lines if we're at the second to last line
+ let need = if prevent_all && i == 0 {
+ front_1 + leading + front_2 + leading + back_1
+ } else if prevent_orphans && i == 0 {
+ front_1 + leading + front_2
+ } else if prevent_widows && i >= 2 && i + 2 == len {
+ back_2 + leading + back_1
+ } else {
+ frame.height()
+ };
+
+ self.output
+ .push(Child::Line(self.boxed(LineChild { frame, align, need })));
+ }
+
+ self.output.push(Child::Rel(spacing.into(), 4));
+ self.last_was_par = true;
+
+ Ok(())
+ }
+
+ /// Collect a block into a [`SingleChild`] or [`MultiChild`] depending on
+ /// whether it is breakable.
+ fn block(&mut self, elem: &'a Packed<BlockElem>, styles: StyleChain<'a>) {
+ let locator = self.locator.next(&elem.span());
+ let align = AlignElem::alignment_in(styles).resolve(styles);
+ let sticky = elem.sticky(styles);
+ let breakable = elem.breakable(styles);
+ let fr = match elem.height(styles) {
+ Sizing::Fr(fr) => Some(fr),
+ _ => None,
+ };
+
+ let fallback = Lazy::new(|| ParElem::spacing_in(styles));
+ let spacing = |amount| match amount {
+ Smart::Auto => Child::Rel((*fallback).into(), 4),
+ Smart::Custom(Spacing::Rel(rel)) => Child::Rel(rel.resolve(styles), 3),
+ Smart::Custom(Spacing::Fr(fr)) => Child::Fr(fr),
+ };
+
+ self.output.push(spacing(elem.above(styles)));
+
+ if !breakable || sticky || fr.is_some() {
+ self.output.push(Child::Single(self.boxed(SingleChild {
+ align,
+ sticky,
+ fr,
elem,
styles,
locator,
- alignment,
- };
- output.push(Child::Placed(BumpBox::new_in(child, bump)));
- } else if child.is::<FlushElem>() {
- output.push(Child::Flush);
- } else if child.is::<PagebreakElem>() {
+ cell: CachedCell::new(),
+ })));
+ } else {
+ let alone = self.children.len() == 1;
+ self.output.push(Child::Multi(self.boxed(MultiChild {
+ align,
+ alone,
+ elem,
+ styles,
+ locator,
+ cell: CachedCell::new(),
+ })));
+ };
+
+ self.output.push(spacing(elem.below(styles)));
+ self.last_was_par = false;
+ }
+
+ /// Collects a placed element into a [`PlacedChild`].
+ fn place(
+ &mut self,
+ idx: usize,
+ elem: &'a Packed<PlaceElem>,
+ styles: StyleChain<'a>,
+ ) -> SourceResult<()> {
+ let alignment = elem.alignment(styles);
+ let align_x = alignment.map_or(FixedAlignment::Center, |align| {
+ align.x().unwrap_or_default().resolve(styles)
+ });
+ let align_y = alignment.map(|align| align.y().map(|y| y.resolve(styles)));
+ let scope = elem.scope(styles);
+ let float = elem.float(styles);
+
+ match (float, align_y) {
+ (true, Smart::Custom(None | Some(FixedAlignment::Center))) => bail!(
+ elem.span(),
+ "vertical floating placement must be `auto`, `top`, or `bottom`"
+ ),
+ (false, Smart::Auto) => bail!(
+ elem.span(),
+ "automatic positioning is only available for floating placement";
+ hint: "you can enable floating placement with `place(float: true, ..)`"
+ ),
+ _ => {}
+ }
+
+ if !float && scope == PlacementScope::Page {
bail!(
- child.span(), "pagebreaks are not allowed inside of containers";
- hint: "try using a `#colbreak()` instead",
+ elem.span(),
+ "page-scoped positioning is currently only available for floating placement";
+ hint: "you can enable floating placement with `place(float: true, ..)`"
);
- } else {
- bail!(child.span(), "{} is not allowed here", child.func().name());
}
+
+ let locator = self.locator.next(&elem.span());
+ let clearance = elem.clearance(styles);
+ let delta = Axes::new(elem.dx(styles), elem.dy(styles)).resolve(styles);
+ self.output.push(Child::Placed(self.boxed(PlacedChild {
+ idx,
+ align_x,
+ align_y,
+ scope,
+ float,
+ clearance,
+ delta,
+ elem,
+ styles,
+ locator,
+ alignment,
+ cell: CachedCell::new(),
+ })));
+
+ Ok(())
}
- Ok(output)
+ /// Wraps a value in a bump-allocated box to reduce its footprint in the
+ /// [`Child`] enum.
+ fn boxed<T>(&self, value: T) -> BumpBox<'a, T> {
+ BumpBox::new_in(value, self.bump)
+ }
}
-/// A child that encapsulates a paragraph line.
+/// A prepared child in flow layout.
+///
+/// The larger variants are bump-boxed to keep the enum size down.
+#[derive(Debug)]
+pub enum Child<'a> {
+ /// An introspection tag.
+ Tag(&'a Tag),
+ /// Relative spacing with a specific weakness level.
+ Rel(Rel<Abs>, u8),
+ /// Fractional spacing.
+ Fr(Fr),
+ /// An already layouted line of a paragraph.
+ Line(BumpBox<'a, LineChild>),
+ /// An unbreakable block.
+ Single(BumpBox<'a, SingleChild<'a>>),
+ /// A breakable block.
+ Multi(BumpBox<'a, MultiChild<'a>>),
+ /// An absolutely or floatingly placed element.
+ Placed(BumpBox<'a, PlacedChild<'a>>),
+ /// A place flush.
+ Flush,
+ /// An explicit column break.
+ Break(bool),
+}
+
+/// A child that encapsulates a layouted line of a paragraph.
+#[derive(Debug)]
pub struct LineChild {
pub frame: Frame,
pub align: Axes<FixedAlignment>,
pub need: Abs,
}
-/// A child that encapsulates a prepared block.
-pub struct BlockChild<'a> {
+/// A child that encapsulates a prepared unbreakable block.
+#[derive(Debug)]
+pub struct SingleChild<'a> {
pub align: Axes<FixedAlignment>,
pub sticky: bool,
- pub rootable: bool,
+ pub fr: Option<Fr>,
elem: &'a Packed<BlockElem>,
styles: StyleChain<'a>,
locator: Locator<'a>,
+ cell: CachedCell<SourceResult<Frame>>,
}
-impl BlockChild<'_> {
+impl SingleChild<'_> {
+ /// Build the child's frame given the region's base size.
+ pub fn layout(&self, engine: &mut Engine, base: Size) -> SourceResult<Frame> {
+ self.cell.get_or_init(base, |base| {
+ self.elem
+ .layout_single(engine, self.locator.relayout(), self.styles, base)
+ .map(|frame| frame.post_processed(self.styles))
+ })
+ }
+}
+
+/// A child that encapsulates a prepared breakable block.
+#[derive(Debug)]
+pub struct MultiChild<'a> {
+ pub align: Axes<FixedAlignment>,
+ alone: bool,
+ elem: &'a Packed<BlockElem>,
+ styles: StyleChain<'a>,
+ locator: Locator<'a>,
+ cell: CachedCell<SourceResult<Fragment>>,
+}
+
+impl<'a> MultiChild<'a> {
/// Build the child's frames given regions.
- pub fn layout(
+ pub fn layout<'b>(
+ &'b self,
+ engine: &mut Engine,
+ regions: Regions,
+ ) -> SourceResult<(Frame, Option<MultiSpill<'a, 'b>>)> {
+ let fragment = self.layout_impl(engine, regions)?;
+
+ // Extract the first frame.
+ let mut frames = fragment.into_iter();
+ let frame = frames.next().unwrap();
+
+ // If there's more, return a `spill`.
+ let mut spill = None;
+ if frames.next().is_some() {
+ spill = Some(MultiSpill {
+ multi: self,
+ full: regions.full,
+ first: regions.size.y,
+ backlog: vec![],
+ });
+ }
+
+ Ok((frame, spill))
+ }
+
+ /// The shared internal implementation of [`Self::layout`] and
+ /// [`MultiSpill::layout`].
+ fn layout_impl(
&self,
engine: &mut Engine,
regions: Regions,
) -> SourceResult<Fragment> {
- let mut fragment =
+ self.cell.get_or_init(regions, |mut regions| {
+ // Vertical expansion is only kept if this block is the only child.
+ regions.expand.y &= self.alone;
self.elem
- .layout(engine, self.locator.relayout(), self.styles, regions)?;
+ .layout_multiple(engine, self.locator.relayout(), self.styles, regions)
+ .map(|mut fragment| {
+ for frame in &mut fragment {
+ frame.post_process(self.styles);
+ }
+ fragment
+ })
+ })
+ }
+}
+
+/// The spilled remains of a `MultiChild` that broke across two regions.
+#[derive(Debug, Clone)]
+pub struct MultiSpill<'a, 'b> {
+ multi: &'b MultiChild<'a>,
+ first: Abs,
+ full: Abs,
+ backlog: Vec<Abs>,
+}
+
+impl MultiSpill<'_, '_> {
+ /// Build the spill's frames given regions.
+ pub fn layout(
+ mut self,
+ engine: &mut Engine,
+ regions: Regions,
+ ) -> SourceResult<(Frame, Option<Self>)> {
+ // We build regions for the whole `MultiChild` with the sizes passed to
+ // earlier parts of it plus the new regions. Then, we layout the
+ // complete block, but extract only the suffix that interests us.
+ self.backlog.push(regions.size.y);
+
+ let mut backlog: Vec<_> =
+ self.backlog.iter().chain(regions.backlog).copied().collect();
+
+ // Remove unnecessary backlog items (also to prevent it from growing
+ // unnecessarily, which would change the region's hash).
+ while !backlog.is_empty() && backlog.last().copied() == regions.last {
+ backlog.pop();
+ }
+
+ // Build the pod with the merged regions.
+ let pod = Regions {
+ size: Size::new(regions.size.x, self.first),
+ expand: regions.expand,
+ full: self.full,
+ backlog: &backlog,
+ last: regions.last,
+ };
- for frame in &mut fragment {
- frame.post_process(self.styles);
+ // Extract the not-yet-processed frames.
+ let mut frames = self
+ .multi
+ .layout_impl(engine, pod)?
+ .into_iter()
+ .skip(self.backlog.len());
+
+ // Save the first frame.
+ let frame = frames.next().unwrap();
+
+ // If there's more, return a `spill`.
+ let mut spill = None;
+ if frames.next().is_some() {
+ spill = Some(self);
}
- Ok(fragment)
+ Ok((frame, spill))
+ }
+
+ /// The alignment of the breakable block.
+ pub fn align(&self) -> Axes<FixedAlignment> {
+ self.multi.align
}
}
/// A child that encapsulates a prepared placed element.
+#[derive(Debug)]
pub struct PlacedChild<'a> {
+ pub idx: usize,
+ pub align_x: FixedAlignment,
+ pub align_y: Smart<Option<FixedAlignment>>,
+ pub scope: PlacementScope,
pub float: bool,
pub clearance: Abs,
pub delta: Axes<Rel<Abs>>,
- pub align_x: FixedAlignment,
- pub align_y: Smart<Option<FixedAlignment>>,
elem: &'a Packed<PlaceElem>,
styles: StyleChain<'a>,
locator: Locator<'a>,
alignment: Smart<Alignment>,
+ cell: CachedCell<SourceResult<Frame>>,
}
impl PlacedChild<'_> {
/// Build the child's frame given the region's base size.
pub fn layout(&self, engine: &mut Engine, base: Size) -> SourceResult<Frame> {
- let align = self.alignment.unwrap_or_else(|| Alignment::CENTER);
- let aligned = AlignElem::set_alignment(align).wrap();
-
- let mut frame = layout_frame(
- engine,
- &self.elem.body,
- self.locator.relayout(),
- self.styles.chain(&aligned),
- Region::new(base, Axes::splat(false)),
- )?;
-
- frame.post_process(self.styles);
- Ok(frame)
+ self.cell.get_or_init(base, |base| {
+ let align = self.alignment.unwrap_or_else(|| Alignment::CENTER);
+ let aligned = AlignElem::set_alignment(align).wrap();
+ layout_frame(
+ engine,
+ &self.elem.body,
+ self.locator.relayout(),
+ self.styles.chain(&aligned),
+ Region::new(base, Axes::splat(false)),
+ )
+ .map(|frame| frame.post_processed(self.styles))
+ })
+ }
+}
+
+/// Wraps a parameterized computation and caches its latest output.
+///
+/// - When the computation is performed multiple times consecutively with the
+/// same argument, reuses the cache.
+/// - When the argument changes, the new output is cached.
+#[derive(Clone)]
+struct CachedCell<T>(RefCell<Option<(u128, T)>>);
+
+impl<T> CachedCell<T> {
+ /// Create an empty cached cell.
+ fn new() -> Self {
+ Self(RefCell::new(None))
+ }
+
+ /// Perform the computation `f` with caching.
+ fn get_or_init<F, I>(&self, input: I, f: F) -> T
+ where
+ I: Hash,
+ T: Clone,
+ F: FnOnce(I) -> T,
+ {
+ let input_hash = crate::utils::hash128(&input);
+
+ let mut slot = self.0.borrow_mut();
+ if let Some((hash, output)) = &*slot {
+ if *hash == input_hash {
+ return output.clone();
+ }
+ }
+
+ let output = f(input);
+ *slot = Some((input_hash, output.clone()));
+ output
+ }
+}
+
+impl<T> Default for CachedCell<T> {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl<T> Debug for CachedCell<T> {
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ f.pad("CachedCell(..)")
}
}
diff --git a/crates/typst/src/layout/flow/compose.rs b/crates/typst/src/layout/flow/compose.rs
new file mode 100644
index 00000000..e5d6f7b1
--- /dev/null
+++ b/crates/typst/src/layout/flow/compose.rs
@@ -0,0 +1,843 @@
+use std::num::NonZeroUsize;
+
+use super::{distribute, Config, FlowResult, PlacedChild, Skip, Stop, Work};
+use crate::diag::SourceResult;
+use crate::engine::Engine;
+use crate::foundations::{Content, NativeElement, Packed, Resolve, Smart};
+use crate::introspection::{
+ Counter, CounterDisplayElem, CounterState, CounterUpdate, Locator, SplitLocator,
+ TagKind,
+};
+use crate::layout::{
+ layout_fragment, layout_frame, Abs, Axes, Dir, FixedAlignment, Frame, FrameItem,
+ OuterHAlignment, PlacementScope, Point, Region, Regions, Rel, Size,
+};
+use crate::model::{
+ FootnoteElem, FootnoteEntry, LineNumberingScope, Numbering, ParLine, ParLineMarker,
+};
+use crate::syntax::Span;
+use crate::utils::NonZeroExt;
+
+/// Composes the contents of a single page/region. A region can have multiple
+/// columns/subregions.
+///
+/// The composer is primarily concerned with layout of out-of-flow insertions
+/// (floats and footnotes). It does this in per-page and per-column loops that
+/// rerun when a new float is added (since it affects the regions available to
+/// the distributor).
+///
+/// To lay out the in-flow contents of individual subregions, the composer
+/// invokes [distribution](distribute).
+pub fn compose(
+ engine: &mut Engine,
+ work: &mut Work,
+ config: &Config,
+ locator: Locator,
+ regions: Regions,
+) -> SourceResult<Frame> {
+ Composer {
+ engine,
+ config,
+ page_base: regions.base(),
+ column: 0,
+ page_insertions: Insertions::default(),
+ column_insertions: Insertions::default(),
+ work,
+ footnote_spill: None,
+ footnote_queue: vec![],
+ }
+ .page(locator, regions)
+}
+
+/// State for composition.
+///
+/// Sadly, we need that many lifetimes because &mut references are invariant and
+/// it would force the lifetimes of various things to be equal if they
+/// shared a lifetime.
+///
+/// The only interesting lifetimes are 'a and 'b. See [Work] for more details
+/// about them.
+pub struct Composer<'a, 'b, 'x, 'y> {
+ pub engine: &'x mut Engine<'y>,
+ pub work: &'x mut Work<'a, 'b>,
+ pub config: &'x Config<'x>,
+ column: usize,
+ page_base: Size,
+ page_insertions: Insertions<'a, 'b>,
+ column_insertions: Insertions<'a, 'b>,
+ // These are here because they have to survive relayout (we could lose the
+ // footnotes otherwise). For floats, we revisit them anyway, so it's okay to
+ // use `work.floats` directly. This is not super clean; probably there's a
+ // better way.
+ footnote_spill: Option<std::vec::IntoIter<Frame>>,
+ footnote_queue: Vec<Packed<FootnoteElem>>,
+}
+
+impl<'a, 'b> Composer<'a, 'b, '_, '_> {
+ /// Lay out a container/page region, including container/page insertions.
+ fn page(mut self, locator: Locator, regions: Regions) -> SourceResult<Frame> {
+ // This loop can restart region layout when requested to do so by a
+ // `Stop`. This happens when there is a page-scoped float.
+ let checkpoint = self.work.clone();
+ let output = loop {
+ // Shrink the available space by the space used by page
+ // insertions.
+ let mut pod = regions;
+ pod.size.y -= self.page_insertions.height();
+
+ match self.page_contents(locator.relayout(), pod) {
+ Ok(frame) => break frame,
+ Err(Stop::Finish(_)) => unreachable!(),
+ Err(Stop::Relayout(PlacementScope::Column)) => unreachable!(),
+ Err(Stop::Relayout(PlacementScope::Page)) => {
+ *self.work = checkpoint.clone();
+ continue;
+ }
+ Err(Stop::Error(err)) => return Err(err),
+ };
+ };
+ drop(checkpoint);
+
+ Ok(self.page_insertions.finalize(self.work, self.config, output))
+ }
+
+ /// Lay out the inner contents of a container/page.
+ fn page_contents(&mut self, locator: Locator, regions: Regions) -> FlowResult<Frame> {
+ // No point in create column regions, if there's just one!
+ if self.config.columns.count == 1 {
+ return self.column(locator, regions);
+ }
+
+ // Create a backlog for multi-column layout.
+ let column_height = regions.size.y;
+ let backlog: Vec<_> = std::iter::once(&column_height)
+ .chain(regions.backlog)
+ .flat_map(|&h| std::iter::repeat(h).take(self.config.columns.count))
+ .skip(1)
+ .collect();
+
+ // Subregions for column layout.
+ let mut inner = Regions {
+ size: Size::new(self.config.columns.width, column_height),
+ backlog: &backlog,
+ expand: Axes::new(true, regions.expand.y),
+ ..regions
+ };
+
+ // The size of the merged frame hosting multiple columns.
+ let size = Size::new(
+ regions.size.x,
+ if regions.expand.y { regions.size.y } else { Abs::zero() },
+ );
+
+ let mut output = Frame::hard(size);
+ let mut offset = Abs::zero();
+ let mut locator = locator.split();
+
+ // Lay out the columns and stitch them together.
+ for i in 0..self.config.columns.count {
+ self.column = i;
+ let frame = self.column(locator.next(&()), inner)?;
+
+ if !regions.expand.y {
+ output.size_mut().y.set_max(frame.height());
+ }
+
+ let width = frame.width();
+ let x = if self.config.columns.dir == Dir::LTR {
+ offset
+ } else {
+ regions.size.x - offset - width
+ };
+ offset += width + self.config.columns.gutter;
+
+ output.push_frame(Point::with_x(x), frame);
+ inner.next();
+ }
+
+ Ok(output)
+ }
+
+ /// Lay out a column, including column insertions.
+ fn column(&mut self, locator: Locator, regions: Regions) -> FlowResult<Frame> {
+ // Reset column insertion when starting a new column.
+ self.column_insertions = Insertions::default();
+
+ // Process footnote spill.
+ if let Some(spill) = self.work.footnote_spill.take() {
+ self.footnote_spill(spill, regions.base())?;
+ }
+
+ // This loop can restart column layout when requested to do so by a
+ // `Stop`. This happens when there is a column-scoped float.
+ let checkpoint = self.work.clone();
+ let inner = loop {
+ // Shrink the available space by the space used by column
+ // insertions.
+ let mut pod = regions;
+ pod.size.y -= self.column_insertions.height();
+
+ match self.column_contents(pod) {
+ Ok(frame) => break frame,
+ Err(Stop::Finish(_)) => unreachable!(),
+ Err(Stop::Relayout(PlacementScope::Column)) => {
+ *self.work = checkpoint.clone();
+ continue;
+ }
+ err => return err,
+ }
+ };
+ drop(checkpoint);
+
+ self.work.footnotes.extend(self.footnote_queue.drain(..));
+ if let Some(spill) = self.footnote_spill.take() {
+ self.work.footnote_spill = Some(spill);
+ }
+
+ let insertions = std::mem::take(&mut self.column_insertions);
+ let mut output = insertions.finalize(self.work, self.config, inner);
+
+ // Lay out per-column line numbers.
+ if self.config.root {
+ layout_line_numbers(
+ self.engine,
+ self.config,
+ locator,
+ self.column,
+ &mut output,
+ )?;
+ }
+
+ Ok(output)
+ }
+
+ /// Lay out the inner contents of a column.
+ fn column_contents(&mut self, regions: Regions) -> FlowResult<Frame> {
+ // Process pending footnotes.
+ for note in std::mem::take(&mut self.work.footnotes) {
+ self.footnote(note, &mut regions.clone(), Abs::zero(), false)?;
+ }
+
+ // Process pending floats.
+ for placed in std::mem::take(&mut self.work.floats) {
+ self.float(placed, &regions, true)?;
+ }
+
+ distribute(self, regions)
+ }
+
+ /// Lays out an item with floating placement.
+ ///
+ /// This is called from within [`distribute`]. When the float fits, this
+ /// returns an `Err(Stop::Relayout(..))`, which bubbles all the way through
+ /// distribution and is handled in [`Self::page`] or [`Self::column`]
+ /// (depending on `placed.scope`).
+ ///
+ /// When the float does not fit, it is queued into `work.floats`. The
+ /// value of `clearance` that between the float and flow content is needed
+ /// --- it is set if there are already distributed items.
+ pub fn float(
+ &mut self,
+ placed: &'b PlacedChild<'a>,
+ regions: &Regions,
+ clearance: bool,
+ ) -> FlowResult<()> {
+ // If the float is already processed, skip it.
+ if self.skipped(Skip::Placed(placed.idx)) {
+ return Ok(());
+ }
+
+ // If there is already a queued float, queue this one as well. We
+ // don't want to disrupt the order.
+ if !self.work.floats.is_empty() {
+ self.work.floats.push(placed);
+ return Ok(());
+ }
+
+ // Determine the base size of the chosen scope.
+ let base = match placed.scope {
+ PlacementScope::Column => regions.base(),
+ PlacementScope::Page => self.page_base,
+ };
+
+ // Lay out the placed element.
+ let frame = placed.layout(self.engine, base)?;
+
+ // Determine the remaining space in the scope. This is exact for column
+ // placement, but only an approximation for page placement.
+ let remaining = match placed.scope {
+ PlacementScope::Column => regions.size.y,
+ PlacementScope::Page => {
+ let remaining: Abs = regions
+ .iter()
+ .map(|size| size.y)
+ .take(self.config.columns.count - self.column)
+ .sum();
+ remaining / self.config.columns.count as f64
+ }
+ };
+
+ // We only require clearance if there is other content.
+ let clearance = if clearance { Abs::zero() } else { placed.clearance };
+ let need = frame.height() + clearance;
+
+ // If the float doesn't fit, queue it for the next region.
+ if !remaining.fits(need) && !regions.in_last() {
+ self.work.floats.push(placed);
+ return Ok(());
+ }
+
+ // Handle footnotes in the float.
+ self.footnotes(regions, &frame, need, false)?;
+
+ // Determine the float's vertical alignment. We can unwrap the inner
+ // `Option` because `Custom(None)` is checked for during collection.
+ let align_y = placed.align_y.map(Option::unwrap).unwrap_or_else(|| {
+ // When the float's vertical midpoint would be above the middle of
+ // the page if it were layouted in-flow, we use top alignment.
+ // Otherwise, we use bottom alignment.
+ let used = base.y - remaining;
+ let half = need / 2.0;
+ let ratio = (used + half) / base.y;
+ if ratio <= 0.5 {
+ FixedAlignment::Start
+ } else {
+ FixedAlignment::End
+ }
+ });
+
+ // Select the insertion area where we'll put this float.
+ let area = match placed.scope {
+ PlacementScope::Column => &mut self.column_insertions,
+ PlacementScope::Page => &mut self.page_insertions,
+ };
+
+ // Put the float there.
+ area.push_float(placed, frame, align_y);
+ area.skips.push(Skip::Placed(placed.idx));
+
+ // Trigger relayout.
+ Err(Stop::Relayout(placed.scope))
+ }
+
+ /// Lays out footnotes in the `frame` if this is the root flow and there are
+ /// any. The value of `breakable` indicates whether the element that
+ /// produced the frame is breakable. If not, the frame is treated as atomic.
+ pub fn footnotes(
+ &mut self,
+ regions: &Regions,
+ frame: &Frame,
+ flow_need: Abs,
+ breakable: bool,
+ ) -> FlowResult<()> {
+ // Footnotes are only supported at the root level.
+ if !self.config.root {
+ return Ok(());
+ }
+
+ // Search for footnotes.
+ let notes = find_in_frame::<FootnoteElem>(frame);
+ if notes.is_empty() {
+ return Ok(());
+ }
+
+ let mut relayout = false;
+ let mut regions = *regions;
+ let mut migratable = !breakable && !regions.in_last();
+
+ for (y, elem) in notes {
+ // The amount of space used by the in-flow content that contains the
+ // footnote marker. For a breakable frame, it's the y position of
+ // the marker. For an unbreakable frame, it's the full height.
+ let flow_need = if breakable { y } else { flow_need };
+
+ // Process the footnote.
+ match self.footnote(elem, &mut regions, flow_need, migratable) {
+ // The footnote was already processed or queued.
+ Ok(()) => {}
+ // First handle more footnotes before relayouting.
+ Err(Stop::Relayout(_)) => relayout = true,
+ // Either of
+ // - A `Stop::Finish` indicating that the frame's origin element
+ // should migrate to uphold the footnote invariant.
+ // - A fatal error.
+ err => return err,
+ }
+
+ // We only migrate the origin frame if the first footnote's first
+ // line didn't fit.
+ migratable = false;
+ }
+
+ // If this is set, we laid out at least one footnote, so we need a
+ // relayout.
+ if relayout {
+ return Err(Stop::Relayout(PlacementScope::Column));
+ }
+
+ Ok(())
+ }
+
+ /// Handles a single footnote.
+ fn footnote(
+ &mut self,
+ elem: Packed<FootnoteElem>,
+ regions: &mut Regions,
+ flow_need: Abs,
+ migratable: bool,
+ ) -> FlowResult<()> {
+ // Ignore reference footnotes and already processed ones.
+ let loc = elem.location().unwrap();
+ if elem.is_ref() || self.skipped(Skip::Footnote(loc)) {
+ return Ok(());
+ }
+
+ // If there is already a queued spill or footnote, queue this one as
+ // well. We don't want to disrupt the order.
+ let area = &mut self.column_insertions;
+ if self.footnote_spill.is_some() || !self.footnote_queue.is_empty() {
+ self.footnote_queue.push(elem);
+ return Ok(());
+ }
+
+ // If there weren't any footnotes so far, account for the footnote
+ // separator.
+ let mut separator = None;
+ let mut separator_need = Abs::zero();
+ if area.footnotes.is_empty() {
+ let frame =
+ layout_footnote_separator(self.engine, self.config, regions.base())?;
+ separator_need += self.config.footnote.clearance + frame.height();
+ separator = Some(frame);
+ }
+
+ // Prepare regions for the footnote.
+ let mut pod = *regions;
+ pod.expand.y = false;
+ pod.size.y -= flow_need + separator_need + self.config.footnote.gap;
+
+ // Layout the footnote entry.
+ let frames = layout_fragment(
+ self.engine,
+ &FootnoteEntry::new(elem.clone()).pack(),
+ Locator::synthesize(elem.location().unwrap()),
+ self.config.shared,
+ pod,
+ )?
+ .into_frames();
+
+ // Find nested footnotes in the entry.
+ let nested = find_in_frames::<FootnoteElem>(&frames);
+
+ // Extract the first frame.
+ let mut iter = frames.into_iter();
+ let first = iter.next().unwrap();
+ let note_need = self.config.footnote.gap + first.height();
+
+ // If the first frame is empty, then none of its content fit. If
+ // possible, we then migrate the origin frame to the next region to
+ // uphold the footnote invariant (that marker and entry are on the same
+ // page). If not, we just queue the footnote for the next page.
+ if first.is_empty() {
+ if migratable {
+ return Err(Stop::Finish(false));
+ } else {
+ self.footnote_queue.push(elem);
+ return Ok(());
+ }
+ }
+
+ // Save the separator.
+ if let Some(frame) = separator {
+ area.push_footnote_separator(self.config, frame);
+ regions.size.y -= separator_need;
+ }
+
+ // Save the footnote's frame.
+ area.push_footnote(self.config, first);
+ area.skips.push(Skip::Footnote(loc));
+ regions.size.y -= note_need;
+
+ // Save the spill.
+ if !iter.as_slice().is_empty() {
+ self.footnote_spill = Some(iter);
+ }
+
+ // Lay out nested footnotes.
+ for (_, note) in nested {
+ self.footnote(note, regions, flow_need, migratable)?;
+ }
+
+ // Since we laid out a footnote, we need a relayout.
+ Err(Stop::Relayout(PlacementScope::Column))
+ }
+
+ /// Handles spillover from a footnote.
+ fn footnote_spill(
+ &mut self,
+ mut iter: std::vec::IntoIter<Frame>,
+ base: Size,
+ ) -> SourceResult<()> {
+ let area = &mut self.column_insertions;
+
+ // Create and save the separator.
+ let separator = layout_footnote_separator(self.engine, self.config, base)?;
+ area.push_footnote_separator(self.config, separator);
+
+ // Save the footnote's frame.
+ let frame = iter.next().unwrap();
+ area.push_footnote(self.config, frame);
+
+ // Save the spill.
+ if !iter.as_slice().is_empty() {
+ self.footnote_spill = Some(iter);
+ }
+
+ Ok(())
+ }
+
+ /// Checks whether an insertion was already processed and doesn't need to be
+ /// handled again.
+ fn skipped(&self, skip: Skip) -> bool {
+ self.work.skips.contains(&skip)
+ || self.page_insertions.skips.contains(&skip)
+ || self.column_insertions.skips.contains(&skip)
+ }
+
+ /// The amount of width needed by insertions.
+ pub fn insertion_width(&self) -> Abs {
+ self.column_insertions.width.max(self.page_insertions.width)
+ }
+}
+
+/// Lay out the footnote separator, typically a line.
+fn layout_footnote_separator(
+ engine: &mut Engine,
+ config: &Config,
+ base: Size,
+) -> SourceResult<Frame> {
+ layout_frame(
+ engine,
+ &config.footnote.separator,
+ Locator::root(),
+ config.shared,
+ Region::new(base, Axes::new(config.footnote.expand, false)),
+ )
+}
+
+/// An additive list of insertions.
+#[derive(Default)]
+struct Insertions<'a, 'b> {
+ top_floats: Vec<(&'b PlacedChild<'a>, Frame)>,
+ bottom_floats: Vec<(&'b PlacedChild<'a>, Frame)>,
+ footnotes: Vec<Frame>,
+ footnote_separator: Option<Frame>,
+ top_size: Abs,
+ bottom_size: Abs,
+ width: Abs,
+ skips: Vec<Skip>,
+}
+
+impl<'a, 'b> Insertions<'a, 'b> {
+ /// Add a float to the top or bottom area.
+ fn push_float(
+ &mut self,
+ placed: &'b PlacedChild<'a>,
+ frame: Frame,
+ align_y: FixedAlignment,
+ ) {
+ self.width.set_max(frame.width());
+
+ let amount = frame.height() + placed.clearance;
+ let pair = (placed, frame);
+
+ if align_y == FixedAlignment::Start {
+ self.top_size += amount;
+ self.top_floats.push(pair);
+ } else {
+ self.bottom_size += amount;
+ self.bottom_floats.push(pair);
+ }
+ }
+
+ /// Add a footnote to the bottom area.
+ fn push_footnote(&mut self, config: &Config, frame: Frame) {
+ self.width.set_max(frame.width());
+ self.bottom_size += config.footnote.gap + frame.height();
+ self.footnotes.push(frame);
+ }
+
+ /// Add a footnote separator to the bottom area.
+ fn push_footnote_separator(&mut self, config: &Config, frame: Frame) {
+ self.width.set_max(frame.width());
+ self.bottom_size += config.footnote.clearance + frame.height();
+ self.footnote_separator = Some(frame);
+ }
+
+ /// The combined height of the top and bottom area (includings clearances).
+ /// Subtracting this from the total region size yields the available space
+ /// for distribution.
+ fn height(&self) -> Abs {
+ self.top_size + self.bottom_size
+ }
+
+ /// Produce a frame for the full region based on the `inner` frame produced
+ /// by distribution or column layout.
+ fn finalize(self, work: &mut Work, config: &Config, inner: Frame) -> Frame {
+ work.extend_skips(&self.skips);
+
+ if self.top_floats.is_empty()
+ && self.bottom_floats.is_empty()
+ && self.footnote_separator.is_none()
+ && self.footnotes.is_empty()
+ {
+ return inner;
+ }
+
+ let size = inner.size() + Size::with_y(self.height());
+
+ let mut output = Frame::soft(size);
+ let mut offset_top = Abs::zero();
+ let mut offset_bottom = size.y - self.bottom_size;
+
+ for (placed, frame) in self.top_floats {
+ let x = placed.align_x.position(size.x - frame.width());
+ let y = offset_top;
+ let delta = placed.delta.zip_map(size, Rel::relative_to).to_point();
+ offset_top += frame.height() + placed.clearance;
+ output.push_frame(Point::new(x, y) + delta, frame);
+ }
+
+ output.push_frame(Point::with_y(self.top_size), inner);
+
+ if let Some(frame) = self.footnote_separator {
+ offset_bottom += config.footnote.clearance;
+ let y = offset_bottom;
+ offset_bottom += frame.height();
+ output.push_frame(Point::with_y(y), frame);
+ }
+
+ for frame in self.footnotes {
+ offset_bottom += config.footnote.gap;
+ let y = offset_bottom;
+ offset_bottom += frame.height();
+ output.push_frame(Point::with_y(y), frame);
+ }
+
+ for (placed, frame) in self.bottom_floats {
+ offset_bottom += placed.clearance;
+ let x = placed.align_x.position(size.x - frame.width());
+ let y = offset_bottom;
+ let delta = placed.delta.zip_map(size, Rel::relative_to).to_point();
+ offset_bottom += frame.height();
+ output.push_frame(Point::new(x, y) + delta, frame);
+ }
+
+ output
+ }
+}
+
+/// Lay out the given collected lines' line numbers to an output frame.
+///
+/// The numbers are placed either on the left margin (left border of the frame)
+/// or on the right margin (right border). Before they are placed, a line number
+/// counter reset is inserted if we're in the first column of the page being
+/// currently laid out and the user requested for line numbers to be reset at
+/// the start of every page.
+fn layout_line_numbers(
+ engine: &mut Engine,
+ config: &Config,
+ locator: Locator,
+ column: usize,
+ output: &mut Frame,
+) -> SourceResult<()> {
+ let mut locator = locator.split();
+
+ // Reset page-scoped line numbers if currently at the first column.
+ if column == 0
+ && ParLine::numbering_scope_in(config.shared) == LineNumberingScope::Page
+ {
+ let reset = layout_line_number_reset(engine, config, &mut locator)?;
+ output.push_frame(Point::zero(), reset);
+ }
+
+ // Find all line markers.
+ let mut lines = find_in_frame::<ParLineMarker>(output);
+ if lines.is_empty() {
+ return Ok(());
+ }
+
+ // Assume the line numbers aren't sorted by height. They must be sorted so
+ // we can deduplicate line numbers below based on vertical proximity.
+ lines.sort_by_key(|&(y, _)| y);
+
+ // Used for horizontal alignment.
+ let mut max_number_width = Abs::zero();
+
+ // This is used to skip lines that are too close together.
+ let mut prev_bottom = None;
+
+ // Buffer line number frames so we can align them horizontally later before
+ // placing, based on the width of the largest line number.
+ let mut line_numbers = vec![];
+
+ // Layout the lines.
+ for &(y, ref marker) in &lines {
+ if prev_bottom.is_some_and(|bottom| y < bottom) {
+ // Lines are too close together. Display as the same line number.
+ continue;
+ }
+
+ // Layout the number and record its width in search of the maximium.
+ let frame = layout_line_number(engine, config, &mut locator, &marker.numbering)?;
+
+ // Note that this line.y is larger than the previous due to sorting.
+ // Therefore, the check at the top of the loop ensures no line numbers
+ // will reasonably intersect with each other. We enforce a minimum
+ // spacing of 1pt between consecutive line numbers in case a zero-height
+ // frame is used.
+ prev_bottom = Some(y + frame.height().max(Abs::pt(1.0)));
+ max_number_width.set_max(frame.width());
+ line_numbers.push((y, marker, frame));
+ }
+
+ for (y, marker, frame) in line_numbers {
+ // The last column will always place line numbers at the end
+ // margin. This should become configurable in the future.
+ let margin = {
+ let opposite =
+ config.columns.count >= 2 && column + 1 == config.columns.count;
+ if opposite { OuterHAlignment::End } else { marker.number_margin }
+ .resolve(config.shared)
+ };
+
+ // Compute the marker's horizontal position. Will be adjusted based on
+ // the maximum number width later.
+ let clearance = marker.number_clearance.resolve(config.shared);
+
+ // Compute the base X position.
+ let x = match margin {
+ // Move the number to the left of the left edge (at 0pt) by the maximum
+ // width and the clearance.
+ FixedAlignment::Start => -max_number_width - clearance,
+ // Move the number to the right edge and add clearance.
+ FixedAlignment::End => output.width() + clearance,
+ // Can't happen due to `OuterHAlignment`.
+ FixedAlignment::Center => unreachable!(),
+ };
+
+ // Determine how much to shift the number due to its alignment.
+ let shift = {
+ let align = marker
+ .number_align
+ .map(|align| align.resolve(config.shared))
+ .unwrap_or_else(|| margin.inv());
+ align.position(max_number_width - frame.width())
+ };
+
+ // Compute the final position of the number and add it to the output.
+ let pos = Point::new(x + shift, y);
+ output.push_frame(pos, frame);
+ }
+
+ Ok(())
+}
+
+/// Creates a frame that resets the line number counter.
+fn layout_line_number_reset(
+ engine: &mut Engine,
+ config: &Config,
+ locator: &mut SplitLocator,
+) -> SourceResult<Frame> {
+ let counter = Counter::of(ParLineMarker::elem());
+ let update = CounterUpdate::Set(CounterState::init(false));
+ let content = counter.update(Span::detached(), update);
+ layout_frame(
+ engine,
+ &content,
+ locator.next(&()),
+ config.shared,
+ Region::new(Axes::splat(Abs::zero()), Axes::splat(false)),
+ )
+}
+
+/// Layout the line number associated with the given line marker.
+///
+/// Produces a counter update and counter display with counter key
+/// `ParLineMarker`. We use `ParLineMarker` as it is an element which is not
+/// exposed to the user and we don't want to expose the line number counter at
+/// the moment, given that its semantics are inconsistent with that of normal
+/// counters (the counter is updated based on height and not on frame order /
+/// layer). When we find a solution to this, we should switch to a counter on
+/// `ParLine` instead, thus exposing the counter as `counter(par.line)` to the
+/// user.
+fn layout_line_number(
+ engine: &mut Engine,
+ config: &Config,
+ locator: &mut SplitLocator,
+ numbering: &Numbering,
+) -> SourceResult<Frame> {
+ let counter = Counter::of(ParLineMarker::elem());
+ let update = CounterUpdate::Step(NonZeroUsize::ONE);
+ let numbering = Smart::Custom(numbering.clone());
+
+ // Combine counter update and display into the content we'll layout.
+ let content = Content::sequence(vec![
+ counter.clone().update(Span::detached(), update),
+ CounterDisplayElem::new(counter, numbering, false).pack(),
+ ]);
+
+ // Layout the number.
+ let mut frame = layout_frame(
+ engine,
+ &content,
+ locator.next(&()),
+ config.shared,
+ Region::new(Axes::splat(Abs::inf()), Axes::splat(false)),
+ )?;
+
+ // Ensure the baseline of the line number aligns with the line's baseline.
+ frame.translate(Point::with_y(-frame.baseline()));
+
+ Ok(frame)
+}
+
+/// Collect all matching elements and their vertical positions in the frame.
+///
+/// On each subframe we encounter, we add that subframe's position to `prev_y`,
+/// until we reach a tag, at which point we add the tag's position and finish.
+/// That gives us the absolute height of the tag from the start of the root
+/// frame.
+fn find_in_frame<T: NativeElement>(frame: &Frame) -> Vec<(Abs, Packed<T>)> {
+ let mut output = vec![];
+ find_in_frame_impl(&mut output, frame, Abs::zero());
+ output
+}
+
+/// Collect all matching elements and their vertical positions in the frames.
+fn find_in_frames<T: NativeElement>(frames: &[Frame]) -> Vec<(Abs, Packed<T>)> {
+ let mut output = vec![];
+ for frame in frames {
+ find_in_frame_impl(&mut output, frame, Abs::zero());
+ }
+ output
+}
+
+fn find_in_frame_impl<T: NativeElement>(
+ output: &mut Vec<(Abs, Packed<T>)>,
+ frame: &Frame,
+ y_offset: Abs,
+) {
+ for (pos, item) in frame.items() {
+ let y = y_offset + pos.y;
+ match item {
+ FrameItem::Group(group) => find_in_frame_impl(output, &group.frame, y),
+ FrameItem::Tag(tag) if tag.kind() == TagKind::Start => {
+ if let Some(elem) = tag.elem().to_packed::<T>() {
+ output.push((y, elem.clone()));
+ }
+ }
+ _ => {}
+ }
+ }
+}
diff --git a/crates/typst/src/layout/flow/distribute.rs b/crates/typst/src/layout/flow/distribute.rs
new file mode 100644
index 00000000..71f9598b
--- /dev/null
+++ b/crates/typst/src/layout/flow/distribute.rs
@@ -0,0 +1,512 @@
+use super::{
+ Child, Composer, FlowResult, LineChild, MultiChild, MultiSpill, PlacedChild,
+ SingleChild, Stop, Work,
+};
+use crate::introspection::Tag;
+use crate::layout::{
+ Abs, Axes, FixedAlignment, Fr, Frame, FrameItem, Point, Region, Regions, Rel, Size,
+};
+use crate::utils::Numeric;
+
+/// Distributes as many children as fit from `composer.work` into the first
+/// region and returns the resulting frame.
+pub fn distribute(composer: &mut Composer, regions: Regions) -> FlowResult<Frame> {
+ let mut distributor = Distributor {
+ composer,
+ regions,
+ items: vec![],
+ sticky: None,
+ stickable: false,
+ };
+ let init = distributor.snapshot();
+ let forced = match distributor.run() {
+ Ok(()) => true,
+ Err(Stop::Finish(forced)) => forced,
+ Err(err) => return Err(err),
+ };
+ let region = Region::new(regions.size, regions.expand);
+ distributor.finalize(region, init, forced)
+}
+
+/// State for distribution.
+///
+/// See [Composer] regarding lifetimes.
+struct Distributor<'a, 'b, 'x, 'y, 'z> {
+ /// The composer that is used to handle insertions.
+ composer: &'z mut Composer<'a, 'b, 'x, 'y>,
+ /// Regions which are continously shrunk as new items are added.
+ regions: Regions<'z>,
+ /// Already laid out items, not yet aligned.
+ items: Vec<Item<'a, 'b>>,
+ /// A snapshot which can be restored to migrate a suffix of sticky blocks to
+ /// the next region.
+ sticky: Option<DistributionSnapshot<'a, 'b>>,
+ /// Whether there was at least one proper block. Otherwise, sticky blocks
+ /// are disabled (or else they'd keep being migrated).
+ stickable: bool,
+}
+
+/// A snapshot of the distribution state.
+struct DistributionSnapshot<'a, 'b> {
+ work: Work<'a, 'b>,
+ items: usize,
+}
+
+/// A laid out item in a distribution.
+enum Item<'a, 'b> {
+ /// Absolute spacing and its weakness level.
+ Abs(Abs, u8),
+ /// Fractional spacing or a fractional block.
+ Fr(Fr, Option<&'b SingleChild<'a>>),
+ /// A frame for a laid out line or block.
+ Frame(Frame, Axes<FixedAlignment>),
+ /// A frame for an absolutely (not floatingly) placed child.
+ Placed(Frame, &'b PlacedChild<'a>),
+}
+
+impl Item<'_, '_> {
+ /// Whether this item should be migrated to the next region if the region
+ /// consists solely of such items.
+ fn migratable(&self) -> bool {
+ match self {
+ Self::Frame(frame, _) => {
+ frame.size().is_zero()
+ && frame.items().all(|(_, item)| {
+ matches!(item, FrameItem::Link(_, _) | FrameItem::Tag(_))
+ })
+ }
+ Self::Placed(_, placed) => !placed.float,
+ _ => false,
+ }
+ }
+}
+
+impl<'a, 'b> Distributor<'a, 'b, '_, '_, '_> {
+ /// Distributes content into the region.
+ fn run(&mut self) -> FlowResult<()> {
+ // First, handle spill of a breakable block.
+ if let Some(spill) = self.composer.work.spill.take() {
+ self.multi_spill(spill)?;
+ }
+
+ // If spill are taken care of, process children until no space is left
+ // or no children are left.
+ while let Some(child) = self.composer.work.head() {
+ self.child(child)?;
+ self.composer.work.advance();
+ }
+
+ Ok(())
+ }
+
+ /// Processes a single child.
+ ///
+ /// - Returns `Ok(())` if the child was successfully processed.
+ /// - Returns `Err(Stop::Finish)` if a region break should be triggered.
+ /// - Returns `Err(Stop::Relayout(_))` if the region needs to be relayouted
+ /// due to an insertion (float/footnote).
+ /// - Returns `Err(Stop::Error(_))` if there was a fatal error.
+ fn child(&mut self, child: &'b Child<'a>) -> FlowResult<()> {
+ match child {
+ Child::Tag(tag) => self.tag(tag),
+ Child::Rel(amount, weakness) => self.rel(*amount, *weakness),
+ Child::Fr(fr) => self.fr(*fr),
+ Child::Line(line) => self.line(line)?,
+ Child::Single(single) => self.single(single)?,
+ Child::Multi(multi) => self.multi(multi)?,
+ Child::Placed(placed) => self.placed(placed)?,
+ Child::Flush => self.flush()?,
+ Child::Break(weak) => self.break_(*weak)?,
+ }
+ Ok(())
+ }
+
+ /// Processes a tag.
+ fn tag(&mut self, tag: &'a Tag) {
+ self.composer.work.tags.push(tag);
+ }
+
+ /// Processes relative spacing.
+ fn rel(&mut self, amount: Rel<Abs>, weakness: u8) {
+ let amount = amount.relative_to(self.regions.base().y);
+ if weakness > 0 && !self.keep_spacing(amount, weakness) {
+ return;
+ }
+
+ self.regions.size.y -= amount;
+ self.items.push(Item::Abs(amount, weakness));
+ }
+
+ /// Processes fractional spacing.
+ fn fr(&mut self, fr: Fr) {
+ self.trim_spacing();
+ self.items.push(Item::Fr(fr, None));
+ }
+
+ /// Decides whether to keep weak spacing based on previous items. If there
+ /// is a preceding weak spacing, it might be patched in place.
+ fn keep_spacing(&mut self, amount: Abs, weakness: u8) -> bool {
+ for item in self.items.iter_mut().rev() {
+ match *item {
+ Item::Abs(prev_amount, prev_weakness @ 1..) => {
+ if weakness <= prev_weakness
+ && (weakness < prev_weakness || amount > prev_amount)
+ {
+ self.regions.size.y -= amount - prev_amount;
+ *item = Item::Abs(amount, weakness);
+ }
+ return false;
+ }
+ Item::Abs(..) | Item::Placed(..) => {}
+ Item::Fr(.., None) => return false,
+ Item::Frame(..) | Item::Fr(.., Some(_)) => return true,
+ }
+ }
+ false
+ }
+
+ /// Trims trailing weak spacing from the items.
+ fn trim_spacing(&mut self) {
+ for (i, item) in self.items.iter().enumerate().rev() {
+ match *item {
+ Item::Abs(amount, 1..) => {
+ self.regions.size.y += amount;
+ self.items.remove(i);
+ break;
+ }
+ Item::Abs(..) | Item::Placed(..) => {}
+ Item::Frame(..) | Item::Fr(..) => break,
+ }
+ }
+ }
+
+ /// The amount of trailing weak spacing.
+ fn weak_spacing(&mut self) -> Abs {
+ for item in self.items.iter().rev() {
+ match *item {
+ Item::Abs(amount, 1..) => return amount,
+ Item::Abs(..) | Item::Placed(..) => {}
+ Item::Frame(..) | Item::Fr(..) => break,
+ }
+ }
+ Abs::zero()
+ }
+
+ /// Processes a line of a paragraph.
+ fn line(&mut self, line: &'b LineChild) -> FlowResult<()> {
+ // If the line doesn't fit and we're allowed to break, finish the
+ // region.
+ if !self.regions.size.y.fits(line.frame.height()) && !self.regions.in_last() {
+ return Err(Stop::Finish(false));
+ }
+
+ // If the line's need, which includes its own height and that of
+ // following lines grouped by widow/orphan prevention, does not fit into
+ // the current region, but does fit into the next region, finish the
+ // region.
+ if !self.regions.size.y.fits(line.need)
+ && self
+ .regions
+ .iter()
+ .nth(1)
+ .is_some_and(|region| region.y.fits(line.need))
+ {
+ return Err(Stop::Finish(false));
+ }
+
+ self.frame(line.frame.clone(), line.align, false, false)
+ }
+
+ /// Processes an unbreakable block.
+ fn single(&mut self, single: &'b SingleChild<'a>) -> FlowResult<()> {
+ // Handle fractionally sized blocks.
+ if let Some(fr) = single.fr {
+ self.items.push(Item::Fr(fr, Some(single)));
+ return Ok(());
+ }
+
+ // Lay out the block.
+ let frame = single.layout(self.composer.engine, self.regions.base())?;
+
+ // If the block doesn't fit and we're allowed to break, finish the
+ // region.
+ if !self.regions.size.y.fits(frame.height()) && !self.regions.in_last() {
+ return Err(Stop::Finish(false));
+ }
+
+ self.frame(frame, single.align, single.sticky, false)
+ }
+
+ /// Processes a breakable block.
+ fn multi(&mut self, multi: &'b MultiChild<'a>) -> FlowResult<()> {
+ // Skip directly if the region is already (over)full. `line` and
+ // `single` implicitly do this through their `fits` checks.
+ if self.regions.is_full() {
+ return Err(Stop::Finish(false));
+ }
+
+ // Lay out the block.
+ let (frame, spill) = multi.layout(self.composer.engine, self.regions)?;
+ self.frame(frame, multi.align, false, true)?;
+
+ // If the block didn't fully fit into the current region, save it into
+ // the `spill` and finish the region.
+ if let Some(spill) = spill {
+ self.composer.work.spill = Some(spill);
+ self.composer.work.advance();
+ return Err(Stop::Finish(false));
+ }
+
+ Ok(())
+ }
+
+ /// Processes spillover from a breakable block.
+ fn multi_spill(&mut self, spill: MultiSpill<'a, 'b>) -> FlowResult<()> {
+ // Skip directly if the region is already (over)full.
+ if self.regions.is_full() {
+ self.composer.work.spill = Some(spill);
+ return Err(Stop::Finish(false));
+ }
+
+ // Lay out the spilled remains.
+ let align = spill.align();
+ let (frame, spill) = spill.layout(self.composer.engine, self.regions)?;
+ self.frame(frame, align, false, true)?;
+
+ // If there's still more, save it into the `spill` and finish the
+ // region.
+ if let Some(spill) = spill {
+ self.composer.work.spill = Some(spill);
+ return Err(Stop::Finish(false));
+ }
+
+ Ok(())
+ }
+
+ /// Processes an in-flow frame, generated from a line or block.
+ fn frame(
+ &mut self,
+ mut frame: Frame,
+ align: Axes<FixedAlignment>,
+ sticky: bool,
+ breakable: bool,
+ ) -> FlowResult<()> {
+ if sticky {
+ // If the frame is sticky and we haven't remember a preceding sticky
+ // element, make a checkpoint which we can restore should we end on
+ // this sticky element.
+ if self.stickable && self.sticky.is_none() {
+ self.sticky = Some(self.snapshot());
+ }
+ } else if !frame.is_empty() {
+ // If the frame isn't sticky, we can forget a previous snapshot.
+ self.stickable = true;
+ self.sticky = None;
+ }
+
+ if !frame.is_empty() {
+ // Drain tags.
+ let tags = &mut self.composer.work.tags;
+ if !tags.is_empty() {
+ frame.prepend_multiple(
+ tags.iter().map(|&tag| (Point::zero(), FrameItem::Tag(tag.clone()))),
+ );
+ }
+
+ // Handle footnotes.
+ self.composer
+ .footnotes(&self.regions, &frame, frame.height(), breakable)?;
+
+ // Clear the drained tags _after_ the footnotes are handled because
+ // a [`Stop::Finish`] could otherwise lose them.
+ self.composer.work.tags.clear();
+ }
+
+ // Push an item for the frame.
+ self.regions.size.y -= frame.height();
+ self.items.push(Item::Frame(frame, align));
+ Ok(())
+ }
+
+ /// Processes an absolutely or floatingly placed child.
+ fn placed(&mut self, placed: &'b PlacedChild<'a>) -> FlowResult<()> {
+ if placed.float {
+ // If the element is floatingly placed, let the composer handle it.
+ // It might require relayout because the area available for
+ // distribution shrinks. We make the spacing occupied by weak
+ // spacing temporarily available again because it can collapse if it
+ // ends up at a break due to the float.
+ let weak_spacing = self.weak_spacing();
+ self.regions.size.y += weak_spacing;
+ self.composer.float(placed, &self.regions, self.items.is_empty())?;
+ self.regions.size.y -= weak_spacing;
+ } else {
+ let frame = placed.layout(self.composer.engine, self.regions.base())?;
+ self.composer.footnotes(&self.regions, &frame, Abs::zero(), true)?;
+ self.items.push(Item::Placed(frame, placed));
+ }
+ Ok(())
+ }
+
+ /// Processes a float flush.
+ fn flush(&mut self) -> FlowResult<()> {
+ // If there are still pending floats, finish the region instead of
+ // adding more content to it.
+ if !self.composer.work.floats.is_empty() {
+ return Err(Stop::Finish(false));
+ }
+ Ok(())
+ }
+
+ /// Processes a column break.
+ fn break_(&mut self, weak: bool) -> FlowResult<()> {
+ // If there is a region to break into, break into it.
+ if (!weak || !self.items.is_empty())
+ && (!self.regions.backlog.is_empty() || self.regions.last.is_some())
+ {
+ self.composer.work.advance();
+ return Err(Stop::Finish(true));
+ }
+ Ok(())
+ }
+
+ /// Arranges the produced items into an output frame.
+ ///
+ /// This performs alignment and resolves fractional spacing and blocks.
+ fn finalize(
+ mut self,
+ region: Region,
+ init: DistributionSnapshot<'a, 'b>,
+ forced: bool,
+ ) -> FlowResult<Frame> {
+ if !forced {
+ if !self.items.is_empty() && self.items.iter().all(Item::migratable) {
+ // Restore the initial state of all items are migratable.
+ self.restore(init);
+ } else {
+ // If we ended on a sticky block, but are not yet at the end of
+ // the flow, restore the saved checkpoint to move the sticky
+ // suffix to the next region.
+ if let Some(snapshot) = self.sticky.take() {
+ self.restore(snapshot)
+ }
+ }
+ }
+
+ self.trim_spacing();
+
+ let mut frs = Fr::zero();
+ let mut used = Size::zero();
+
+ // Determine the amount of used space and the sum of fractionals.
+ for item in &self.items {
+ match item {
+ Item::Abs(v, _) => used.y += *v,
+ Item::Fr(v, _) => frs += *v,
+ Item::Frame(frame, _) => {
+ used.y += frame.height();
+ used.x.set_max(frame.width());
+ }
+ Item::Placed(..) => {}
+ }
+ }
+
+ // When we have fractional spacing, occupy the remaining space with it.
+ let mut fr_space = Abs::zero();
+ let mut fr_frames = vec![];
+ if frs.get() > 0.0 && region.size.y.is_finite() {
+ fr_space = region.size.y - used.y;
+ used.y = region.size.y;
+
+ // Lay out fractionally sized blocks.
+ for item in &self.items {
+ let Item::Fr(v, Some(single)) = item else { continue };
+ let length = v.share(frs, fr_space);
+ let base = Size::new(region.size.x, length);
+ let frame = single.layout(self.composer.engine, base)?;
+ used.x.set_max(frame.width());
+ fr_frames.push(frame);
+ }
+ }
+
+ // Also consider the width of insertions for alignment.
+ if !region.expand.x {
+ used.x.set_max(self.composer.insertion_width());
+ }
+
+ // Determine the region's size.
+ let size = region.expand.select(region.size, used.min(region.size));
+
+ let mut output = Frame::soft(size);
+ let mut ruler = FixedAlignment::Start;
+ let mut offset = Abs::zero();
+ let mut fr_frames = fr_frames.into_iter();
+
+ // Position all items.
+ for item in self.items {
+ match item {
+ Item::Abs(v, _) => {
+ offset += v;
+ }
+ Item::Fr(v, single) => {
+ let length = v.share(frs, fr_space);
+ if let Some(single) = single {
+ let frame = fr_frames.next().unwrap();
+ let x = single.align.x.position(size.x - frame.width());
+ let pos = Point::new(x, offset);
+ output.push_frame(pos, frame);
+ }
+ offset += length;
+ }
+ Item::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);
+ }
+ Item::Placed(frame, placed) => {
+ let x = placed.align_x.position(size.x - frame.width());
+ let y = match placed.align_y.unwrap_or_default() {
+ Some(align) => align.position(size.y - frame.height()),
+ _ => offset + ruler.position(size.y - used.y),
+ };
+
+ let pos = Point::new(x, y)
+ + placed.delta.zip_map(size, Rel::relative_to).to_point();
+
+ output.push_frame(pos, frame);
+ }
+ }
+ }
+
+ // If this is the very end of the flow, drain trailing tags.
+ if forced && !self.composer.work.tags.is_empty() {
+ let tags = &mut self.composer.work.tags;
+ let pos = Point::with_y(offset);
+ output.push_multiple(
+ tags.iter().map(|&tag| (pos, FrameItem::Tag(tag.clone()))),
+ );
+ tags.clear();
+ }
+
+ Ok(output)
+ }
+
+ /// Create a snapshot of the work and items.
+ fn snapshot(&self) -> DistributionSnapshot<'a, 'b> {
+ DistributionSnapshot {
+ work: self.composer.work.clone(),
+ items: self.items.len(),
+ }
+ }
+
+ /// Restore a snapshot of the work and items.
+ fn restore(&mut self, snapshot: DistributionSnapshot<'a, 'b>) {
+ *self.composer.work = snapshot.work;
+ self.items.truncate(snapshot.items);
+ }
+}
diff --git a/crates/typst/src/layout/flow/mod.rs b/crates/typst/src/layout/flow/mod.rs
index 351ef6b1..2491ef7c 100644
--- a/crates/typst/src/layout/flow/mod.rs
+++ b/crates/typst/src/layout/flow/mod.rs
@@ -1,41 +1,40 @@
-//! Layout of content
-//! - at the top-level, into a [`Document`].
-//! - inside of a container, into a [`Frame`] or [`Fragment`].
+//! Layout of content into a [`Frame`] or [`Fragment`].
mod collect;
+mod compose;
+mod distribute;
use std::collections::HashSet;
use std::num::NonZeroUsize;
+use std::rc::Rc;
use bumpalo::Bump;
use comemo::{Track, Tracked, TrackedMut};
+use ecow::EcoVec;
-use self::collect::{collect, BlockChild, Child, LineChild, PlacedChild};
-use crate::diag::{bail, At, SourceResult};
-use crate::engine::{Engine, Route, Sink, Traced};
-use crate::foundations::{
- Content, NativeElement, Packed, Resolve, SequenceElem, Smart, StyleChain,
+use self::collect::{
+ collect, Child, LineChild, MultiChild, MultiSpill, PlacedChild, SingleChild,
};
+use self::compose::{compose, Composer};
+use self::distribute::distribute;
+use crate::diag::{bail, At, SourceDiagnostic, SourceResult};
+use crate::engine::{Engine, Route, Sink, Traced};
+use crate::foundations::{Content, Packed, StyleChain};
use crate::introspection::{
- Counter, CounterDisplayElem, CounterKey, CounterState, CounterUpdate, Introspector,
- Location, Locator, LocatorLink, SplitLocator, Tag,
+ Introspector, Location, Locator, LocatorLink, SplitLocator, Tag,
};
use crate::layout::{
- Abs, Axes, BlockElem, Dir, FixedAlignment, Fr, Fragment, Frame, FrameItem,
- OuterHAlignment, Point, Region, Regions, Rel, Size,
-};
-use crate::model::{
- FootnoteElem, FootnoteEntry, ParLine, ParLineMarker, ParLineNumberingScope,
+ Abs, Dir, Fragment, Frame, PlacementScope, Region, Regions, Rel, Size,
};
+use crate::model::{FootnoteElem, FootnoteEntry};
use crate::realize::{realize, Arenas, Pair, RealizationKind};
-use crate::syntax::Span;
use crate::text::TextElem;
use crate::utils::{NonZeroExt, Numeric};
use crate::World;
-/// Layout content into multiple regions.
+/// Lays out content into multiple regions.
///
-/// When just layouting into a single region, prefer [`layout_frame`].
+/// When laying out into just one region, prefer [`layout_frame`].
pub fn layout_fragment(
engine: &mut Engine,
content: &Content,
@@ -58,12 +57,10 @@ pub fn layout_fragment(
)
}
-/// Layout content into regions with columns.
+/// Lays out content into regions with columns.
///
-/// For now, this just invokes normal layout on cycled smaller regions. However,
-/// in the future, columns will be able to interact (e.g. through floating
-/// figures), so this is already factored out because it'll be conceptually
-/// different from just layouting into more smaller regions.
+/// This is different from just laying out into column-sized regions as the
+/// columns can interact due to page-scoped placed elements.
pub fn layout_fragment_with_columns(
engine: &mut Engine,
content: &Content,
@@ -88,7 +85,7 @@ pub fn layout_fragment_with_columns(
)
}
-/// Layout content into a single region.
+/// Lays out content into a single region, producing a single frame.
pub fn layout_frame(
engine: &mut Engine,
content: &Content,
@@ -116,6 +113,13 @@ fn layout_fragment_impl(
columns: NonZeroUsize,
column_gutter: Rel<Abs>,
) -> SourceResult<Fragment> {
+ if !regions.size.x.is_finite() && regions.expand.x {
+ bail!(content.span(), "cannot expand into infinite width");
+ }
+ if !regions.size.y.is_finite() && regions.expand.y {
+ bail!(content.span(), "cannot expand into infinite height");
+ }
+
let link = LocatorLink::new(locator);
let mut locator = Locator::link(&link).split();
let mut engine = Engine {
@@ -140,1108 +144,219 @@ fn layout_fragment_impl(
layout_flow(
&mut engine,
- &arenas.bump,
&children,
&mut locator,
styles,
regions,
columns,
column_gutter,
- content.span(),
+ false,
)
}
-/// Layout flow content.
+/// Lays out realized content into regions, potentially with columns.
#[allow(clippy::too_many_arguments)]
pub(crate) fn layout_flow(
engine: &mut Engine,
- bump: &Bump,
children: &[Pair],
locator: &mut SplitLocator,
shared: StyleChain,
- regions: Regions,
+ mut regions: Regions,
columns: NonZeroUsize,
column_gutter: Rel<Abs>,
- span: Span,
+ root: bool,
) -> SourceResult<Fragment> {
- // Separating the infinite space into infinite columns does not make
- // much sense.
- let mut columns = columns.get();
- if !regions.size.x.is_finite() {
- columns = 1;
- }
-
- // Determine the width of the gutter and each column.
- let column_gutter = column_gutter.relative_to(regions.base().x);
-
- let backlog: Vec<Abs>;
- let mut pod = if columns > 1 {
- backlog = std::iter::once(&regions.size.y)
- .chain(regions.backlog)
- .flat_map(|&height| std::iter::repeat(height).take(columns))
- .skip(1)
- .collect();
-
- let width =
- (regions.size.x - column_gutter * (columns - 1) as f64) / columns as f64;
-
- // Create the pod regions.
- 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,
- }
- } else {
- regions
- };
-
- // The children aren't root.
- pod.root = false;
-
- // Check whether we have just a single multiple-layoutable element. In
- // that case, we do not set `expand.y` to `false`, but rather keep it at
- // its original value (since that element can take the full space).
- //
- // Consider the following code: `block(height: 5cm, pad(10pt,
- // align(bottom, ..)))`. Thanks to the code below, the expansion will be
- // passed all the way through the block & pad and reach the innermost
- // flow, so that things are properly bottom-aligned.
- let mut alone = false;
- if let [(child, _)] = children {
- alone = child.is::<BlockElem>();
- }
-
- // Disable vertical expansion when there are multiple or not directly
- // layoutable children.
- if !alone {
- pod.expand.y = false;
- }
-
- let children =
- collect(engine, bump, children, locator.next(&()), pod.base(), pod.expand.x)?;
-
- let layouter = FlowLayouter {
- engine,
- span,
- root: regions.root,
- locator,
+ // Prepare configuration that is shared across the whole flow.
+ let config = Config {
+ root,
shared,
- columns,
- column_gutter,
- regions: pod,
- expand: regions.expand,
- initial: pod.size,
- items: vec![],
- pending_tags: vec![],
- pending_floats: vec![],
- has_footnotes: false,
- footnote_config: FootnoteConfig {
+ columns: {
+ let mut count = columns.get();
+ if !regions.size.x.is_finite() {
+ count = 1;
+ }
+
+ let gutter = column_gutter.relative_to(regions.base().x);
+ let width = (regions.size.x - gutter * (count - 1) as f64) / count as f64;
+ let dir = TextElem::dir_in(shared);
+ ColumnConfig { count, width, gutter, dir }
+ },
+ footnote: FootnoteConfig {
separator: FootnoteEntry::separator_in(shared),
clearance: FootnoteEntry::clearance_in(shared),
gap: FootnoteEntry::gap_in(shared),
+ expand: regions.expand.x,
},
- visited_footnotes: HashSet::new(),
- finished: vec![],
};
- layouter.layout(&children, regions)
-}
-
-/// Layouts a collection of block-level elements.
-struct FlowLayouter<'a, 'b, 'x, 'y> {
- /// The engine.
- engine: &'a mut Engine<'x>,
- /// A span to use for errors.
- span: Span,
- /// Whether this is the root flow.
- root: bool,
- /// Provides unique locations to the flow's children.
- locator: &'a mut SplitLocator<'y>,
- /// The shared styles.
- shared: StyleChain<'a>,
- /// The number of columns.
- columns: usize,
- /// The gutter between columns.
- column_gutter: Abs,
- /// The regions to layout children into. These already incorporate the
- /// columns.
- regions: Regions<'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,
- /// Spacing and layouted blocks for the current region.
- items: Vec<FlowItem<'a, 'b>>,
- /// A queue of tags that will be attached to the next frame.
- pending_tags: Vec<&'a Tag>,
- /// A queue of floating elements.
- pending_floats: Vec<FlowItem<'a, 'b>>,
- /// Whether we have any footnotes in the current region.
- has_footnotes: bool,
- /// Footnote configuration.
- footnote_config: FootnoteConfig,
- /// Footnotes that we have already processed.
- visited_footnotes: HashSet<Location>,
- /// Finished frames for previous regions.
- finished: Vec<Frame>,
-}
-
-/// Cached footnote configuration.
-struct FootnoteConfig {
- separator: Content,
- clearance: Abs,
- gap: Abs,
-}
-
-/// Information needed to generate a line number.
-struct CollectedParLine {
- y: Abs,
- marker: Packed<ParLineMarker>,
-}
-
-/// A prepared item in a flow layout.
-enum FlowItem<'a, 'b> {
- /// Spacing between other items and its weakness level.
- Absolute(Abs, u8),
- /// Fractional spacing between other items.
- Fractional(Fr),
- /// A frame for a layouted block.
- Frame {
- /// The frame itself.
- frame: Frame,
- /// How to align the frame.
- align: Axes<FixedAlignment>,
- /// Whether the frame sticks to the item after it (for orphan prevention).
- sticky: bool,
- /// Whether the frame comes from a rootable block, which may be laid
- /// out as a root flow and thus display its own line numbers.
- /// Therefore, we do not display line numbers for these frames.
- ///
- /// Currently, this is only used by columns.
- rootable: bool,
- /// Whether the frame is movable; that is, kept together with its
- /// footnotes.
- ///
- /// This is true for frames created by paragraphs and
- /// [`BlockElem::single_layouter`] elements.
- movable: bool,
- },
- /// An absolutely placed frame.
- Placed(&'b PlacedChild<'a>, Frame, Smart<Option<FixedAlignment>>),
- /// A footnote frame (can also be the separator).
- Footnote(Frame),
-}
-
-impl FlowItem<'_, '_> {
- /// Whether this item is out-of-flow.
- ///
- /// Out-of-flow items are guaranteed to have a [zero size][Size::zero()].
- fn is_out_of_flow(&self) -> bool {
- match self {
- Self::Placed(placed, ..) => !placed.float,
- Self::Frame { frame, .. } => {
- frame.size().is_zero()
- && frame.items().all(|(_, item)| {
- matches!(item, FrameItem::Link(_, _) | FrameItem::Tag(_))
- })
- }
- _ => false,
- }
- }
-}
-
-impl<'a, 'b, 'x, 'y> FlowLayouter<'a, 'b, 'x, 'y> {
- /// Layout the flow.
- fn layout(
- mut self,
- children: &'b [Child<'a>],
- regions: Regions,
- ) -> SourceResult<Fragment> {
- for child in children {
- match child {
- Child::Tag(tag) => {
- self.pending_tags.push(tag);
- }
- Child::Rel(amount, weakness) => {
- self.handle_rel(*amount, *weakness)?;
- }
- Child::Fr(fr) => {
- self.handle_item(FlowItem::Fractional(*fr))?;
- }
- Child::Line(line) => {
- self.handle_line(line)?;
- }
- Child::Block(block) => {
- self.handle_block(block)?;
- }
- Child::Placed(placed) => {
- self.handle_placed(placed)?;
- }
- Child::Break(weak) => {
- self.handle_colbreak(*weak)?;
- }
- Child::Flush => {
- self.handle_flush()?;
- }
- }
- }
-
- self.finish(regions)
- }
-
- /// Layout relative spacing, handling weakness.
- fn handle_rel(&mut self, amount: Rel<Abs>, weakness: u8) -> SourceResult<()> {
- self.handle_item(FlowItem::Absolute(
- // Resolve the spacing relative to the current base height.
- amount.relative_to(self.initial.y),
- weakness,
- ))
- }
-
- /// Layout a paragraph.
- fn handle_line(&mut self, line: &LineChild) -> SourceResult<()> {
- // If the first line doesn’t fit in this region, then defer any
- // previous sticky frame to the next region (if available)
- if !self.regions.in_last()
- && !self.regions.size.y.fits(line.need)
- && self
- .regions
- .iter()
- .nth(1)
- .is_some_and(|region| region.y.fits(line.need))
- {
- self.finish_region_with_migration()?;
- }
-
- let mut frame = line.frame.clone();
- self.drain_tag(&mut frame);
- self.handle_item(FlowItem::Frame {
- frame,
- align: line.align,
- sticky: false,
- rootable: false,
- movable: true,
- })
- }
-
- /// Layout into multiple regions.
- fn handle_block(&mut self, block: &BlockChild) -> SourceResult<()> {
- // If the block is "rootable" it may host footnotes. In that case, we
- // defer rootness to it temporarily. We disable our own rootness to
- // prevent duplicate footnotes.
- let is_root = self.root;
- if is_root && block.rootable {
- self.root = false;
- self.regions.root = true;
- }
-
- // Skip directly if region is already full.
- if self.regions.is_full() {
- self.finish_region(false)?;
- }
-
- // Layout the block itself.
- let fragment = block.layout(self.engine, self.regions)?;
-
- let mut notes = Vec::new();
- for (i, mut frame) in fragment.into_iter().enumerate() {
- // Find footnotes in the frame.
- if self.root {
- self.collect_footnotes(&mut notes, &frame);
- }
-
- if i > 0 {
- self.finish_region(false)?;
- }
-
- self.drain_tag(&mut frame);
- self.handle_item(FlowItem::Frame {
- frame,
- align: block.align,
- sticky: block.sticky,
- rootable: block.rootable,
- movable: false,
- })?;
- }
-
- self.try_handle_footnotes(notes)?;
-
- self.root = is_root;
- self.regions.root = false;
-
- Ok(())
- }
-
- /// Layout a placed element.
- fn handle_placed(&mut self, placed: &'b PlacedChild<'a>) -> SourceResult<()> {
- let frame = placed.layout(self.engine, self.regions.base())?;
- self.handle_item(FlowItem::Placed(placed, frame, placed.align_y))
- }
-
- /// Layout a column break.
- fn handle_colbreak(&mut self, _weak: bool) -> SourceResult<()> {
- // If there is still an available region, skip to it.
- // TODO: Turn this into a region abstraction.
- if !self.regions.backlog.is_empty() || self.regions.last.is_some() {
- self.finish_region(true)?;
- }
- Ok(())
- }
-
- /// Lays out all floating elements before continuing with other content.
- fn handle_flush(&mut self) -> SourceResult<()> {
- for item in std::mem::take(&mut self.pending_floats) {
- self.handle_item(item)?;
- }
- while !self.pending_floats.is_empty() {
- self.finish_region(false)?;
- }
- Ok(())
- }
-
- /// Layout a finished frame.
- fn handle_item(&mut self, mut item: FlowItem<'a, 'b>) -> SourceResult<()> {
- match item {
- FlowItem::Absolute(v, weakness) => {
- if weakness > 0 {
- let mut has_frame = false;
- for prev in self.items.iter_mut().rev() {
- match prev {
- FlowItem::Frame { .. } => {
- has_frame = true;
- break;
- }
- FlowItem::Absolute(prev_amount, prev_level)
- if *prev_level > 0 =>
- {
- if *prev_level >= weakness {
- let diff = v - *prev_amount;
- if *prev_level > weakness || diff > Abs::zero() {
- self.regions.size.y -= diff;
- *prev = item;
- }
- }
- return Ok(());
- }
- FlowItem::Fractional(_) => return Ok(()),
- _ => {}
- }
- }
- if !has_frame {
- return Ok(());
- }
- }
- self.regions.size.y -= v;
- }
- FlowItem::Fractional(..) => {
- self.trim_weak_spacing();
- }
- FlowItem::Frame { ref frame, movable, .. } => {
- let height = frame.height();
- while !self.regions.size.y.fits(height) && !self.regions.in_last() {
- self.finish_region(false)?;
- }
-
- let in_last = self.regions.in_last();
- self.regions.size.y -= height;
- if self.root && movable {
- let mut notes = Vec::new();
- self.collect_footnotes(&mut notes, frame);
- self.items.push(item);
-
- // When we are already in_last, we can directly force the
- // footnotes.
- if !self.handle_footnotes(&mut notes, true, in_last)? {
- let item = self.items.pop();
- self.finish_region(false)?;
- self.items.extend(item);
- self.regions.size.y -= height;
- self.handle_footnotes(&mut notes, true, true)?;
- }
- return Ok(());
- }
- }
- FlowItem::Placed(placed, ..) if !placed.float => {}
- FlowItem::Placed(placed, ref mut frame, ref mut align_y) => {
- // If there is a queued float in front or if the float doesn't
- // fit, queue it for the next region.
- if !self.pending_floats.is_empty()
- || (!self.regions.size.y.fits(frame.height() + placed.clearance)
- && !self.regions.in_last())
- {
- self.pending_floats.push(item);
- return Ok(());
- }
-
- // Select the closer placement, top or bottom.
- if align_y.is_auto() {
- // When the figure's vertical midpoint would be above the
- // middle of the page if it were layouted in-flow, we use
- // top alignment. Otherwise, we use bottom alignment.
- let used = self.regions.full - self.regions.size.y;
- let half = (frame.height() + placed.clearance) / 2.0;
- let ratio = (used + half) / self.regions.full;
- let better_align = if ratio <= 0.5 {
- FixedAlignment::Start
- } else {
- FixedAlignment::End
- };
- *align_y = Smart::Custom(Some(better_align));
- }
-
- // Add some clearance so that the float doesn't touch the main
- // content.
- frame.size_mut().y += placed.clearance;
- if *align_y == Smart::Custom(Some(FixedAlignment::End)) {
- frame.translate(Point::with_y(placed.clearance));
- }
-
- self.regions.size.y -= frame.height();
-
- // Find footnotes in the frame.
- if self.root {
- let mut notes = vec![];
- self.collect_footnotes(&mut notes, frame);
- self.try_handle_footnotes(notes)?;
- }
- }
- FlowItem::Footnote(_) => {}
- }
-
- self.items.push(item);
- Ok(())
- }
-
- /// Trim trailing weak spacing from the items.
- fn trim_weak_spacing(&mut self) {
- for (i, item) in self.items.iter().enumerate().rev() {
- match item {
- FlowItem::Absolute(amount, 1..) => {
- self.regions.size.y += *amount;
- self.items.remove(i);
- return;
- }
- FlowItem::Frame { .. } => return,
- _ => {}
- }
- }
- }
-
- /// Attach currently pending metadata to the frame.
- fn drain_tag(&mut self, frame: &mut Frame) {
- if !self.pending_tags.is_empty() && !frame.is_empty() {
- frame.prepend_multiple(
- self.pending_tags
- .drain(..)
- .map(|tag| (Point::zero(), FrameItem::Tag(tag.clone()))),
- );
- }
- }
-
- /// Finisht the region, migrating all sticky items to the next one.
- ///
- /// Returns whether we migrated into a last region.
- fn finish_region_with_migration(&mut self) -> SourceResult<()> {
- // Find the suffix of sticky items.
- 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,
- }
- }
-
- let carry: Vec<_> = self.items.drain(sticky..).collect();
- self.finish_region(false)?;
-
- for item in carry {
- self.handle_item(item)?;
- }
-
- Ok(())
- }
-
- /// Finish the frame for one region.
- ///
- /// Set `force` to `true` to allow creating a frame for out-of-flow elements
- /// only (this is used to force the creation of a frame in case the
- /// remaining elements are all out-of-flow).
- fn finish_region(&mut self, force: bool) -> SourceResult<()> {
- self.trim_weak_spacing();
-
- // Early return if we don't have any relevant items.
- if !force
- && !self.items.is_empty()
- && self.items.iter().all(FlowItem::is_out_of_flow)
- {
- // Run line number layout here even though we have no line numbers
- // to ensure we reset line numbers at the start of the page if
- // requested, which is still necessary if e.g. the first column is
- // empty when the others aren't.
- let mut output = Frame::soft(self.initial);
- self.layout_line_numbers(&mut output, self.initial, vec![])?;
-
- self.finished.push(output);
- self.regions.next();
- self.initial = self.regions.size;
- return Ok(());
- }
-
- // 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(placed, ..) if !placed.float => {}
- FlowItem::Placed(_, frame, align_y) => match align_y {
- Smart::Custom(Some(FixedAlignment::Start)) => {
- float_top_height += frame.height()
- }
- Smart::Custom(Some(FixedAlignment::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;
- }
-
- if !self.regions.size.x.is_finite() && self.expand.x {
- bail!(self.span, "cannot expand into infinite width");
- }
- if !self.regions.size.y.is_finite() && self.expand.y {
- bail!(self.span, "cannot expand into infinite height");
- }
-
- let mut output = Frame::soft(size);
- let mut ruler = FixedAlignment::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();
-
- let mut lines: Vec<CollectedParLine> = vec![];
-
- // 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;
- let length = v.share(fr, remaining);
- offset += length;
- }
- FlowItem::Frame { frame, align, rootable, .. } => {
- 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();
-
- // Do not display line numbers for frames coming from
- // rootable blocks as they will display their own line
- // numbers when laid out as a root flow themselves.
- if self.root && !rootable {
- collect_par_lines(&mut lines, &frame, pos, Abs::zero());
- }
-
- output.push_frame(pos, frame);
- }
- FlowItem::Placed(placed, frame, align_y) => {
- let x = placed.align_x.position(size.x - frame.width());
- let y = if placed.float {
- match align_y {
- Smart::Custom(Some(FixedAlignment::Start)) => {
- let y = float_top_offset;
- float_top_offset += frame.height();
- y
- }
- Smart::Custom(Some(FixedAlignment::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 align_y {
- Smart::Custom(Some(align)) => {
- align.position(size.y - frame.height())
- }
- _ => offset + ruler.position(size.y - used.y),
- }
- };
-
- let pos = Point::new(x, y)
- + placed.delta.zip_map(size, Rel::relative_to).to_point();
-
- if self.root {
- collect_par_lines(&mut lines, &frame, pos, Abs::zero());
- }
-
- 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);
- }
- }
- }
-
- // Sort, deduplicate and layout line numbers.
- //
- // We do this after placing all frames since they might not necessarily
- // be ordered by height (e.g. you can have a `place(bottom)` followed
- // by a paragraph, but the paragraph appears at the top), so we buffer
- // all line numbers to later sort and deduplicate them based on how
- // close they are to each other in `layout_line_numbers`.
- self.layout_line_numbers(&mut output, size, lines)?;
+ // Collect the elements into pre-processed children. These are much easier
+ // to handle than the raw elements.
+ let bump = Bump::new();
+ let children = collect(
+ engine,
+ &bump,
+ children,
+ locator.next(&()),
+ Size::new(config.columns.width, regions.full),
+ regions.expand.x,
+ )?;
- if force && !self.pending_tags.is_empty() {
- let pos = Point::with_y(offset);
- output.push_multiple(
- self.pending_tags
- .drain(..)
- .map(|tag| (pos, FrameItem::Tag(tag.clone()))),
- );
- }
+ let mut work = Work::new(&children);
+ let mut finished = vec![];
- // Advance to the next region.
- self.finished.push(output);
- self.regions.next();
- self.initial = self.regions.size;
- self.has_footnotes = false;
+ // This loop runs once per region produced by the flow layout.
+ loop {
+ let frame = compose(engine, &mut work, &config, locator.next(&()), regions)?;
+ finished.push(frame);
- // Try to place floats into the next region.
- for item in std::mem::take(&mut self.pending_floats) {
- self.handle_item(item)?;
+ // Terminate the loop when everything is processed, though draining the
+ // backlog if necessary.
+ if work.done() && (!regions.expand.y || regions.backlog.is_empty()) {
+ break;
}
- Ok(())
+ regions.next();
}
- /// Finish layouting and return the resulting fragment.
- fn finish(mut self, regions: Regions) -> SourceResult<Fragment> {
- if self.expand.y {
- while !self.regions.backlog.is_empty() {
- self.finish_region(true)?;
- }
- }
-
- self.finish_region(true)?;
- while !self.items.is_empty() {
- self.finish_region(true)?;
- }
-
- if self.columns == 1 {
- return Ok(Fragment::frames(self.finished));
- }
-
- // Stitch together the column for each region.
- let dir = TextElem::dir_in(self.shared);
- let total = (self.finished.len() as f32 / self.columns as f32).ceil() as usize;
-
- let mut collected = vec![];
- let mut iter = self.finished.into_iter();
- for region in regions.iter().take(total) {
- // 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..self.columns {
- let Some(frame) = iter.next() else { break };
- if !regions.expand.y {
- output.size_mut().y.set_max(frame.height());
- }
+ Ok(Fragment::frames(finished))
+}
- let width = frame.width();
- let x = if dir == Dir::LTR {
- cursor
- } else {
- regions.size.x - cursor - width
- };
+/// The work that is left to do by flow layout.
+///
+/// The lifetimes 'a and 'b are used across flow layout:
+/// - 'a is that of the content coming out of realization
+/// - 'b is that of the collected/prepared children
+#[derive(Clone)]
+struct Work<'a, 'b> {
+ /// Children that we haven't processed yet. This slice shrinks over time.
+ children: &'b [Child<'a>],
+ /// Leftovers from a breakable block.
+ spill: Option<MultiSpill<'a, 'b>>,
+ /// Queued floats that didn't fit in previous regions.
+ floats: EcoVec<&'b PlacedChild<'a>>,
+ /// Queued footnotes that didn't fit in previous regions.
+ footnotes: EcoVec<Packed<FootnoteElem>>,
+ /// Spilled frames of a footnote that didn't fully fit. Similar to `spill`.
+ footnote_spill: Option<std::vec::IntoIter<Frame>>,
+ /// Queued tags that will be attached to the next frame.
+ tags: EcoVec<&'a Tag>,
+ /// Identifies floats and footnotes that can be skipped if visited because
+ /// they were already handled and incorporated as column or page level
+ /// insertions.
+ skips: Rc<HashSet<Skip>>,
+}
- output.push_frame(Point::with_x(x), frame);
- cursor += width + self.column_gutter;
- }
+/// Identifies an element that that can be skipped if visited because it was
+/// already processed.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+enum Skip {
+ /// Uniquely identifies a placed elements. We can't use a [`Location`]
+ /// because `PlaceElem` is not currently locatable.
+ Placed(usize),
+ /// Uniquely identifies a footnote.
+ Footnote(Location),
+}
- collected.push(output);
+impl<'a, 'b> Work<'a, 'b> {
+ /// Create the initial work state from a list of children.
+ fn new(children: &'b [Child<'a>]) -> Self {
+ Self {
+ children,
+ spill: None,
+ floats: EcoVec::new(),
+ footnotes: EcoVec::new(),
+ footnote_spill: None,
+ tags: EcoVec::new(),
+ skips: Rc::new(HashSet::new()),
}
-
- Ok(Fragment::frames(collected))
}
- /// Tries to process all footnotes in the frame, placing them
- /// in the next region if they could not be placed in the current
- /// one.
- fn try_handle_footnotes(
- &mut self,
- mut notes: Vec<Packed<FootnoteElem>>,
- ) -> SourceResult<()> {
- // When we are already in_last, we can directly force the
- // footnotes.
- if self.root
- && !self.handle_footnotes(&mut notes, false, self.regions.in_last())?
- {
- self.finish_region(false)?;
- self.handle_footnotes(&mut notes, false, true)?;
- }
- Ok(())
+ /// Get the first unprocessed child, from the start of the slice.
+ fn head(&self) -> Option<&'b Child<'a>> {
+ self.children.first()
}
- /// Processes all footnotes in the frame.
- ///
- /// Returns true if the footnote entries fit in the allotted
- /// regions.
- fn handle_footnotes(
- &mut self,
- notes: &mut Vec<Packed<FootnoteElem>>,
- movable: bool,
- force: bool,
- ) -> SourceResult<bool> {
- let prev_notes_len = notes.len();
- let prev_items_len = self.items.len();
- let prev_size = self.regions.size;
- let prev_has_footnotes = self.has_footnotes;
-
- // 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()?;
- }
-
- self.regions.size.y -= self.footnote_config.gap;
- let frames = layout_fragment(
- self.engine,
- &FootnoteEntry::new(notes[k].clone()).pack(),
- Locator::synthesize(notes[k].location().unwrap()),
- self.shared,
- 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().is_some_and(Frame::is_empty)
- {
- // Undo everything.
- notes.truncate(prev_notes_len);
- self.items.truncate(prev_items_len);
- self.regions.size = prev_size;
- self.has_footnotes = prev_has_footnotes;
- return Ok(false);
- }
-
- let prev = notes.len();
- for (i, frame) in frames.into_iter().enumerate() {
- self.collect_footnotes(notes, &frame);
- if i > 0 {
- self.finish_region(false)?;
- self.layout_footnote_separator()?;
- 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)
+ /// Mark the `head()` child as processed, advancing the slice by one.
+ fn advance(&mut self) {
+ self.children = &self.children[1..];
}
- /// Layout and save the footnote separator, typically a line.
- fn layout_footnote_separator(&mut self) -> SourceResult<()> {
- let expand = Axes::new(self.regions.expand.x, false);
- let pod = Region::new(self.regions.base(), expand);
- let separator = &self.footnote_config.separator;
-
- // FIXME: Shouldn't use `root()` here.
- let mut frame =
- layout_frame(self.engine, separator, Locator::root(), self.shared, pod)?;
- 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(())
+ /// Whether all work is done. This means we can terminate flow layout.
+ fn done(&self) -> bool {
+ self.children.is_empty()
+ && self.spill.is_none()
+ && self.floats.is_empty()
+ && self.footnote_spill.is_none()
+ && self.footnotes.is_empty()
}
- /// Layout the given collected lines' line numbers to an output frame.
- ///
- /// The numbers are placed either on the left margin (left border of the
- /// frame) or on the right margin (right border). Before they are placed,
- /// a line number counter reset is inserted if we're in the first column of
- /// the page being currently laid out and the user requested for line
- /// numbers to be reset at the start of every page.
- fn layout_line_numbers(
- &mut self,
- output: &mut Frame,
- size: Size,
- mut lines: Vec<CollectedParLine>,
- ) -> SourceResult<()> {
- // Reset page-scoped line numbers if currently at the first column.
- if self.root
- && (self.columns == 1 || self.finished.len() % self.columns == 0)
- && ParLine::numbering_scope_in(self.shared) == ParLineNumberingScope::Page
- {
- let reset =
- CounterState::init(&CounterKey::Selector(ParLineMarker::elem().select()));
- let counter = Counter::of(ParLineMarker::elem());
- let update = counter.update(Span::detached(), CounterUpdate::Set(reset));
- let locator = self.locator.next(&update);
- let pod = Region::new(Axes::splat(Abs::zero()), Axes::splat(false));
- let reset_frame =
- layout_frame(self.engine, &update, locator, self.shared, pod)?;
- output.push_frame(Point::zero(), reset_frame);
- }
-
- if lines.is_empty() {
- // We always stop here if this is not the root flow.
- return Ok(());
- }
-
- // Assume the line numbers aren't sorted by height.
- // They must be sorted so we can deduplicate line numbers below based
- // on vertical proximity.
- lines.sort_by_key(|line| line.y);
-
- // Buffer line number frames so we can align them horizontally later
- // before placing, based on the width of the largest line number.
- let mut line_numbers = vec![];
- // Used for horizontal alignment.
- let mut max_number_width = Abs::zero();
- let mut prev_bottom = None;
- for line in lines {
- if prev_bottom.is_some_and(|prev_bottom| line.y < prev_bottom) {
- // Lines are too close together. Display as the same line
- // number.
- continue;
- }
-
- let current_column = self.finished.len() % self.columns;
- let number_margin = if self.columns >= 2 && current_column + 1 == self.columns
- {
- // The last column will always place line numbers at the end
- // margin. This should become configurable in the future.
- OuterHAlignment::End.resolve(self.shared)
- } else {
- line.marker.number_margin().resolve(self.shared)
- };
-
- let number_align = line
- .marker
- .number_align()
- .map(|align| align.resolve(self.shared))
- .unwrap_or_else(|| number_margin.inv());
-
- let number_clearance = line.marker.number_clearance().resolve(self.shared);
- let number = self.layout_line_number(line.marker)?;
- let number_x = match number_margin {
- FixedAlignment::Start => -number_clearance,
- FixedAlignment::End => size.x + number_clearance,
-
- // Shouldn't be specifiable by the user due to
- // 'OuterHAlignment'.
- FixedAlignment::Center => unreachable!(),
- };
- let number_pos = Point::new(number_x, line.y);
-
- // Note that this line.y is larger than the previous due to
- // sorting. Therefore, the check at the top of the loop ensures no
- // line numbers will reasonably intersect with each other.
- //
- // We enforce a minimum spacing of 1pt between consecutive line
- // numbers in case a zero-height frame is used.
- prev_bottom = Some(line.y + number.height().max(Abs::pt(1.0)));
-
- // Collect line numbers and compute the max width so we can align
- // them later.
- max_number_width.set_max(number.width());
- line_numbers.push((number_pos, number, number_align, number_margin));
- }
-
- for (mut pos, number, align, margin) in line_numbers {
- if matches!(margin, FixedAlignment::Start) {
- // Move the line number backwards the more aligned to the left
- // it is, instead of moving to the right when it's right
- // aligned. We do it this way, without fully overriding the
- // 'x' coordinate, to preserve the original clearance between
- // the line numbers and the text.
- pos.x -=
- max_number_width - align.position(max_number_width - number.width());
- } else {
- // Move the line number forwards when aligned to the right.
- // Leave as is when aligned to the left.
- pos.x += align.position(max_number_width - number.width());
- }
-
- output.push_frame(pos, number);
+ /// Add skipped floats and footnotes from the insertion areas to the skip
+ /// set.
+ fn extend_skips(&mut self, skips: &[Skip]) {
+ if !skips.is_empty() {
+ Rc::make_mut(&mut self.skips).extend(skips.iter().copied());
}
-
- Ok(())
- }
-
- /// Layout the line number associated with the given line marker.
- ///
- /// Produces a counter update and counter display with counter key
- /// `ParLineMarker`. We use `ParLineMarker` as it is an element which is
- /// not exposed to the user, as we don't want to expose the line number
- /// counter at the moment, given that its semantics are inconsistent with
- /// that of normal counters (the counter is updated based on height and not
- /// on frame order / layer). When we find a solution to this, we should
- /// switch to a counter on `ParLine` instead, thus exposing the counter as
- /// `counter(par.line)` to the user.
- fn layout_line_number(
- &mut self,
- marker: Packed<ParLineMarker>,
- ) -> SourceResult<Frame> {
- let counter = Counter::of(ParLineMarker::elem());
- let counter_update = counter
- .clone()
- .update(Span::detached(), CounterUpdate::Step(NonZeroUsize::ONE));
- let counter_display = CounterDisplayElem::new(
- counter,
- Smart::Custom(marker.numbering().clone()),
- false,
- );
- let number = SequenceElem::new(vec![counter_update, counter_display.pack()]);
- let locator = self.locator.next(&number);
-
- let pod = Region::new(Axes::splat(Abs::inf()), Axes::splat(false));
- let mut frame =
- layout_frame(self.engine, &number.pack(), locator, self.shared, pod)?;
-
- // Ensure the baseline of the line number aligns with the line's own
- // baseline.
- frame.translate(Point::with_y(-frame.baseline()));
-
- Ok(frame)
}
+}
- /// Collect all footnotes in a frame.
- fn collect_footnotes(
- &mut self,
- notes: &mut Vec<Packed<FootnoteElem>>,
- frame: &Frame,
- ) {
- for (_, item) in frame.items() {
- match item {
- FrameItem::Group(group) => self.collect_footnotes(notes, &group.frame),
- FrameItem::Tag(tag) => {
- let Some(footnote) = tag.elem().to_packed::<FootnoteElem>() else {
- continue;
- };
- if self.visited_footnotes.insert(tag.location()) {
- notes.push(footnote.clone());
- }
- }
- _ => {}
- }
- }
- }
+/// Shared configuration for the whole flow.
+struct Config<'x> {
+ /// Whether this is the root flow, which can host footnotes and line
+ /// numbers.
+ root: bool,
+ /// The styles shared by the whole flow. This is used for footnotes and line
+ /// numbers.
+ shared: StyleChain<'x>,
+ /// Settings for columns.
+ columns: ColumnConfig,
+ /// Settings for footnotes.
+ footnote: FootnoteConfig,
}
-/// Collect all numbered paragraph lines in the frame.
-/// The 'prev_y' parameter starts at 0 on the first call to 'collect_par_lines'.
-/// On each subframe we encounter, we add that subframe's position to 'prev_y',
-/// until we reach a line's tag, at which point we add the tag's position
-/// and finish. That gives us the relative height of the line from the start of
-/// the initial frame.
-fn collect_par_lines(
- lines: &mut Vec<CollectedParLine>,
- frame: &Frame,
- frame_pos: Point,
- prev_y: Abs,
-) {
- for (pos, item) in frame.items() {
- match item {
- FrameItem::Group(group) => {
- collect_par_lines(lines, &group.frame, frame_pos, prev_y + pos.y)
- }
+/// Configuration of footnotes.
+struct FootnoteConfig {
+ /// The separator between flow content and footnotes. Typically a line.
+ separator: Content,
+ /// The amount of space left above the separator.
+ clearance: Abs,
+ /// The gap between footnote entries.
+ gap: Abs,
+ /// Whether horizontal expansion is enabled for footnotes.
+ expand: bool,
+}
- // Unlike footnotes, we don't need to guard against duplicate tags
- // here, since we already deduplicate line markers based on their
- // height later on, in `finish_region`.
- FrameItem::Tag(tag) => {
- let Some(marker) = tag.elem().to_packed::<ParLineMarker>() else {
- continue;
- };
+/// Configuration of columns.
+struct ColumnConfig {
+ /// The number of columns.
+ count: usize,
+ /// The width of each column.
+ width: Abs,
+ /// The amount of space between columns.
+ gutter: Abs,
+ /// The horizontal direction in which columns progress. Defined by
+ /// `text.dir`.
+ dir: Dir,
+}
- // 1. 'prev_y' is the accumulated relative height from the top
- // of the frame we're searching so far;
- // 2. 'prev_y + pos.y' gives us the final relative height of
- // the line we just found from the top of the initial frame;
- // 3. 'frame_pos.y' is the height of the initial frame relative
- // to the root flow (and thus its absolute 'y');
- // 4. Therefore, 'y' will be the line's absolute 'y' in the
- // page based on its marker's position, and thus the 'y' we
- // should use for line numbers. In particular, this represents
- // the 'y' at the line's general baseline, due to the marker
- // placement logic within the 'line::commit()' function in the
- // 'inline' module. We only account for the line number's own
- // baseline later, upon layout.
- let y = frame_pos.y + prev_y + pos.y;
+/// The result type for flow layout.
+///
+/// The `Err(_)` variant incorporate control flow events for finishing and
+/// relayouting regions.
+type FlowResult<T> = Result<T, Stop>;
+
+/// A control flow event during flow layout.
+enum Stop {
+ /// Indicates that the current subregion should be finished. Can be caused
+ /// by a lack of space (`false`) or an explicit column break (`true`).
+ Finish(bool),
+ /// Indicates that the given scope should be relayouted.
+ Relayout(PlacementScope),
+ /// A fatal error.
+ Error(EcoVec<SourceDiagnostic>),
+}
- lines.push(CollectedParLine { y, marker: marker.clone() });
- }
- _ => {}
- }
+impl From<EcoVec<SourceDiagnostic>> for Stop {
+ fn from(error: EcoVec<SourceDiagnostic>) -> Self {
+ Stop::Error(error)
}
}
diff --git a/crates/typst/src/layout/frame.rs b/crates/typst/src/layout/frame.rs
index 60b690dd..2f68e936 100644
--- a/crates/typst/src/layout/frame.rs
+++ b/crates/typst/src/layout/frame.rs
@@ -320,6 +320,12 @@ impl Frame {
/// that result from realization will take care of it and the styles can
/// only apply to them as a whole, not part of it (because they don't manage
/// styles).
+ pub fn post_processed(mut self, styles: StyleChain) -> Self {
+ self.post_process(styles);
+ self
+ }
+
+ /// Post process in place.
pub fn post_process(&mut self, styles: StyleChain) {
if !self.is_empty() {
self.post_process_raw(
diff --git a/crates/typst/src/layout/inline/line.rs b/crates/typst/src/layout/inline/line.rs
index 1ac56e52..7115630e 100644
--- a/crates/typst/src/layout/inline/line.rs
+++ b/crates/typst/src/layout/inline/line.rs
@@ -511,28 +511,25 @@ pub fn commit(
let region = Size::new(amount, full);
let mut frame =
elem.layout(engine, loc.relayout(), *styles, region)?;
- frame.post_process(*styles);
frame.translate(Point::with_y(TextElem::baseline_in(*styles)));
- push(&mut offset, frame);
+ push(&mut offset, frame.post_processed(*styles));
} else {
offset += amount;
}
}
Item::Text(shaped) => {
- let mut frame = shaped.build(
+ let frame = shaped.build(
engine,
&p.spans,
justification_ratio,
extra_justification,
);
- frame.post_process(shaped.styles);
- push(&mut offset, frame);
+ push(&mut offset, frame.post_processed(shaped.styles));
}
Item::Frame(frame, styles) => {
let mut frame = frame.clone();
- frame.post_process(*styles);
frame.translate(Point::with_y(TextElem::baseline_in(*styles)));
- push(&mut offset, frame);
+ push(&mut offset, frame.post_processed(*styles));
}
Item::Tag(tag) => {
let mut frame = Frame::soft(Size::zero());
diff --git a/crates/typst/src/layout/pages/mod.rs b/crates/typst/src/layout/pages/mod.rs
index 860c43f9..574703d7 100644
--- a/crates/typst/src/layout/pages/mod.rs
+++ b/crates/typst/src/layout/pages/mod.rs
@@ -1,3 +1,5 @@
+//! Layout of content into a [`Document`].
+
mod collect;
mod finalize;
mod run;
diff --git a/crates/typst/src/layout/pages/run.rs b/crates/typst/src/layout/pages/run.rs
index 091dba22..b5c2834f 100644
--- a/crates/typst/src/layout/pages/run.rs
+++ b/crates/typst/src/layout/pages/run.rs
@@ -13,7 +13,6 @@ use crate::layout::{
};
use crate::model::Numbering;
use crate::realize::Pair;
-use crate::syntax::Span;
use crate::text::TextElem;
use crate::utils::Numeric;
use crate::visualize::Paint;
@@ -117,11 +116,6 @@ fn layout_page_run_impl(
.resolve(styles)
.relative_to(size);
- // Realize columns.
- let area = size - margin.sum_by_axis();
- let mut regions = Regions::repeat(area, area.map(Abs::is_finite));
- regions.root = true;
-
let fill = PageElem::fill_in(styles);
let foreground = PageElem::foreground_in(styles);
let background = PageElem::background_in(styles);
@@ -167,17 +161,16 @@ fn layout_page_run_impl(
};
// Layout the children.
- let bump = bumpalo::Bump::new();
+ let area = size - margin.sum_by_axis();
let fragment = layout_flow(
&mut engine,
- &bump,
children,
&mut locator,
styles,
- regions,
+ Regions::repeat(area, area.map(Abs::is_finite)),
PageElem::columns_in(styles),
ColumnsElem::gutter_in(styles),
- Span::detached(),
+ true,
)?;
// Layouts a single marginal.
diff --git a/crates/typst/src/layout/place.rs b/crates/typst/src/layout/place.rs
index 5c508872..dfca40f4 100644
--- a/crates/typst/src/layout/place.rs
+++ b/crates/typst/src/layout/place.rs
@@ -1,4 +1,4 @@
-use crate::foundations::{elem, scope, Content, Smart};
+use crate::foundations::{elem, scope, Cast, Content, Smart};
use crate::layout::{Alignment, Em, Length, Rel};
/// Places content at an absolute position.
@@ -35,6 +35,19 @@ pub struct PlaceElem {
#[default(Smart::Custom(Alignment::START))]
pub alignment: Smart<Alignment>,
+ /// Relative to which containing scope something is placed.
+ ///
+ /// Page-scoped floating placement is primarily used with figures and, for
+ /// this reason, the figure function has a mirrored [`scope`
+ /// parameter]($figure.scope). Nonetheless, it can also be more generally
+ /// useful to break out of the columns. A typical example would be to
+ /// [create a single-column title section]($guides/page-setup/#columns) in a
+ /// two-column document.
+ ///
+ /// Note that page-scoped placement is currently only supported if `float`
+ /// is `{true}`. This may change in the future.
+ pub scope: PlacementScope,
+
/// Whether the placed element has floating layout.
///
/// Floating elements are positioned at the top or bottom of the page,
@@ -61,6 +74,8 @@ pub struct PlaceElem {
pub float: bool,
/// The amount of clearance the placed element has in a floating layout.
+ ///
+ /// Has no effect if `float` is `{false}`.
#[default(Em::new(1.5).into())]
#[resolve]
pub clearance: Length,
@@ -98,6 +113,16 @@ impl PlaceElem {
type FlushElem;
}
+/// Relative to which containing scope something shall be placed.
+#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Cast)]
+pub enum PlacementScope {
+ /// Place into the current column.
+ #[default]
+ Column,
+ /// Place relative to the page, letting the content span over all columns.
+ Page,
+}
+
/// Asks the layout algorithm to place pending floating elements before
/// continuing with the content.
///
diff --git a/crates/typst/src/layout/regions.rs b/crates/typst/src/layout/regions.rs
index 7ff2e1c4..68ad4b7a 100644
--- a/crates/typst/src/layout/regions.rs
+++ b/crates/typst/src/layout/regions.rs
@@ -27,7 +27,6 @@ impl From<Region> for Regions<'_> {
full: region.size.y,
backlog: &[],
last: None,
- root: false,
}
}
}
@@ -53,11 +52,6 @@ pub struct Regions<'a> {
/// 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 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<'_> {
@@ -69,7 +63,6 @@ impl Regions<'_> {
backlog: &[],
last: Some(size.y),
expand,
- root: false,
}
}
@@ -98,7 +91,6 @@ impl Regions<'_> {
backlog,
last: self.last.map(|y| f(Size::new(x, y)).y),
expand: self.expand,
- root: self.root,
}
}
@@ -114,11 +106,6 @@ impl Regions<'_> {
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
diff --git a/crates/typst/src/math/fragment.rs b/crates/typst/src/math/fragment.rs
index 17e98878..a3fcc9c6 100644
--- a/crates/typst/src/math/fragment.rs
+++ b/crates/typst/src/math/fragment.rs
@@ -467,12 +467,11 @@ pub struct FrameFragment {
}
impl FrameFragment {
- pub fn new(ctx: &MathContext, styles: StyleChain, mut frame: Frame) -> Self {
+ pub fn new(ctx: &MathContext, styles: StyleChain, frame: Frame) -> Self {
let base_ascent = frame.ascent();
let accent_attach = frame.width() / 2.0;
- frame.post_process(styles);
Self {
- frame,
+ frame: frame.post_processed(styles),
font_size: scaled_font_size(ctx, styles),
class: EquationElem::class_in(styles).unwrap_or(MathClass::Normal),
math_size: EquationElem::size_in(styles),
diff --git a/crates/typst/src/model/figure.rs b/crates/typst/src/model/figure.rs
index 618a6b4e..08606c26 100644
--- a/crates/typst/src/model/figure.rs
+++ b/crates/typst/src/model/figure.rs
@@ -15,7 +15,7 @@ use crate::introspection::{
};
use crate::layout::{
AlignElem, Alignment, BlockBody, BlockElem, Em, HAlignment, Length, OuterVAlignment,
- PlaceElem, VAlignment, VElem,
+ PlaceElem, PlacementScope, VAlignment, VElem,
};
use crate::model::{Numbering, NumberingPattern, Outlinable, Refable, Supplement};
use crate::text::{Lang, Region, TextElem};
@@ -133,6 +133,14 @@ pub struct FigureElem {
/// ```
pub placement: Option<Smart<VAlignment>>,
+ /// Relative to which containing scope something is placed.
+ ///
+ /// Set this to `{"page"}` to create a full-width figure in a two-column
+ /// document.
+ ///
+ /// Has no effect if `placement` is `{none}`.
+ pub scope: PlacementScope,
+
/// The figure's caption.
pub caption: Option<Packed<FigureCaption>>,
@@ -325,8 +333,9 @@ impl Show for Packed<FigureElem> {
// Wrap in a float.
if let Some(align) = self.placement(styles) {
realized = PlaceElem::new(realized)
- .with_float(true)
.with_alignment(align.map(|align| HAlignment::Center + align))
+ .with_scope(self.scope(styles))
+ .with_float(true)
.pack()
.spanned(self.span());
}
diff --git a/crates/typst/src/model/footnote.rs b/crates/typst/src/model/footnote.rs
index 2aeaad1a..813990a9 100644
--- a/crates/typst/src/model/footnote.rs
+++ b/crates/typst/src/model/footnote.rs
@@ -125,6 +125,9 @@ impl Packed<FootnoteElem> {
let footnote = element
.to_packed::<FootnoteElem>()
.ok_or("referenced element should be a footnote")?;
+ if self.location() == footnote.location() {
+ bail!("footnote cannot reference itself");
+ }
footnote.declaration_location(engine)
}
_ => Ok(self.location().unwrap()),
diff --git a/crates/typst/src/model/par.rs b/crates/typst/src/model/par.rs
index 326d151e..2e23bd74 100644
--- a/crates/typst/src/model/par.rs
+++ b/crates/typst/src/model/par.rs
@@ -297,8 +297,8 @@ pub struct ParLine {
/// Second line again
/// ```
#[ghost]
- #[default(ParLineNumberingScope::Document)]
- pub numbering_scope: ParLineNumberingScope,
+ #[default(LineNumberingScope::Document)]
+ pub numbering_scope: LineNumberingScope,
}
impl Construct for ParLine {
@@ -310,7 +310,7 @@ impl Construct for ParLine {
/// Possible line numbering scope options, indicating how often the line number
/// counter should be reset.
#[derive(Debug, Cast, Clone, Copy, PartialEq, Eq, Hash)]
-pub enum ParLineNumberingScope {
+pub enum LineNumberingScope {
/// Indicates the line number counter spans the whole document, that is,
/// is never automatically reset.
Document,
diff --git a/crates/typst/src/visualize/image/mod.rs b/crates/typst/src/visualize/image/mod.rs
index b11cf587..d267ca04 100644
--- a/crates/typst/src/visualize/image/mod.rs
+++ b/crates/typst/src/visualize/image/mod.rs
@@ -22,7 +22,7 @@ use crate::foundations::{
use crate::introspection::Locator;
use crate::layout::{
Abs, Axes, BlockElem, FixedAlignment, Frame, FrameItem, Length, Point, Region, Rel,
- Size,
+ Size, Sizing,
};
use crate::loading::Readable;
use crate::model::Figurable;
@@ -79,7 +79,7 @@ pub struct ImageElem {
pub width: Smart<Rel<Length>>,
/// The height of the image.
- pub height: Smart<Rel<Length>>,
+ pub height: Sizing,
/// A text describing the image.
pub alt: Option<EcoString>,
@@ -127,7 +127,7 @@ impl ImageElem {
width: Option<Smart<Rel<Length>>>,
/// The height of the image.
#[named]
- height: Option<Smart<Rel<Length>>>,
+ height: Option<Sizing>,
/// A text describing the image.
#[named]
alt: Option<Option<EcoString>>,
diff --git a/crates/typst/src/visualize/shape.rs b/crates/typst/src/visualize/shape.rs
index b2125bf5..633872cc 100644
--- a/crates/typst/src/visualize/shape.rs
+++ b/crates/typst/src/visualize/shape.rs
@@ -8,7 +8,7 @@ use crate::foundations::{
use crate::introspection::Locator;
use crate::layout::{
layout_frame, Abs, Axes, BlockElem, Corner, Corners, Frame, FrameItem, Length, Point,
- Ratio, Region, Rel, Sides, Size,
+ Ratio, Region, Rel, Sides, Size, Sizing,
};
use crate::syntax::Span;
use crate::utils::Get;
@@ -33,7 +33,7 @@ pub struct RectElem {
pub width: Smart<Rel<Length>>,
/// The rectangle's height, relative to its parent container.
- pub height: Smart<Rel<Length>>,
+ pub height: Sizing,
/// How to fill the rectangle.
///
@@ -202,9 +202,9 @@ pub struct SquareElem {
/// height.
#[parse(match size {
None => args.named("height")?,
- size => size,
+ size => size.map(Into::into),
})]
- pub height: Smart<Rel<Length>>,
+ pub height: Sizing,
/// How to fill the square. See the [rectangle's documentation]($rect.fill)
/// for more details.
@@ -293,7 +293,7 @@ pub struct EllipseElem {
pub width: Smart<Rel<Length>>,
/// The ellipse's height, relative to its parent container.
- pub height: Smart<Rel<Length>>,
+ pub height: Sizing,
/// How to fill the ellipse. See the [rectangle's documentation]($rect.fill)
/// for more details.
@@ -399,9 +399,9 @@ pub struct CircleElem {
/// height.
#[parse(match size {
None => args.named("height")?,
- size => size,
+ size => size.map(Into::into),
})]
- pub height: Smart<Rel<Length>>,
+ pub height: Sizing,
/// How to fill the circle. See the [rectangle's documentation]($rect.fill)
/// for more details.
diff --git a/docs/guides/page-setup.md b/docs/guides/page-setup.md
index 14c35e1b..ac475583 100644
--- a/docs/guides/page-setup.md
+++ b/docs/guides/page-setup.md
@@ -390,59 +390,52 @@ Add columns to your document to fit more on a page while maintaining legible
line lengths. Columns are vertical blocks of text which are separated by some
whitespace. This space is called the gutter.
-If all of your content needs to be laid out in columns, you can just specify the
-desired number of columns in the [`{page}`]($page.columns) set rule:
+To lay out your content in columns, just specify the desired number of columns
+in a [`{page}`]($page.columns) set rule. To adjust the amount of space between
+the columns, add a set rule on the [`columns` function]($columns), specifying
+the `gutter` parameter.
```example
>>> #set page(height: 120pt)
#set page(columns: 2)
+#set columns(gutter: 12pt)
+
#lorem(30)
```
-If you need to adjust the gutter between the columns, refer to the method used
-in the next section.
-
-### Use columns anywhere in your document { #columns-anywhere }
Very commonly, scientific papers have a single-column title and abstract, while
-the main body is set in two-columns. To achieve this effect, Typst includes a
-standalone [`{columns}` function]($columns) that can be used to insert columns
-anywhere on a page.
-
-Conceptually, the `columns` function must wrap the content of the columns:
+the main body is set in two-columns. To achieve this effect, Typst's [`place`
+function]($place) can temporarily escape the two-column layout by specifying
+`{float: true}` and `{scope: "page"}`:
```example:single
>>> #set page(height: 180pt)
-= Impacts of Odobenidae
-
+#set page(columns: 2)
#set par(justify: true)
->>> #h(11pt)
-#columns(2)[
- == About seals in the wild
- #lorem(80)
-]
-```
-However, we can use the ["everything show rule"]($styling/#show-rules) to reduce
-nesting and write more legible Typst markup:
-
-```example:single
->>> #set page(height: 180pt)
-= Impacts of Odobenidae
-
-#set par(justify: true)
->>> #h(11pt)
-#show: columns.with(2)
+#place(
+ top + center,
+ float: true,
+ scope: "page",
+ text(1.4em, weight: "bold")[
+ Impacts of Odobenidae
+ ],
+)
== About seals in the wild
#lorem(80)
```
-The show rule will wrap everything that comes after it in its function. The
-[`with` method]($function.with) allows us to pass arguments, in this case, the
-column count, to a function without calling it.
+_Floating placement_ refers to elements being pushed to the top or bottom of the
+column or page, with the remaining content flowing in between. It is also
+frequently used for [figures]($figure.placement).
-Another use of the `columns` function is to create columns inside of a container
-like a rectangle or to customize gutter size:
+### Use columns anywhere in your document { #columns-anywhere }
+To create columns within a nested layout, e.g. within a rectangle, you can use
+the [`columns` function]($columns) directly. However, it should really only be
+used within nested layouts. At the page-level, the page set rule is preferrable
+because it has better interactions with things like page-level floats,
+footnotes, and line numbers.
```example
#rect(
diff --git a/tests/ref/block-fr-height-auto-width.png b/tests/ref/block-fr-height-auto-width.png
new file mode 100644
index 00000000..21cd3f51
--- /dev/null
+++ b/tests/ref/block-fr-height-auto-width.png
Binary files differ
diff --git a/tests/ref/block-fr-height-first-child.png b/tests/ref/block-fr-height-first-child.png
new file mode 100644
index 00000000..0a179576
--- /dev/null
+++ b/tests/ref/block-fr-height-first-child.png
Binary files differ
diff --git a/tests/ref/block-fr-height-multiple.png b/tests/ref/block-fr-height-multiple.png
new file mode 100644
index 00000000..f502343c
--- /dev/null
+++ b/tests/ref/block-fr-height-multiple.png
Binary files differ
diff --git a/tests/ref/block-fr-height.png b/tests/ref/block-fr-height.png
new file mode 100644
index 00000000..657cb43a
--- /dev/null
+++ b/tests/ref/block-fr-height.png
Binary files differ
diff --git a/tests/ref/block-sticky-alone.png b/tests/ref/block-sticky-alone.png
new file mode 100644
index 00000000..74e30b3b
--- /dev/null
+++ b/tests/ref/block-sticky-alone.png
Binary files differ
diff --git a/tests/ref/block-sticky-colbreak.png b/tests/ref/block-sticky-colbreak.png
new file mode 100644
index 00000000..a56b25d9
--- /dev/null
+++ b/tests/ref/block-sticky-colbreak.png
Binary files differ
diff --git a/tests/ref/block-sticky-many.png b/tests/ref/block-sticky-many.png
new file mode 100644
index 00000000..cdcf291d
--- /dev/null
+++ b/tests/ref/block-sticky-many.png
Binary files differ
diff --git a/tests/ref/block-sticky.png b/tests/ref/block-sticky.png
new file mode 100644
index 00000000..4f236c89
--- /dev/null
+++ b/tests/ref/block-sticky.png
Binary files differ
diff --git a/tests/ref/box-width-fr.png b/tests/ref/box-fr-width.png
index 30d48163..30d48163 100644
--- a/tests/ref/box-width-fr.png
+++ b/tests/ref/box-fr-width.png
Binary files differ
diff --git a/tests/ref/colbreak-weak.png b/tests/ref/colbreak-weak.png
new file mode 100644
index 00000000..e2ce5b96
--- /dev/null
+++ b/tests/ref/colbreak-weak.png
Binary files differ
diff --git a/tests/ref/figure-placement.png b/tests/ref/figure-placement.png
new file mode 100644
index 00000000..c9ebd2ba
--- /dev/null
+++ b/tests/ref/figure-placement.png
Binary files differ
diff --git a/tests/ref/footnote-block-at-end.png b/tests/ref/footnote-block-at-end.png
new file mode 100644
index 00000000..86416c48
--- /dev/null
+++ b/tests/ref/footnote-block-at-end.png
Binary files differ
diff --git a/tests/ref/footnote-break-across-pages-block.png b/tests/ref/footnote-break-across-pages-block.png
new file mode 100644
index 00000000..ae27b41a
--- /dev/null
+++ b/tests/ref/footnote-break-across-pages-block.png
Binary files differ
diff --git a/tests/ref/footnote-break-across-pages-float.png b/tests/ref/footnote-break-across-pages-float.png
new file mode 100644
index 00000000..eb5f6f5d
--- /dev/null
+++ b/tests/ref/footnote-break-across-pages-float.png
Binary files differ
diff --git a/tests/ref/footnote-break-across-pages-nested.png b/tests/ref/footnote-break-across-pages-nested.png
new file mode 100644
index 00000000..49061844
--- /dev/null
+++ b/tests/ref/footnote-break-across-pages-nested.png
Binary files differ
diff --git a/tests/ref/footnote-break-across-pages.png b/tests/ref/footnote-break-across-pages.png
index 73a483b2..8e5deabc 100644
--- a/tests/ref/footnote-break-across-pages.png
+++ b/tests/ref/footnote-break-across-pages.png
Binary files differ
diff --git a/tests/ref/footnote-float-priority.png b/tests/ref/footnote-float-priority.png
new file mode 100644
index 00000000..26797331
--- /dev/null
+++ b/tests/ref/footnote-float-priority.png
Binary files differ
diff --git a/tests/ref/footnote-in-list.png b/tests/ref/footnote-in-list.png
new file mode 100644
index 00000000..504c3578
--- /dev/null
+++ b/tests/ref/footnote-in-list.png
Binary files differ
diff --git a/tests/ref/footnote-in-place.png b/tests/ref/footnote-in-place.png
new file mode 100644
index 00000000..d41316db
--- /dev/null
+++ b/tests/ref/footnote-in-place.png
Binary files differ
diff --git a/tests/ref/footnote-in-table.png b/tests/ref/footnote-in-table.png
index 3f8f50ca..7aa2bbf4 100644
--- a/tests/ref/footnote-in-table.png
+++ b/tests/ref/footnote-in-table.png
Binary files differ
diff --git a/tests/ref/footnote-multiple-in-one-line.png b/tests/ref/footnote-multiple-in-one-line.png
new file mode 100644
index 00000000..6f60b609
--- /dev/null
+++ b/tests/ref/footnote-multiple-in-one-line.png
Binary files differ
diff --git a/tests/ref/footnote-nested-same-frame.png b/tests/ref/footnote-nested-same-frame.png
deleted file mode 100644
index b22276d5..00000000
--- a/tests/ref/footnote-nested-same-frame.png
+++ /dev/null
Binary files differ
diff --git a/tests/ref/footnote-nested.png b/tests/ref/footnote-nested.png
index fecf2e8d..50cc2637 100644
--- a/tests/ref/footnote-nested.png
+++ b/tests/ref/footnote-nested.png
Binary files differ
diff --git a/tests/ref/issue-2213-align-fr.png b/tests/ref/issue-2213-align-fr.png
new file mode 100644
index 00000000..66f56d76
--- /dev/null
+++ b/tests/ref/issue-2213-align-fr.png
Binary files differ
diff --git a/tests/ref/issue-3481-cite-location.png b/tests/ref/issue-3481-cite-location.png
index cfc13db5..63388bd2 100644
--- a/tests/ref/issue-3481-cite-location.png
+++ b/tests/ref/issue-3481-cite-location.png
Binary files differ
diff --git a/tests/ref/issue-3641-float-loop.png b/tests/ref/issue-3641-float-loop.png
index c898d54e..c294c1e4 100644
--- a/tests/ref/issue-3641-float-loop.png
+++ b/tests/ref/issue-3641-float-loop.png
Binary files differ
diff --git a/tests/ref/issue-3866-block-migration.png b/tests/ref/issue-3866-block-migration.png
new file mode 100644
index 00000000..6e48f486
--- /dev/null
+++ b/tests/ref/issue-3866-block-migration.png
Binary files differ
diff --git a/tests/ref/issue-footnotes-skip-first-page.png b/tests/ref/issue-footnotes-skip-first-page.png
index d24387e3..fd973af7 100644
--- a/tests/ref/issue-footnotes-skip-first-page.png
+++ b/tests/ref/issue-footnotes-skip-first-page.png
Binary files differ
diff --git a/tests/ref/issue-multiple-footnote-in-one-line.png b/tests/ref/issue-multiple-footnote-in-one-line.png
deleted file mode 100644
index cdb83af2..00000000
--- a/tests/ref/issue-multiple-footnote-in-one-line.png
+++ /dev/null
Binary files differ
diff --git a/tests/ref/place-float-block-backlog.png b/tests/ref/place-float-block-backlog.png
new file mode 100644
index 00000000..c212717e
--- /dev/null
+++ b/tests/ref/place-float-block-backlog.png
Binary files differ
diff --git a/tests/ref/place-float-clearance-empty.png b/tests/ref/place-float-clearance-empty.png
new file mode 100644
index 00000000..f170df34
--- /dev/null
+++ b/tests/ref/place-float-clearance-empty.png
Binary files differ
diff --git a/tests/ref/place-float-column-align-auto.png b/tests/ref/place-float-column-align-auto.png
new file mode 100644
index 00000000..a1dc9203
--- /dev/null
+++ b/tests/ref/place-float-column-align-auto.png
Binary files differ
diff --git a/tests/ref/place-float-in-column-queued.png b/tests/ref/place-float-column-queued.png
index 3eeb13dd..3eeb13dd 100644
--- a/tests/ref/place-float-in-column-queued.png
+++ b/tests/ref/place-float-column-queued.png
Binary files differ
diff --git a/tests/ref/place-float-counter.png b/tests/ref/place-float-counter.png
new file mode 100644
index 00000000..aa669521
--- /dev/null
+++ b/tests/ref/place-float-counter.png
Binary files differ
diff --git a/tests/ref/place-float-delta.png b/tests/ref/place-float-delta.png
new file mode 100644
index 00000000..578d9301
--- /dev/null
+++ b/tests/ref/place-float-delta.png
Binary files differ
diff --git a/tests/ref/place-float-flow-size-alone.png b/tests/ref/place-float-flow-size-alone.png
new file mode 100644
index 00000000..e1b6adac
--- /dev/null
+++ b/tests/ref/place-float-flow-size-alone.png
Binary files differ
diff --git a/tests/ref/place-float-flow-size.png b/tests/ref/place-float-flow-size.png
new file mode 100644
index 00000000..60bbc7cd
--- /dev/null
+++ b/tests/ref/place-float-flow-size.png
Binary files differ
diff --git a/tests/ref/place-float-fr.png b/tests/ref/place-float-fr.png
new file mode 100644
index 00000000..83d31054
--- /dev/null
+++ b/tests/ref/place-float-fr.png
Binary files differ
diff --git a/tests/ref/place-float-in-column-align-auto.png b/tests/ref/place-float-in-column-align-auto.png
deleted file mode 100644
index 58ba9741..00000000
--- a/tests/ref/place-float-in-column-align-auto.png
+++ /dev/null
Binary files differ
diff --git a/tests/ref/place-float-rel-sizing.png b/tests/ref/place-float-rel-sizing.png
new file mode 100644
index 00000000..1b4e44b2
--- /dev/null
+++ b/tests/ref/place-float-rel-sizing.png
Binary files differ
diff --git a/tests/ref/place-float-threecolumn-block-backlog.png b/tests/ref/place-float-threecolumn-block-backlog.png
new file mode 100644
index 00000000..768d4eda
--- /dev/null
+++ b/tests/ref/place-float-threecolumn-block-backlog.png
Binary files differ
diff --git a/tests/ref/place-float-threecolumn.png b/tests/ref/place-float-threecolumn.png
new file mode 100644
index 00000000..65ad88ac
--- /dev/null
+++ b/tests/ref/place-float-threecolumn.png
Binary files differ
diff --git a/tests/ref/place-float-twocolumn-align-auto.png b/tests/ref/place-float-twocolumn-align-auto.png
new file mode 100644
index 00000000..5d9932a3
--- /dev/null
+++ b/tests/ref/place-float-twocolumn-align-auto.png
Binary files differ
diff --git a/tests/ref/place-float-twocolumn-fits-not.png b/tests/ref/place-float-twocolumn-fits-not.png
new file mode 100644
index 00000000..e533daf9
--- /dev/null
+++ b/tests/ref/place-float-twocolumn-fits-not.png
Binary files differ
diff --git a/tests/ref/place-float-twocolumn-fits.png b/tests/ref/place-float-twocolumn-fits.png
new file mode 100644
index 00000000..07e4c25f
--- /dev/null
+++ b/tests/ref/place-float-twocolumn-fits.png
Binary files differ
diff --git a/tests/ref/place-float-twocolumn-queued.png b/tests/ref/place-float-twocolumn-queued.png
new file mode 100644
index 00000000..e5fa387d
--- /dev/null
+++ b/tests/ref/place-float-twocolumn-queued.png
Binary files differ
diff --git a/tests/ref/place-float-twocolumn.png b/tests/ref/place-float-twocolumn.png
new file mode 100644
index 00000000..3ed2f729
--- /dev/null
+++ b/tests/ref/place-float-twocolumn.png
Binary files differ
diff --git a/tests/ref/query-running-header.png b/tests/ref/query-running-header.png
index 210c7810..1dd04418 100644
--- a/tests/ref/query-running-header.png
+++ b/tests/ref/query-running-header.png
Binary files differ
diff --git a/tests/skip.txt b/tests/skip.txt
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/tests/skip.txt
diff --git a/tests/src/collect.rs b/tests/src/collect.rs
index 4dae0b70..cc3ff736 100644
--- a/tests/src/collect.rs
+++ b/tests/src/collect.rs
@@ -5,6 +5,7 @@ use std::path::{Path, PathBuf};
use std::str::FromStr;
use ecow::{eco_format, EcoString};
+use once_cell::sync::Lazy;
use typst::syntax::package::PackageVersion;
use typst::syntax::{is_id_continue, is_ident, is_newline, FileId, Source, VirtualPath};
use unscanny::Scanner;
@@ -389,6 +390,18 @@ impl<'a> Parser<'a> {
/// Whether a test is within the selected set to run.
fn selected(name: &str, abs: PathBuf) -> bool {
+ static SKIPPED: Lazy<HashSet<&'static str>> = Lazy::new(|| {
+ String::leak(std::fs::read_to_string(crate::SKIP_PATH).unwrap())
+ .lines()
+ .map(|line| line.trim())
+ .filter(|line| !line.is_empty() && !line.starts_with("//"))
+ .collect()
+ });
+
+ if SKIPPED.contains(name) {
+ return false;
+ }
+
let paths = &crate::ARGS.path;
if !paths.is_empty() && !paths.iter().any(|path| abs.starts_with(path)) {
return false;
diff --git a/tests/src/tests.rs b/tests/src/tests.rs
index a2d85fec..58bd7cf7 100644
--- a/tests/src/tests.rs
+++ b/tests/src/tests.rs
@@ -30,6 +30,9 @@ const STORE_PATH: &str = "tests/store";
/// The directory where the reference images are stored.
const REF_PATH: &str = "tests/ref";
+/// The file where the skipped tests are stored.
+const SKIP_PATH: &str = "tests/skip.txt";
+
/// The maximum size of reference images that aren't marked as `// LARGE`.
const REF_LIMIT: usize = 20 * 1024;
diff --git a/tests/suite/introspection/query.typ b/tests/suite/introspection/query.typ
index b078863e..ddb518f5 100644
--- a/tests/suite/introspection/query.typ
+++ b/tests/suite/introspection/query.typ
@@ -25,10 +25,10 @@
#outline()
= Introduction
-#v(1cm)
+#lines(1)
= Background
-#v(2cm)
+#lines(2)
= Approach
diff --git a/tests/suite/layout/align.typ b/tests/suite/layout/align.typ
index 61b79975..c4ed9ab9 100644
--- a/tests/suite/layout/align.typ
+++ b/tests/suite/layout/align.typ
@@ -140,3 +140,11 @@ To the right! Where the sunlight peeks behind the mountain.
// Test right-aligning a line and a rectangle.
#align(right, line(length: 30%))
#align(right, rect())
+
+--- issue-2213-align-fr ---
+// Test a mix of alignment and fr units (fr wins).
+#set page(height: 80pt)
+A
+#v(1fr)
+B
+#align(bottom + right)[C]
diff --git a/tests/suite/layout/columns.typ b/tests/suite/layout/columns.typ
index 87a9f773..b86b798b 100644
--- a/tests/suite/layout/columns.typ
+++ b/tests/suite/layout/columns.typ
@@ -122,3 +122,10 @@ Hallo
= B
Text
]
+
+--- colbreak-weak ---
+#set page(columns: 2)
+#colbreak(weak: true)
+A
+#colbreak(weak: true)
+B
diff --git a/tests/suite/layout/container.typ b/tests/suite/layout/container.typ
index 9ce3dc7d..508f1a36 100644
--- a/tests/suite/layout/container.typ
+++ b/tests/suite/layout/container.typ
@@ -18,10 +18,41 @@ Apart
#block(width: 50%, height: 60%, fill: blue)
]
---- box-width-fr ---
+--- box-fr-width ---
// Test fr box.
Hello #box(width: 1fr, rect(height: 0.7em, width: 100%)) World
+--- block-fr-height ---
+#set page(height: 100pt)
+#rect(height: 10pt, width: 100%)
+#align(center, block(height: 1fr, width: 20pt, stroke: 1pt))
+#rect(height: 10pt, width: 100%)
+
+--- block-fr-height-auto-width ---
+// Test that the fr block can also expand its parent.
+#set page(height: 100pt)
+#set align(center)
+#block(inset: 5pt, stroke: green)[
+ #rect(height: 10pt)
+ #block(height: 1fr, stroke: 1pt, inset: 5pt)[
+ #set align(center + horizon)
+ I am the widest
+ ]
+ #rect(height: 10pt)
+]
+
+--- block-fr-height-first-child ---
+// Test that block spacing is not trimmed if only an fr block precedes it.
+#set page(height: 100pt)
+#rect(height: 1fr)
+#rect()
+
+--- block-fr-height-multiple ---
+#set page(height: 100pt)
+#rect(height: 1fr)
+#rect()
+#block(height: 1fr, line(length: 100%, angle: 90deg))
+
--- block-multiple-pages ---
// Test block over multiple pages.
#set page(height: 60pt)
@@ -121,6 +152,34 @@ Paragraph
#show bibliography: none
#bibliography("/assets/bib/works.bib")
+--- block-sticky ---
+#set page(height: 100pt)
+#lines(3)
+#block(sticky: true)[D]
+#block(sticky: true)[E]
+F
+
+--- block-sticky-alone ---
+#set page(height: 50pt)
+#block(sticky: true)[A]
+
+--- block-sticky-many ---
+#set page(height: 80pt)
+#set block(sticky: true)
+#block[A]
+#block[B]
+#block[C]
+#block[D]
+E
+#block[F]
+#block[G]
+
+--- block-sticky-colbreak ---
+A
+#block(sticky: true)[B]
+#colbreak()
+C
+
--- box-clip-rect ---
// Test box clipping with a rectangle
Hello #box(width: 1em, height: 1em, clip: false)[#rect(width: 3em, height: 3em, fill: red)]
diff --git a/tests/suite/layout/flow/flow.typ b/tests/suite/layout/flow/flow.typ
index 88075c5b..fcbc005b 100644
--- a/tests/suite/layout/flow/flow.typ
+++ b/tests/suite/layout/flow/flow.typ
@@ -55,9 +55,6 @@
--- issue-3641-float-loop ---
// Flow layout should terminate!
-//
-// This is not yet ideal: The heading should not move to the second page, but
-// that's a separate bug and not a regression.
#set page(height: 40pt)
= Heading
@@ -69,3 +66,13 @@
#metadata(none)
#v(10pt, weak: true)
Hi
+
+--- issue-3866-block-migration ---
+#set page(height: 120pt)
+#set text(costs: (widow: 0%, orphan: 0%))
+#v(50pt)
+#columns(2)[
+ #lines(6)
+ #block(rect(width: 80%, height: 80pt), breakable: false)
+ #lines(6)
+]
diff --git a/tests/suite/model/footnote.typ b/tests/suite/layout/flow/footnote.typ
index 41091228..f7722e15 100644
--- a/tests/suite/model/footnote.typ
+++ b/tests/suite/layout/flow/footnote.typ
@@ -9,16 +9,12 @@ A#footnote[A] \
A #footnote[A]
--- footnote-nested ---
-// Test nested footnotes.
-First \
-Second #footnote[A, #footnote[B, #footnote[C]]] \
-Third #footnote[D, #footnote[E]] \
-Fourth
-
---- footnote-nested-same-frame ---
// Currently, numbers a bit out of order if a nested footnote ends up in the
// same frame as another one. :(
-#footnote[A, #footnote[B]], #footnote[C]
+First \
+Second #footnote[A, #footnote[B, #footnote[C]]]
+Third #footnote[D, #footnote[E]] \
+Fourth #footnote[F]
--- footnote-entry ---
// Test customization.
@@ -48,18 +44,94 @@ Beautiful footnotes. #footnote[Wonderful, aren't they?]
#lines(6)
#footnote[V] // 5
---- footnote-in-columns ---
-// Test footnotes in columns, even those that are not enabled via `set page`.
+--- footnote-break-across-pages-block ---
+#set page(height: 100pt)
+#block[
+ #lines(3) #footnote(lines(6, "1"))
+ #footnote[Y]
+ #footnote[Z]
+]
+
+--- footnote-break-across-pages-float ---
+#set page(height: 180pt)
+
+#lines(5)
+
+#place(
+ bottom,
+ float: true,
+ rect(height: 50pt, width: 100%, {
+ footnote(lines(6, "1"))
+ footnote(lines(2, "I"))
+ })
+)
+
+#lines(5)
+
+--- footnote-break-across-pages-nested ---
#set page(height: 120pt)
-#align(center, strong[Title])
+#block[
+ #lines(4)
+ #footnote[
+ #lines(6, "1")
+ #footnote(lines(3, "I"))
+ ]
+]
+
+--- footnote-in-columns ---
+#set page(height: 120pt, columns: 2)
+
+#place(
+ top + center,
+ float: true,
+ scope: "page",
+ clearance: 12pt,
+ strong[Title],
+)
-#show: columns.with(2)
#lines(3)
#footnote(lines(4, "1"))
#lines(2)
#footnote(lines(2, "1"))
+--- footnote-in-list ---
+#set page(height: 120pt)
+
+- A #footnote[a]
+- B #footnote[b]
+- C #footnote[c]
+- D #footnote[d]
+- E #footnote[e]
+- F #footnote[f]
+- G #footnote[g]
+
+--- footnote-block-at-end ---
+#set page(height: 50pt)
+A
+#block(footnote[hello])
+
+--- footnote-float-priority ---
+#set page(height: 100pt)
+
+#lines(3)
+
+#place(
+ top,
+ float: true,
+ rect(height: 40pt)
+)
+
+#block[
+ V
+ #footnote[1]
+ #footnote[2]
+ #footnote[3]
+ #footnote[4]
+]
+
+#lines(5)
+
--- footnote-in-caption ---
// Test footnote in caption.
Read the docs #footnote[https://typst.app/docs]!
@@ -71,6 +143,15 @@ Read the docs #footnote[https://typst.app/docs]!
)
More #footnote[just for ...] footnotes #footnote[... testing. :)]
+--- footnote-in-place ---
+A
+#place(top + right, footnote[A])
+#figure(
+ placement: bottom,
+ caption: footnote[B],
+ rect(),
+)
+
--- footnote-duplicate ---
// Test duplicate footnotes.
#let lang = footnote[Languages.]
@@ -105,6 +186,10 @@ A #footnote(lines(6, "1"))
A footnote #footnote[Hi]<fn> \
A reference to it @fn
+--- footnote-self-ref ---
+// Error: 2-16 footnote cannot reference itself
+#footnote(<fn>) <fn>
+
--- footnote-ref-multiple ---
// Multiple footnotes are refs
First #footnote[A]<fn1> \
@@ -163,10 +248,7 @@ Ref @fn
.map(v => upper(v) + footnote(v))
)
---- issue-multiple-footnote-in-one-line ---
-// Test that the logic that keeps footnote entry together with
-// their markers also works for multiple footnotes in a single
-// line.
+--- footnote-multiple-in-one-line ---
#set page(height: 100pt)
#v(50pt)
A #footnote[a]
diff --git a/tests/suite/layout/flow/place-float.typ b/tests/suite/layout/flow/place-float.typ
deleted file mode 100644
index 50a8a112..00000000
--- a/tests/suite/layout/flow/place-float.typ
+++ /dev/null
@@ -1,83 +0,0 @@
---- place-float-flow-around ---
-#set page(height: 80pt)
-#set place(float: true)
-#place(bottom + center, rect(height: 20pt))
-#lines(4)
-
---- place-float-queued ---
-#set page(height: 180pt)
-#set figure(placement: auto)
-
-#figure(rect(height: 60pt), caption: [I])
-#figure(rect(height: 40pt), caption: [II])
-#figure(rect(), caption: [III])
-#figure(rect(), caption: [IV])
-A
-
---- place-float-align-auto ---
-#set page(height: 140pt)
-#set place(clearance: 5pt)
-#set place(auto, float: true)
-
-#place(rect[A])
-#place(rect[B])
-1 \ 2
-#place(rect[C])
-#place(rect[D])
-
---- place-float-in-column-align-auto ---
-#set page(height: 150pt, columns: 2)
-#set place(auto, float: true, clearance: 10pt)
-#set rect(width: 75%)
-
-#place(rect[I])
-#place(rect[II])
-#place(rect[III])
-#place(rect[IV])
-
-#lines(6)
-
-#place(rect[V])
-
---- place-float-in-column-queued ---
-#set page(height: 100pt, columns: 2)
-#set place(float: true, clearance: 10pt)
-#set rect(width: 75%)
-#set text(costs: (widow: 0%, orphan: 0%))
-
-#lines(3)
-
-#place(top, rect[I])
-#place(top, rect[II])
-#place(bottom, rect[III])
-
-#lines(3)
-
---- place-float-missing ---
-// Error: 2-20 automatic positioning is only available for floating placement
-// Hint: 2-20 you can enable floating placement with `place(float: true, ..)`
-#place(auto)[Hello]
-
---- place-float-center-horizon ---
-// Error: 2-45 floating placement must be `auto`, `top`, or `bottom`
-#place(center + horizon, float: true)[Hello]
-
---- place-float-horizon ---
-// Error: 2-36 floating placement must be `auto`, `top`, or `bottom`
-#place(horizon, float: true)[Hello]
-
---- place-float-default ---
-// Error: 2-27 floating placement must be `auto`, `top`, or `bottom`
-#place(float: true)[Hello]
-
---- place-float-right ---
-// Error: 2-34 floating placement must be `auto`, `top`, or `bottom`
-#place(right, float: true)[Hello]
-
---- issue-2595-float-overlap ---
-#set page(height: 80pt)
-
-1
-#place(auto, float: true, block(height: 100%, width: 100%, fill: aqua))
-#place(auto, float: true, block(height: 100%, width: 100%, fill: red))
-#lines(7)
diff --git a/tests/suite/layout/flow/place-flush.typ b/tests/suite/layout/flow/place-flush.typ
deleted file mode 100644
index 8f55a6fd..00000000
--- a/tests/suite/layout/flow/place-flush.typ
+++ /dev/null
@@ -1,29 +0,0 @@
---- place-flush ---
-#set page(height: 120pt)
-#let floater(align, height) = place(
- align,
- float: true,
- rect(width: 100%, height: height),
-)
-
-#floater(top, 30pt)
-A
-
-#floater(bottom, 50pt)
-#place.flush()
-B // Should be on the second page.
-
---- place-flush-figure ---
-#set page(height: 120pt)
-#let floater(align, height, caption) = figure(
- placement: align,
- caption: caption,
- rect(width: 100%, height: height),
-)
-
-#floater(top, 30pt)[I]
-A
-
-#floater(bottom, 50pt)[II]
-#place.flush()
-B // Should be on the second page.
diff --git a/tests/suite/layout/flow/place.typ b/tests/suite/layout/flow/place.typ
index f3231735..dc655ec5 100644
--- a/tests/suite/layout/flow/place.typ
+++ b/tests/suite/layout/flow/place.typ
@@ -70,6 +70,288 @@ Second
#line(length: 50pt)
]
+--- place-float-flow-around ---
+#set page(height: 80pt)
+#set place(float: true)
+#place(bottom + center, rect(height: 20pt))
+#lines(4)
+
+--- place-float-queued ---
+#set page(height: 180pt)
+#set figure(placement: auto)
+
+#figure(rect(height: 60pt), caption: [I])
+#figure(rect(height: 40pt), caption: [II])
+#figure(rect(), caption: [III])
+A
+#figure(rect(), caption: [IV])
+
+--- place-float-align-auto ---
+#set page(height: 140pt)
+#set place(auto, float: true, clearance: 5pt)
+
+#place(rect[A])
+#place(rect[B])
+1 \ 2
+#place(rect[C])
+#place(rect[D])
+
+--- place-float-delta ---
+#place(top + center, float: true, dx: 10pt, rect[I])
+A
+#place(bottom + center, float: true, dx: -10pt, rect[II])
+
+--- place-float-flow-size ---
+#set page(width: auto, height: auto)
+#set place(float: true, clearance: 5pt)
+
+#place(bottom, rect(width: 80pt, height: 10pt))
+#place(top + center, rect(height: 20pt))
+#align(center)[A]
+#pagebreak()
+#align(center)[B]
+#place(bottom, scope: "page", rect(height: 10pt))
+
+--- place-float-flow-size-alone ---
+#set page(width: auto, height: auto)
+#set place(float: true, clearance: 5pt)
+#place(auto)[A]
+
+--- place-float-fr ---
+#set page(height: 120pt, columns: 2)
+#set place(float: true, clearance: 10pt)
+#set rect(width: 70%)
+
+#place(top + center, rect[I])
+#place(bottom + center, scope: "page", rect[II])
+
+A
+#v(1fr)
+B
+#colbreak()
+C
+#align(bottom)[D]
+
+--- place-float-rel-sizing ---
+#set page(height: 100pt, columns: 2)
+#set place(float: true, clearance: 10pt)
+#set rect(width: 70%)
+
+#place(top + center, scope: "page", rect[I])
+#place(top + center, rect[II])
+
+// This test result is not ideal: The first column takes 30% of the full page,
+// while the second takes 30% of the remaining space since there is no concept
+// of `full` for followup pages.
+#set align(bottom)
+#rect(width: 100%, height: 30%)
+#rect(width: 100%, height: 30%)
+
+--- place-float-block-backlog ---
+#set page(height: 100pt)
+#v(60pt)
+#place(top, float: true, rect())
+#list(.."ABCDEFGHIJ".clusters())
+
+--- place-float-clearance-empty ---
+// Check that we don't require space for clearance if there is no content.
+#set page(height: 100pt)
+#v(1fr)
+#table(
+ columns: (1fr, 1fr),
+ lines(2),
+ [],
+ lines(8),
+ place(auto, float: true, block(width: 100%, height: 100%, fill: aqua))
+)
+
+
+--- place-float-column-align-auto ---
+#set page(height: 150pt, columns: 2)
+#set place(auto, float: true, clearance: 10pt)
+#set rect(width: 75%)
+
+#place(rect[I])
+#place(rect[II])
+#place(rect[III])
+#place(rect[IV])
+
+#lines(6)
+
+#place(rect[V])
+#place(rect[VI])
+
+--- place-float-column-queued ---
+#set page(height: 100pt, columns: 2)
+#set place(float: true, clearance: 10pt)
+#set rect(width: 75%)
+#set text(costs: (widow: 0%, orphan: 0%))
+
+#lines(3)
+
+#place(top, rect[I])
+#place(top, rect[II])
+#place(bottom, rect[III])
+
+#lines(3)
+
+--- place-float-twocolumn ---
+#set page(height: 100pt, columns: 2)
+#set place(float: true, clearance: 10pt)
+#set rect(width: 70%)
+
+#place(top + center, scope: "page", rect[I])
+#place(top + center, rect[II])
+#lines(4)
+#place(top + center, rect[III])
+#block(width: 100%, height: 70pt, fill: conifer)
+#place(bottom + center, scope: "page", rect[IV])
+#place(bottom + center, rect[V])
+#v(1pt, weak: true)
+#block(width: 100%, height: 60pt, fill: aqua)
+
+--- place-float-twocolumn-queued ---
+#set page(height: 100pt, columns: 2)
+#set place(float: true, scope: "page", clearance: 10pt)
+#let t(align, fill) = place(top + align, rect(fill: fill, height: 25pt))
+
+#t(left, aqua)
+#t(center, forest)
+#t(right, conifer)
+#lines(7)
+
+--- place-float-twocolumn-align-auto ---
+#set page(height: 100pt, columns: 2)
+#set place(float: true, clearance: 10pt)
+#set rect(width: 70%)
+
+#place(auto, scope: "page", rect[I]) // Should end up `top`
+#lines(4)
+#place(auto, scope: "page", rect[II]) // Should end up `bottom`
+#lines(4)
+
+--- place-float-twocolumn-fits ---
+#set page(height: 100pt, columns: 2)
+#set place(float: true, clearance: 10pt)
+#set rect(width: 70%)
+
+#lines(6)
+#place(auto, scope: "page", rect[I])
+#lines(12, "1")
+
+--- place-float-twocolumn-fits-not ---
+#set page(height: 100pt, columns: 2)
+#set place(float: true, clearance: 10pt)
+#set rect(width: 70%)
+
+#lines(10)
+#place(auto, scope: "page", rect[I])
+#lines(10, "1")
+
+--- place-float-threecolumn ---
+#set page(height: 100pt, columns: 3)
+#set place(float: true, clearance: 10pt)
+#set rect(width: 70%)
+
+#place(bottom + center, scope: "page", rect[I])
+#lines(21)
+#place(top + center, scope: "page", rect[II])
+
+--- place-float-threecolumn-block-backlog ---
+#set page(height: 100pt, columns: 3)
+#set place(float: true, clearance: 10pt)
+#set rect(width: 70%)
+
+// The most important part of this test is that we get the backlog of the
+// conifer (green) block right.
+#place(top + center, scope: "page", rect[I])
+#block(fill: aqua, width: 100%, height: 70pt)
+#block(fill: conifer, width: 100%, height: 160pt)
+#place(bottom + center, scope: "page", rect[II])
+#place(top, rect(height: 40%)[III])
+#block(fill: yellow, width: 100%, height: 60pt)
+
+--- place-float-counter ---
+#let c = counter("c")
+#let cd = context c.display()
+
+#set page(
+ height: 100pt,
+ margin: (y: 20pt),
+ header: [H: #cd],
+ footer: [F: #cd],
+ columns: 2,
+)
+
+#let t(align, scope: "column", n) = place(
+ align,
+ float: true,
+ scope: scope,
+ clearance: 10pt,
+ line(length: 100%) + c.update(n),
+)
+
+#t(bottom, 6)
+#cd
+#t(top, 3)
+#colbreak()
+#cd
+#t(scope: "page", bottom, 11)
+#colbreak()
+#cd
+#t(top, 12)
+
+--- place-float-missing ---
+// Error: 2-20 automatic positioning is only available for floating placement
+// Hint: 2-20 you can enable floating placement with `place(float: true, ..)`
+#place(auto)[Hello]
+
+--- place-float-center-horizon ---
+// Error: 2-45 vertical floating placement must be `auto`, `top`, or `bottom`
+#place(center + horizon, float: true)[Hello]
+
+--- place-float-horizon ---
+// Error: 2-36 vertical floating placement must be `auto`, `top`, or `bottom`
+#place(horizon, float: true)[Hello]
+
+--- place-float-default ---
+// Error: 2-27 vertical floating placement must be `auto`, `top`, or `bottom`
+#place(float: true)[Hello]
+
+--- place-float-right ---
+// Error: 2-34 vertical floating placement must be `auto`, `top`, or `bottom`
+#place(right, float: true)[Hello]
+
+--- place-flush ---
+#set page(height: 120pt)
+#let floater(align, height) = place(
+ align,
+ float: true,
+ rect(width: 100%, height: height),
+)
+
+#floater(top, 30pt)
+A
+
+#floater(bottom, 50pt)
+#place.flush()
+B // Should be on the second page.
+
+--- place-flush-figure ---
+#set page(height: 120pt)
+#let floater(align, height, caption) = figure(
+ placement: align,
+ caption: caption,
+ rect(width: 100%, height: height),
+)
+
+#floater(top, 30pt)[I]
+A
+
+#floater(bottom, 50pt)[II]
+#place.flush()
+B // Should be on the second page.
+
--- issue-place-base ---
// Test that placement is relative to container and not itself.
#set page(height: 80pt, margin: 0pt)
@@ -98,3 +380,11 @@ Paragraph after float.
Paragraph before place.
#place(rect())
Paragraph after place.
+
+--- issue-2595-float-overlap ---
+#set page(height: 80pt)
+
+1
+#place(auto, float: true, block(height: 100%, width: 100%, fill: aqua))
+#place(auto, float: true, block(height: 100%, width: 100%, fill: red))
+#lines(7)
diff --git a/tests/suite/model/cite.typ b/tests/suite/model/cite.typ
index ffbd3b52..f69fe9f4 100644
--- a/tests/suite/model/cite.typ
+++ b/tests/suite/model/cite.typ
@@ -102,8 +102,7 @@ B #cite(<netwok>) #cite(<arrgh>).
// Everything moves to the second page because we want to keep the line and
// its footnotes together.
-#footnote[@netwok]
-#footnote[A]
+#footnote[@netwok \ A]
#show bibliography: none
#bibliography("/assets/bib/works.bib")
diff --git a/tests/suite/model/figure.typ b/tests/suite/model/figure.typ
index 13e94481..d71d92e3 100644
--- a/tests/suite/model/figure.typ
+++ b/tests/suite/model/figure.typ
@@ -41,6 +41,42 @@ We can clearly see that @fig-cylinder and
caption: "A table containing images."
) <fig-image-in-table>
+--- figure-placement ---
+#set page(height: 160pt, columns: 2)
+#set place(clearance: 10pt)
+
+#lines(4)
+
+#figure(
+ placement: auto,
+ scope: "page",
+ caption: [I],
+ rect(height: 15pt, width: 80%),
+)
+
+#figure(
+ placement: bottom,
+ caption: [II],
+ rect(height: 15pt, width: 80%),
+)
+
+#lines(2)
+
+#figure(
+ placement: bottom,
+ caption: [III],
+ rect(height: 25pt, width: 80%),
+)
+
+#figure(
+ placement: auto,
+ scope: "page",
+ caption: [IV],
+ rect(width: 80%),
+)
+
+#lines(15)
+
--- figure-theorem ---
// Testing show rules with figures with a simple theorem display
#show figure.where(kind: "theorem"): it => {