New flow layout, with multi-column floats (#5017)

This commit is contained in:
Laurenz 2024-09-25 10:26:41 +02:00 committed by GitHub
parent fd449f3e08
commit e25389a85e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
82 changed files with 2884 additions and 1684 deletions

View File

@ -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.

View File

@ -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:
///
/// #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.
/// ]
/// ```example:single
/// #set page(columns: 2, height: 150pt)
///
/// #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()))
}

View File

@ -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,50 +188,21 @@ 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))
}
// 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);
}
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);
}
// 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)
}
}
/// An inline-level container that can produce arbitrary items that can break
@ -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>(

View File

@ -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,40 +30,94 @@ 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)
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());
}
}
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),
});
} 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>() {
}
/// 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 lines = crate::layout::layout_inline(
engine,
self.engine,
&elem.children,
locator.next(&elem.span()),
self.locator.next(&elem.span()),
styles,
last_was_par,
base,
expand,
self.last_was_par,
self.base,
self.expand,
)?
.into_frames();
output.push(Child::Rel(spacing.into(), 4));
self.output.push(Child::Rel(spacing.into(), 4));
// Determine whether to prevent widow and orphans.
let len = lines.len();
@ -100,7 +137,7 @@ pub fn collect<'a>(
for (i, frame) in lines.into_iter().enumerate() {
if i > 0 {
output.push(Child::Rel(leading.into(), 5));
self.output.push(Child::Rel(leading.into(), 5));
}
// To prevent widows and orphans, we require enough space for
@ -117,17 +154,27 @@ pub fn collect<'a>(
frame.height()
};
let child = LineChild { frame, align, need };
output.push(Child::Line(BumpBox::new_in(child, bump)));
self.output
.push(Child::Line(self.boxed(LineChild { frame, align, need })));
}
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());
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 rootable = elem.rootable(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 {
@ -136,29 +183,53 @@ pub fn collect<'a>(
Smart::Custom(Spacing::Fr(fr)) => Child::Fr(fr),
};
output.push(spacing(elem.above(styles)));
self.output.push(spacing(elem.above(styles)));
let child = BlockChild { align, sticky, rootable, elem, styles, locator };
output.push(Child::Block(BumpBox::new_in(child, bump)));
if !breakable || sticky || fr.is_some() {
self.output.push(Child::Single(self.boxed(SingleChild {
align,
sticky,
fr,
elem,
styles,
locator,
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(),
})));
};
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(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(),
"floating placement must be `auto`, `top`, or `bottom`"
"vertical floating placement must be `auto`, `top`, or `bottom`"
),
(false, Smart::Auto) => bail!(
elem.span(),
@ -168,97 +239,301 @@ pub fn collect<'a>(
_ => {}
}
let child = PlacedChild {
if !float && scope == PlacementScope::Page {
bail!(
elem.span(),
"page-scoped positioning is currently only available for floating placement";
hint: "you can enable floating placement with `place(float: true, ..)`"
);
}
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,
align_x,
align_y,
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>() {
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());
cell: CachedCell::new(),
})));
Ok(())
}
/// 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)
}
}
Ok(output)
/// 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 paragraph line.
/// 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
})
})
}
}
Ok(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,
};
// 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((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> {
self.cell.get_or_init(base, |base| {
let align = self.alignment.unwrap_or_else(|| Alignment::CENTER);
let aligned = AlignElem::set_alignment(align).wrap();
let mut frame = layout_frame(
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))
})
}
}
frame.post_process(self.styles);
Ok(frame)
/// 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(..)")
}
}

View File

@ -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()));
}
}
_ => {}
}
}
}

View File

@ -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);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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(

View File

@ -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());

View File

@ -1,3 +1,5 @@
//! Layout of content into a [`Document`].
mod collect;
mod finalize;
mod run;

View File

@ -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.

View File

@ -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.
///

View File

@ -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

View File

@ -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),

View File

@ -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());
}

View File

@ -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()),

View File

@ -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,

View File

@ -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>>,

View File

@ -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.

View File

@ -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.
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's [`place`
function]($place) can temporarily escape the two-column layout by specifying
`{float: true}` and `{scope: "page"}`:
```example:single
>>> #set page(height: 180pt)
#set page(columns: 2)
#set par(justify: true)
#place(
top + center,
float: true,
scope: "page",
text(1.4em, weight: "bold")[
Impacts of Odobenidae
],
)
== About seals in the wild
#lorem(80)
```
_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).
### 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:
```example:single
>>> #set page(height: 180pt)
= Impacts of Odobenidae
#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)
== 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.
Another use of the `columns` function is to create columns inside of a container
like a rectangle or to customize gutter size:
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(

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 570 B

BIN
tests/ref/block-sticky.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 469 B

View File

Before

Width:  |  Height:  |  Size: 464 B

After

Width:  |  Height:  |  Size: 464 B

BIN
tests/ref/colbreak-weak.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 620 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 693 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 743 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 291 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 602 B

After

Width:  |  Height:  |  Size: 504 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 704 B

After

Width:  |  Height:  |  Size: 678 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 777 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 571 B

After

Width:  |  Height:  |  Size: 512 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 826 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 932 B

View File

Before

Width:  |  Height:  |  Size: 638 B

After

Width:  |  Height:  |  Size: 638 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 674 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 347 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 507 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 843 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 707 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 719 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1001 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

0
tests/skip.txt Normal file
View File

View File

@ -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;

View File

@ -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;

View File

@ -25,10 +25,10 @@
#outline()
= Introduction
#v(1cm)
#lines(1)
= Background
#v(2cm)
#lines(2)
= Approach

View File

@ -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]

View File

@ -122,3 +122,10 @@ Hallo
= B
Text
]
--- colbreak-weak ---
#set page(columns: 2)
#colbreak(weak: true)
A
#colbreak(weak: true)
B

View File

@ -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)]

View File

@ -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)
]

View File

@ -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`.
#set page(height: 120pt)
#align(center, strong[Title])
--- 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)
#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]

View File

@ -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)

View File

@ -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.

View File

@ -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)

View File

@ -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")

View File

@ -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 => {