New flow layout, with multi-column floats (#5017)
@ -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.
|
||||
|
@ -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()))
|
||||
}
|
||||
|
@ -143,7 +143,7 @@ impl Packed<BoxElem> {
|
||||
let inset = self.inset(styles).unwrap_or_default();
|
||||
|
||||
// Build the pod region.
|
||||
let pod = Self::pod(&width, &height, &inset, styles, region);
|
||||
let pod = unbreakable_pod(&width, &height.into(), &inset, styles, region);
|
||||
|
||||
// Layout the body.
|
||||
let mut frame = match self.body(styles) {
|
||||
@ -166,14 +166,6 @@ impl Packed<BoxElem> {
|
||||
crate::layout::grow(&mut frame, &inset);
|
||||
}
|
||||
|
||||
// Apply baseline shift. Do this after setting the size and applying the
|
||||
// inset, so that a relative shift is resolved relative to the final
|
||||
// height.
|
||||
let shift = self.baseline(styles).relative_to(frame.height());
|
||||
if !shift.is_zero() {
|
||||
frame.set_baseline(frame.baseline() - shift);
|
||||
}
|
||||
|
||||
// Prepare fill and stroke.
|
||||
let fill = self.fill(styles);
|
||||
let stroke = self
|
||||
@ -196,49 +188,20 @@ impl Packed<BoxElem> {
|
||||
frame.fill_and_stroke(fill, &stroke, &outset, &radius, self.span());
|
||||
}
|
||||
|
||||
// Assign label to the frame.
|
||||
if let Some(label) = self.label() {
|
||||
frame.group(|group| group.label = Some(label))
|
||||
}
|
||||
|
||||
Ok(frame)
|
||||
}
|
||||
|
||||
/// Builds the pod region for box layout.
|
||||
fn pod(
|
||||
width: &Sizing,
|
||||
height: &Smart<Rel>,
|
||||
inset: &Sides<Rel<Abs>>,
|
||||
styles: StyleChain,
|
||||
region: Size,
|
||||
) -> Region {
|
||||
// Resolve the size.
|
||||
let mut size = Size::new(
|
||||
match width {
|
||||
// For auto, the whole region is available.
|
||||
Sizing::Auto => region.x,
|
||||
// Resolve the relative sizing.
|
||||
Sizing::Rel(rel) => rel.resolve(styles).relative_to(region.x),
|
||||
// Fr is handled outside and already factored into the `region`,
|
||||
// so we can treat it equivalently to 100%.
|
||||
Sizing::Fr(_) => region.x,
|
||||
},
|
||||
match height {
|
||||
// See above. Note that fr is not supported on this axis.
|
||||
Smart::Auto => region.y,
|
||||
Smart::Custom(rel) => rel.resolve(styles).relative_to(region.y),
|
||||
},
|
||||
);
|
||||
|
||||
// Take the inset, if any, into account.
|
||||
if !inset.is_zero() {
|
||||
size = crate::layout::shrink(size, inset);
|
||||
// Apply baseline shift. Do this after setting the size and applying the
|
||||
// inset, so that a relative shift is resolved relative to the final
|
||||
// height.
|
||||
let shift = self.baseline(styles).relative_to(frame.height());
|
||||
if !shift.is_zero() {
|
||||
frame.set_baseline(frame.baseline() - shift);
|
||||
}
|
||||
|
||||
// If the child is not auto-sized, the size is forced and we should
|
||||
// enable expansion.
|
||||
let expand = Axes::new(*width != Sizing::Auto, *height != Smart::Auto);
|
||||
|
||||
Region::new(size, expand)
|
||||
Ok(frame)
|
||||
}
|
||||
}
|
||||
|
||||
@ -355,7 +318,7 @@ pub struct BlockElem {
|
||||
/// fill: aqua,
|
||||
/// )
|
||||
/// ```
|
||||
pub height: Smart<Rel<Length>>,
|
||||
pub height: Sizing,
|
||||
|
||||
/// Whether the block can be broken and continue on the next page.
|
||||
///
|
||||
@ -453,20 +416,16 @@ pub struct BlockElem {
|
||||
#[default(false)]
|
||||
pub clip: bool,
|
||||
|
||||
/// Whether this block must stick to the following one.
|
||||
/// Whether this block must stick to the following one, with no break in
|
||||
/// between.
|
||||
///
|
||||
/// Use this to prevent page breaks between e.g. a heading and its body.
|
||||
#[internal]
|
||||
/// This is, by default, set on heading blocks to prevent orphaned headings
|
||||
/// at the bottom of the page.
|
||||
///
|
||||
/// Marking a block as sticky makes it unbreakable.
|
||||
#[default(false)]
|
||||
#[parse(None)]
|
||||
pub sticky: bool,
|
||||
|
||||
/// Whether this block can host footnotes.
|
||||
#[internal]
|
||||
#[default(false)]
|
||||
#[parse(None)]
|
||||
pub rootable: bool,
|
||||
|
||||
/// The contents of the block.
|
||||
#[positional]
|
||||
#[borrowed]
|
||||
@ -513,9 +472,97 @@ impl BlockElem {
|
||||
}
|
||||
|
||||
impl Packed<BlockElem> {
|
||||
/// Layout this block as part of a flow.
|
||||
/// Lay this out as an unbreakable block.
|
||||
#[typst_macros::time(name = "block", span = self.span())]
|
||||
pub fn layout(
|
||||
pub fn layout_single(
|
||||
&self,
|
||||
engine: &mut Engine,
|
||||
locator: Locator,
|
||||
styles: StyleChain,
|
||||
base: Size,
|
||||
) -> SourceResult<Frame> {
|
||||
// Fetch sizing properties.
|
||||
let width = self.width(styles);
|
||||
let height = self.height(styles);
|
||||
let inset = self.inset(styles).unwrap_or_default();
|
||||
|
||||
// Build the pod regions.
|
||||
let pod = unbreakable_pod(&width.into(), &height, &inset, styles, base);
|
||||
|
||||
// Layout the body.
|
||||
let body = self.body(styles);
|
||||
let mut frame = match body {
|
||||
// If we have no body, just create one frame. Its size will be
|
||||
// adjusted below.
|
||||
None => Frame::hard(Size::zero()),
|
||||
|
||||
// If we have content as our body, just layout it.
|
||||
Some(BlockBody::Content(body)) => {
|
||||
layout_frame(engine, body, locator.relayout(), styles, pod)?
|
||||
}
|
||||
|
||||
// If we have a child that wants to layout with just access to the
|
||||
// base region, give it that.
|
||||
Some(BlockBody::SingleLayouter(callback)) => {
|
||||
callback.call(engine, locator, styles, pod)?
|
||||
}
|
||||
|
||||
// If we have a child that wants to layout with full region access,
|
||||
// we layout it.
|
||||
Some(BlockBody::MultiLayouter(callback)) => {
|
||||
callback.call(engine, locator, styles, pod.into())?.into_frame()
|
||||
}
|
||||
};
|
||||
|
||||
// Explicit blocks are boundaries for gradient relativeness.
|
||||
if matches!(body, None | Some(BlockBody::Content(_))) {
|
||||
frame.set_kind(FrameKind::Hard);
|
||||
}
|
||||
|
||||
// Enforce a correct frame size on the expanded axes. Do this before
|
||||
// applying the inset, since the pod shrunk.
|
||||
frame.set_size(pod.expand.select(pod.size, frame.size()));
|
||||
|
||||
// Apply the inset.
|
||||
if !inset.is_zero() {
|
||||
crate::layout::grow(&mut frame, &inset);
|
||||
}
|
||||
|
||||
// Prepare fill and stroke.
|
||||
let fill = self.fill(styles);
|
||||
let stroke = self
|
||||
.stroke(styles)
|
||||
.unwrap_or_default()
|
||||
.map(|s| s.map(Stroke::unwrap_or_default));
|
||||
|
||||
// Only fetch these if necessary (for clipping or filling/stroking).
|
||||
let outset = Lazy::new(|| self.outset(styles).unwrap_or_default());
|
||||
let radius = Lazy::new(|| self.radius(styles).unwrap_or_default());
|
||||
|
||||
// Clip the contents, if requested.
|
||||
if self.clip(styles) {
|
||||
let size = frame.size() + outset.relative_to(frame.size()).sum_by_axis();
|
||||
frame.clip(clip_rect(size, &radius, &stroke));
|
||||
}
|
||||
|
||||
// Add fill and/or stroke.
|
||||
if fill.is_some() || stroke.iter().any(Option::is_some) {
|
||||
frame.fill_and_stroke(fill, &stroke, &outset, &radius, self.span());
|
||||
}
|
||||
|
||||
// Assign label to each frame in the fragment.
|
||||
if let Some(label) = self.label() {
|
||||
frame.group(|group| group.label = Some(label));
|
||||
}
|
||||
|
||||
Ok(frame)
|
||||
}
|
||||
}
|
||||
|
||||
impl Packed<BlockElem> {
|
||||
/// Lay this out as a breakable block.
|
||||
#[typst_macros::time(name = "block", span = self.span())]
|
||||
pub fn layout_multiple(
|
||||
&self,
|
||||
engine: &mut Engine,
|
||||
locator: Locator,
|
||||
@ -526,14 +573,13 @@ impl Packed<BlockElem> {
|
||||
let width = self.width(styles);
|
||||
let height = self.height(styles);
|
||||
let inset = self.inset(styles).unwrap_or_default();
|
||||
let breakable = self.breakable(styles);
|
||||
|
||||
// Allocate a small vector for backlogs.
|
||||
let mut buf = SmallVec::<[Abs; 2]>::new();
|
||||
|
||||
// Build the pod regions.
|
||||
let pod =
|
||||
Self::pod(&width, &height, &inset, breakable, styles, regions, &mut buf);
|
||||
breakable_pod(&width.into(), &height, &inset, styles, regions, &mut buf);
|
||||
|
||||
// Layout the body.
|
||||
let body = self.body(styles);
|
||||
@ -673,116 +719,6 @@ impl Packed<BlockElem> {
|
||||
|
||||
Ok(fragment)
|
||||
}
|
||||
|
||||
/// Builds the pod regions for block layout.
|
||||
///
|
||||
/// If `breakable` is `false`, this will only ever return a single region.
|
||||
fn pod<'a>(
|
||||
width: &Smart<Rel>,
|
||||
height: &Smart<Rel>,
|
||||
inset: &Sides<Rel<Abs>>,
|
||||
breakable: bool,
|
||||
styles: StyleChain,
|
||||
regions: Regions,
|
||||
buf: &'a mut SmallVec<[Abs; 2]>,
|
||||
) -> Regions<'a> {
|
||||
let base = regions.base();
|
||||
|
||||
// The vertical region sizes we're about to build.
|
||||
let first;
|
||||
let full;
|
||||
let backlog: &mut [Abs];
|
||||
let last;
|
||||
|
||||
// If the block has a fixed height, things are very different, so we
|
||||
// handle that case completely separately.
|
||||
match height {
|
||||
Smart::Auto => {
|
||||
if breakable {
|
||||
// If the block automatically sized and breakable, we can
|
||||
// just inherit the regions.
|
||||
first = regions.size.y;
|
||||
buf.extend_from_slice(regions.backlog);
|
||||
backlog = buf;
|
||||
last = regions.last;
|
||||
} else {
|
||||
// If the block is automatically sized, but not breakable,
|
||||
// we provide the full base height. It doesn't really make
|
||||
// sense to provide just the remaining height to an
|
||||
// unbreakable block.
|
||||
first = regions.full;
|
||||
backlog = &mut [];
|
||||
last = None;
|
||||
}
|
||||
|
||||
// Since we're automatically sized, we inherit the base size.
|
||||
full = regions.full;
|
||||
}
|
||||
|
||||
Smart::Custom(rel) => {
|
||||
// Resolve the sizing to a concrete size.
|
||||
let resolved = rel.resolve(styles).relative_to(base.y);
|
||||
|
||||
if breakable {
|
||||
// If the block is fixed-height and breakable, distribute
|
||||
// the fixed height across a start region and a backlog.
|
||||
(first, backlog) = distribute(resolved, regions, buf);
|
||||
} else {
|
||||
// If the block is fixed-height, but not breakable, the
|
||||
// fixed height is all in the first region, and we have no
|
||||
// backlog.
|
||||
first = resolved;
|
||||
backlog = &mut [];
|
||||
}
|
||||
|
||||
// Since we're manually sized, the resolved size is also the
|
||||
// base height.
|
||||
full = resolved;
|
||||
|
||||
// If the height is manually sized, we don't want a final
|
||||
// repeatable region.
|
||||
last = None;
|
||||
}
|
||||
};
|
||||
|
||||
// Resolve the horizontal sizing to a concrete width and combine
|
||||
// `width` and `first` into `size`.
|
||||
let mut size = Size::new(
|
||||
match width {
|
||||
Smart::Auto => regions.size.x,
|
||||
Smart::Custom(rel) => rel.resolve(styles).relative_to(base.x),
|
||||
},
|
||||
first,
|
||||
);
|
||||
|
||||
// Take the inset, if any, into account, applying it to the
|
||||
// individual region components.
|
||||
let (mut full, mut last) = (full, last);
|
||||
if !inset.is_zero() {
|
||||
crate::layout::shrink_multiple(
|
||||
&mut size, &mut full, backlog, &mut last, inset,
|
||||
);
|
||||
}
|
||||
|
||||
// If the child is manually sized along an axis (i.e. not `auto`), then
|
||||
// it should expand along that axis. We also ensure that we only expand
|
||||
// if the size is finite because it just doesn't make sense to expand
|
||||
// into infinite regions.
|
||||
let expand = Axes::new(*width != Smart::Auto, *height != Smart::Auto)
|
||||
& size.map(Abs::is_finite);
|
||||
|
||||
Regions {
|
||||
size,
|
||||
full,
|
||||
backlog,
|
||||
last,
|
||||
expand,
|
||||
// This will only ever be set by the flow if the block is
|
||||
// `rootable`. It is important that we propagate this, so that
|
||||
// columns can hold footnotes.
|
||||
root: regions.root,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The contents of a block.
|
||||
@ -873,6 +809,118 @@ cast! {
|
||||
v: Fr => Self::Fr(v),
|
||||
}
|
||||
|
||||
/// Builds the pod region for an unbreakable sized container.
|
||||
fn unbreakable_pod(
|
||||
width: &Sizing,
|
||||
height: &Sizing,
|
||||
inset: &Sides<Rel<Abs>>,
|
||||
styles: StyleChain,
|
||||
base: Size,
|
||||
) -> Region {
|
||||
// Resolve the size.
|
||||
let mut size = Size::new(
|
||||
match width {
|
||||
// - For auto, the whole region is available.
|
||||
// - Fr is handled outside and already factored into the `region`,
|
||||
// so we can treat it equivalently to 100%.
|
||||
Sizing::Auto | Sizing::Fr(_) => base.x,
|
||||
// Resolve the relative sizing.
|
||||
Sizing::Rel(rel) => rel.resolve(styles).relative_to(base.x),
|
||||
},
|
||||
match height {
|
||||
Sizing::Auto | Sizing::Fr(_) => base.y,
|
||||
Sizing::Rel(rel) => rel.resolve(styles).relative_to(base.y),
|
||||
},
|
||||
);
|
||||
|
||||
// Take the inset, if any, into account.
|
||||
if !inset.is_zero() {
|
||||
size = crate::layout::shrink(size, inset);
|
||||
}
|
||||
|
||||
// If the child is manually, the size is forced and we should enable
|
||||
// expansion.
|
||||
let expand = Axes::new(
|
||||
*width != Sizing::Auto && size.x.is_finite(),
|
||||
*height != Sizing::Auto && size.y.is_finite(),
|
||||
);
|
||||
|
||||
Region::new(size, expand)
|
||||
}
|
||||
|
||||
/// Builds the pod regions for a breakable sized container.
|
||||
fn breakable_pod<'a>(
|
||||
width: &Sizing,
|
||||
height: &Sizing,
|
||||
inset: &Sides<Rel<Abs>>,
|
||||
styles: StyleChain,
|
||||
regions: Regions,
|
||||
buf: &'a mut SmallVec<[Abs; 2]>,
|
||||
) -> Regions<'a> {
|
||||
let base = regions.base();
|
||||
|
||||
// The vertical region sizes we're about to build.
|
||||
let first;
|
||||
let full;
|
||||
let backlog: &mut [Abs];
|
||||
let last;
|
||||
|
||||
// If the block has a fixed height, things are very different, so we
|
||||
// handle that case completely separately.
|
||||
match height {
|
||||
Sizing::Auto | Sizing::Fr(_) => {
|
||||
// If the block is automatically sized, we can just inherit the
|
||||
// regions.
|
||||
first = regions.size.y;
|
||||
full = regions.full;
|
||||
buf.extend_from_slice(regions.backlog);
|
||||
backlog = buf;
|
||||
last = regions.last;
|
||||
}
|
||||
|
||||
Sizing::Rel(rel) => {
|
||||
// Resolve the sizing to a concrete size.
|
||||
let resolved = rel.resolve(styles).relative_to(base.y);
|
||||
|
||||
// Since we're manually sized, the resolved size is the base height.
|
||||
full = resolved;
|
||||
|
||||
// Distribute the fixed height across a start region and a backlog.
|
||||
(first, backlog) = distribute(resolved, regions, buf);
|
||||
|
||||
// If the height is manually sized, we don't want a final repeatable
|
||||
// region.
|
||||
last = None;
|
||||
}
|
||||
};
|
||||
|
||||
// Resolve the horizontal sizing to a concrete width and combine
|
||||
// `width` and `first` into `size`.
|
||||
let mut size = Size::new(
|
||||
match width {
|
||||
Sizing::Auto | Sizing::Fr(_) => regions.size.x,
|
||||
Sizing::Rel(rel) => rel.resolve(styles).relative_to(base.x),
|
||||
},
|
||||
first,
|
||||
);
|
||||
|
||||
// Take the inset, if any, into account, applying it to the
|
||||
// individual region components.
|
||||
let (mut full, mut last) = (full, last);
|
||||
if !inset.is_zero() {
|
||||
crate::layout::shrink_multiple(&mut size, &mut full, backlog, &mut last, inset);
|
||||
}
|
||||
|
||||
// If the child is manually, the size is forced and we should enable
|
||||
// expansion.
|
||||
let expand = Axes::new(
|
||||
*width != Sizing::Auto && size.x.is_finite(),
|
||||
*height != Sizing::Auto && size.y.is_finite(),
|
||||
);
|
||||
|
||||
Regions { size, full, backlog, last, expand }
|
||||
}
|
||||
|
||||
/// Distribute a fixed height spread over existing regions into a new first
|
||||
/// height and a new backlog.
|
||||
fn distribute<'a>(
|
||||
|
@ -1,3 +1,7 @@
|
||||
use std::cell::RefCell;
|
||||
use std::fmt::{self, Debug, Formatter};
|
||||
use std::hash::Hash;
|
||||
|
||||
use bumpalo::boxed::Box as BumpBox;
|
||||
use bumpalo::Bump;
|
||||
use once_cell::unsync::Lazy;
|
||||
@ -5,39 +9,18 @@ use once_cell::unsync::Lazy;
|
||||
use crate::diag::{bail, SourceResult};
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{Packed, Resolve, Smart, StyleChain};
|
||||
use crate::introspection::{Locator, Tag, TagElem};
|
||||
use crate::introspection::{Locator, SplitLocator, Tag, TagElem};
|
||||
use crate::layout::{
|
||||
layout_frame, Abs, AlignElem, Alignment, Axes, BlockElem, ColbreakElem,
|
||||
FixedAlignment, FlushElem, Fr, Fragment, Frame, PagebreakElem, PlaceElem, Ratio,
|
||||
Region, Regions, Rel, Size, Spacing, VElem,
|
||||
FixedAlignment, FlushElem, Fr, Fragment, Frame, PagebreakElem, PlaceElem,
|
||||
PlacementScope, Ratio, Region, Regions, Rel, Size, Sizing, Spacing, VElem,
|
||||
};
|
||||
use crate::model::ParElem;
|
||||
use crate::realize::Pair;
|
||||
use crate::text::TextElem;
|
||||
|
||||
/// A prepared child in flow layout.
|
||||
///
|
||||
/// The larger variants are bump-boxed to keep the enum size down.
|
||||
pub enum Child<'a> {
|
||||
/// An introspection tag.
|
||||
Tag(&'a Tag),
|
||||
/// Relative spacing with a specific weakness.
|
||||
Rel(Rel<Abs>, u8),
|
||||
/// Fractional spacing.
|
||||
Fr(Fr),
|
||||
/// An already layouted line of a paragraph.
|
||||
Line(BumpBox<'a, LineChild>),
|
||||
/// A potentially breakable block.
|
||||
Block(BumpBox<'a, BlockChild<'a>>),
|
||||
/// An absolutely or floatingly placed element.
|
||||
Placed(BumpBox<'a, PlacedChild<'a>>),
|
||||
/// A column break.
|
||||
Break(bool),
|
||||
/// A place flush.
|
||||
Flush,
|
||||
}
|
||||
|
||||
/// Collects all content of the flow into prepared children.
|
||||
/// Collects all elements of the flow into prepared children. These are much
|
||||
/// simpler to handle than the raw elements.
|
||||
#[typst_macros::time]
|
||||
pub fn collect<'a>(
|
||||
engine: &mut Engine,
|
||||
@ -47,218 +30,510 @@ pub fn collect<'a>(
|
||||
base: Size,
|
||||
expand: bool,
|
||||
) -> SourceResult<Vec<Child<'a>>> {
|
||||
let mut locator = locator.split();
|
||||
let mut output = Vec::with_capacity(children.len());
|
||||
let mut last_was_par = false;
|
||||
Collector {
|
||||
engine,
|
||||
bump,
|
||||
children,
|
||||
locator: locator.split(),
|
||||
base,
|
||||
expand,
|
||||
output: Vec::with_capacity(children.len()),
|
||||
last_was_par: false,
|
||||
}
|
||||
.run()
|
||||
}
|
||||
|
||||
for &(child, styles) in children {
|
||||
if let Some(elem) = child.to_packed::<TagElem>() {
|
||||
output.push(Child::Tag(&elem.tag));
|
||||
} else if let Some(elem) = child.to_packed::<VElem>() {
|
||||
output.push(match elem.amount {
|
||||
Spacing::Rel(rel) => {
|
||||
Child::Rel(rel.resolve(styles), elem.weak(styles) as u8)
|
||||
}
|
||||
Spacing::Fr(fr) => Child::Fr(fr),
|
||||
});
|
||||
} else if let Some(elem) = child.to_packed::<ColbreakElem>() {
|
||||
output.push(Child::Break(elem.weak(styles)));
|
||||
} else if let Some(elem) = child.to_packed::<ParElem>() {
|
||||
let align = AlignElem::alignment_in(styles).resolve(styles);
|
||||
let leading = ParElem::leading_in(styles);
|
||||
let spacing = ParElem::spacing_in(styles);
|
||||
let costs = TextElem::costs_in(styles);
|
||||
/// 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,
|
||||
}
|
||||
|
||||
let lines = crate::layout::layout_inline(
|
||||
engine,
|
||||
&elem.children,
|
||||
locator.next(&elem.span()),
|
||||
styles,
|
||||
last_was_par,
|
||||
base,
|
||||
expand,
|
||||
)?
|
||||
.into_frames();
|
||||
impl<'a> Collector<'a, '_, '_> {
|
||||
/// Perform the collection.
|
||||
fn run(mut self) -> SourceResult<Vec<Child<'a>>> {
|
||||
for (idx, &(child, styles)) in self.children.iter().enumerate() {
|
||||
if let Some(elem) = child.to_packed::<TagElem>() {
|
||||
self.output.push(Child::Tag(&elem.tag));
|
||||
} else if let Some(elem) = child.to_packed::<VElem>() {
|
||||
self.v(elem, styles);
|
||||
} else if let Some(elem) = child.to_packed::<ParElem>() {
|
||||
self.par(elem, styles)?;
|
||||
} else if let Some(elem) = child.to_packed::<BlockElem>() {
|
||||
self.block(elem, styles);
|
||||
} else if let Some(elem) = child.to_packed::<PlaceElem>() {
|
||||
self.place(idx, elem, styles)?;
|
||||
} else if child.is::<FlushElem>() {
|
||||
self.output.push(Child::Flush);
|
||||
} else if let Some(elem) = child.to_packed::<ColbreakElem>() {
|
||||
self.output.push(Child::Break(elem.weak(styles)));
|
||||
} else if child.is::<PagebreakElem>() {
|
||||
bail!(
|
||||
child.span(), "pagebreaks are not allowed inside of containers";
|
||||
hint: "try using a `#colbreak()` instead",
|
||||
);
|
||||
} else {
|
||||
bail!(child.span(), "{} is not allowed here", child.func().name());
|
||||
}
|
||||
}
|
||||
|
||||
output.push(Child::Rel(spacing.into(), 4));
|
||||
Ok(self.output)
|
||||
}
|
||||
|
||||
// Determine whether to prevent widow and orphans.
|
||||
let len = lines.len();
|
||||
let prevent_orphans =
|
||||
costs.orphan() > Ratio::zero() && len >= 2 && !lines[1].is_empty();
|
||||
let prevent_widows =
|
||||
costs.widow() > Ratio::zero() && len >= 2 && !lines[len - 2].is_empty();
|
||||
let prevent_all = len == 3 && prevent_orphans && prevent_widows;
|
||||
/// 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),
|
||||
});
|
||||
}
|
||||
|
||||
// Store the heights of lines at the edges because we'll potentially
|
||||
// need these later when `lines` is already moved.
|
||||
let height_at = |i| lines.get(i).map(Frame::height).unwrap_or_default();
|
||||
let front_1 = height_at(0);
|
||||
let front_2 = height_at(1);
|
||||
let back_2 = height_at(len.saturating_sub(2));
|
||||
let back_1 = height_at(len.saturating_sub(1));
|
||||
/// 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);
|
||||
|
||||
for (i, frame) in lines.into_iter().enumerate() {
|
||||
if i > 0 {
|
||||
output.push(Child::Rel(leading.into(), 5));
|
||||
}
|
||||
let lines = crate::layout::layout_inline(
|
||||
self.engine,
|
||||
&elem.children,
|
||||
self.locator.next(&elem.span()),
|
||||
styles,
|
||||
self.last_was_par,
|
||||
self.base,
|
||||
self.expand,
|
||||
)?
|
||||
.into_frames();
|
||||
|
||||
// To prevent widows and orphans, we require enough space for
|
||||
// - all lines if it's just three
|
||||
// - the first two lines if we're at the first line
|
||||
// - the last two lines if we're at the second to last line
|
||||
let need = if prevent_all && i == 0 {
|
||||
front_1 + leading + front_2 + leading + back_1
|
||||
} else if prevent_orphans && i == 0 {
|
||||
front_1 + leading + front_2
|
||||
} else if prevent_widows && i >= 2 && i + 2 == len {
|
||||
back_2 + leading + back_1
|
||||
} else {
|
||||
frame.height()
|
||||
};
|
||||
self.output.push(Child::Rel(spacing.into(), 4));
|
||||
|
||||
let child = LineChild { frame, align, need };
|
||||
output.push(Child::Line(BumpBox::new_in(child, bump)));
|
||||
// Determine whether to prevent widow and orphans.
|
||||
let len = lines.len();
|
||||
let prevent_orphans =
|
||||
costs.orphan() > Ratio::zero() && len >= 2 && !lines[1].is_empty();
|
||||
let prevent_widows =
|
||||
costs.widow() > Ratio::zero() && len >= 2 && !lines[len - 2].is_empty();
|
||||
let prevent_all = len == 3 && prevent_orphans && prevent_widows;
|
||||
|
||||
// Store the heights of lines at the edges because we'll potentially
|
||||
// need these later when `lines` is already moved.
|
||||
let height_at = |i| lines.get(i).map(Frame::height).unwrap_or_default();
|
||||
let front_1 = height_at(0);
|
||||
let front_2 = height_at(1);
|
||||
let back_2 = height_at(len.saturating_sub(2));
|
||||
let back_1 = height_at(len.saturating_sub(1));
|
||||
|
||||
for (i, frame) in lines.into_iter().enumerate() {
|
||||
if i > 0 {
|
||||
self.output.push(Child::Rel(leading.into(), 5));
|
||||
}
|
||||
|
||||
output.push(Child::Rel(spacing.into(), 4));
|
||||
last_was_par = true;
|
||||
} else if let Some(elem) = child.to_packed::<BlockElem>() {
|
||||
let locator = locator.next(&elem.span());
|
||||
let align = AlignElem::alignment_in(styles).resolve(styles);
|
||||
let sticky = elem.sticky(styles);
|
||||
let rootable = elem.rootable(styles);
|
||||
|
||||
let fallback = Lazy::new(|| ParElem::spacing_in(styles));
|
||||
let spacing = |amount| match amount {
|
||||
Smart::Auto => Child::Rel((*fallback).into(), 4),
|
||||
Smart::Custom(Spacing::Rel(rel)) => Child::Rel(rel.resolve(styles), 3),
|
||||
Smart::Custom(Spacing::Fr(fr)) => Child::Fr(fr),
|
||||
// To prevent widows and orphans, we require enough space for
|
||||
// - all lines if it's just three
|
||||
// - the first two lines if we're at the first line
|
||||
// - the last two lines if we're at the second to last line
|
||||
let need = if prevent_all && i == 0 {
|
||||
front_1 + leading + front_2 + leading + back_1
|
||||
} else if prevent_orphans && i == 0 {
|
||||
front_1 + leading + front_2
|
||||
} else if prevent_widows && i >= 2 && i + 2 == len {
|
||||
back_2 + leading + back_1
|
||||
} else {
|
||||
frame.height()
|
||||
};
|
||||
|
||||
output.push(spacing(elem.above(styles)));
|
||||
self.output
|
||||
.push(Child::Line(self.boxed(LineChild { frame, align, need })));
|
||||
}
|
||||
|
||||
let child = BlockChild { align, sticky, rootable, elem, styles, locator };
|
||||
output.push(Child::Block(BumpBox::new_in(child, bump)));
|
||||
self.output.push(Child::Rel(spacing.into(), 4));
|
||||
self.last_was_par = true;
|
||||
|
||||
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);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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)));
|
||||
/// Collect a block into a [`SingleChild`] or [`MultiChild`] depending on
|
||||
/// whether it is breakable.
|
||||
fn block(&mut self, elem: &'a Packed<BlockElem>, styles: StyleChain<'a>) {
|
||||
let locator = self.locator.next(&elem.span());
|
||||
let align = AlignElem::alignment_in(styles).resolve(styles);
|
||||
let sticky = elem.sticky(styles);
|
||||
let breakable = elem.breakable(styles);
|
||||
let fr = match elem.height(styles) {
|
||||
Sizing::Fr(fr) => Some(fr),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
match (float, align_y) {
|
||||
(true, Smart::Custom(None | Some(FixedAlignment::Center))) => bail!(
|
||||
elem.span(),
|
||||
"floating placement must be `auto`, `top`, or `bottom`"
|
||||
),
|
||||
(false, Smart::Auto) => bail!(
|
||||
elem.span(),
|
||||
"automatic positioning is only available for floating placement";
|
||||
hint: "you can enable floating placement with `place(float: true, ..)`"
|
||||
),
|
||||
_ => {}
|
||||
}
|
||||
let fallback = Lazy::new(|| ParElem::spacing_in(styles));
|
||||
let spacing = |amount| match amount {
|
||||
Smart::Auto => Child::Rel((*fallback).into(), 4),
|
||||
Smart::Custom(Spacing::Rel(rel)) => Child::Rel(rel.resolve(styles), 3),
|
||||
Smart::Custom(Spacing::Fr(fr)) => Child::Fr(fr),
|
||||
};
|
||||
|
||||
let child = PlacedChild {
|
||||
float,
|
||||
clearance,
|
||||
delta,
|
||||
align_x,
|
||||
align_y,
|
||||
self.output.push(spacing(elem.above(styles)));
|
||||
|
||||
if !breakable || sticky || fr.is_some() {
|
||||
self.output.push(Child::Single(self.boxed(SingleChild {
|
||||
align,
|
||||
sticky,
|
||||
fr,
|
||||
elem,
|
||||
styles,
|
||||
locator,
|
||||
alignment,
|
||||
};
|
||||
output.push(Child::Placed(BumpBox::new_in(child, bump)));
|
||||
} else if child.is::<FlushElem>() {
|
||||
output.push(Child::Flush);
|
||||
} else if child.is::<PagebreakElem>() {
|
||||
bail!(
|
||||
child.span(), "pagebreaks are not allowed inside of containers";
|
||||
hint: "try using a `#colbreak()` instead",
|
||||
);
|
||||
cell: CachedCell::new(),
|
||||
})));
|
||||
} else {
|
||||
bail!(child.span(), "{} is not allowed here", child.func().name());
|
||||
}
|
||||
let alone = self.children.len() == 1;
|
||||
self.output.push(Child::Multi(self.boxed(MultiChild {
|
||||
align,
|
||||
alone,
|
||||
elem,
|
||||
styles,
|
||||
locator,
|
||||
cell: CachedCell::new(),
|
||||
})));
|
||||
};
|
||||
|
||||
self.output.push(spacing(elem.below(styles)));
|
||||
self.last_was_par = false;
|
||||
}
|
||||
|
||||
Ok(output)
|
||||
/// Collects a placed element into a [`PlacedChild`].
|
||||
fn place(
|
||||
&mut self,
|
||||
idx: usize,
|
||||
elem: &'a Packed<PlaceElem>,
|
||||
styles: StyleChain<'a>,
|
||||
) -> SourceResult<()> {
|
||||
let alignment = elem.alignment(styles);
|
||||
let align_x = alignment.map_or(FixedAlignment::Center, |align| {
|
||||
align.x().unwrap_or_default().resolve(styles)
|
||||
});
|
||||
let align_y = alignment.map(|align| align.y().map(|y| y.resolve(styles)));
|
||||
let scope = elem.scope(styles);
|
||||
let float = elem.float(styles);
|
||||
|
||||
match (float, align_y) {
|
||||
(true, Smart::Custom(None | Some(FixedAlignment::Center))) => bail!(
|
||||
elem.span(),
|
||||
"vertical floating placement must be `auto`, `top`, or `bottom`"
|
||||
),
|
||||
(false, Smart::Auto) => bail!(
|
||||
elem.span(),
|
||||
"automatic positioning is only available for floating placement";
|
||||
hint: "you can enable floating placement with `place(float: true, ..)`"
|
||||
),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if !float && scope == PlacementScope::Page {
|
||||
bail!(
|
||||
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,
|
||||
elem,
|
||||
styles,
|
||||
locator,
|
||||
alignment,
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
/// A child that encapsulates a paragraph line.
|
||||
/// A prepared child in flow layout.
|
||||
///
|
||||
/// The larger variants are bump-boxed to keep the enum size down.
|
||||
#[derive(Debug)]
|
||||
pub enum Child<'a> {
|
||||
/// An introspection tag.
|
||||
Tag(&'a Tag),
|
||||
/// Relative spacing with a specific weakness level.
|
||||
Rel(Rel<Abs>, u8),
|
||||
/// Fractional spacing.
|
||||
Fr(Fr),
|
||||
/// An already layouted line of a paragraph.
|
||||
Line(BumpBox<'a, LineChild>),
|
||||
/// An unbreakable block.
|
||||
Single(BumpBox<'a, SingleChild<'a>>),
|
||||
/// A breakable block.
|
||||
Multi(BumpBox<'a, MultiChild<'a>>),
|
||||
/// An absolutely or floatingly placed element.
|
||||
Placed(BumpBox<'a, PlacedChild<'a>>),
|
||||
/// A place flush.
|
||||
Flush,
|
||||
/// An explicit column break.
|
||||
Break(bool),
|
||||
}
|
||||
|
||||
/// A child that encapsulates a layouted line of a paragraph.
|
||||
#[derive(Debug)]
|
||||
pub struct LineChild {
|
||||
pub frame: Frame,
|
||||
pub align: Axes<FixedAlignment>,
|
||||
pub need: Abs,
|
||||
}
|
||||
|
||||
/// A child that encapsulates a prepared block.
|
||||
pub struct BlockChild<'a> {
|
||||
/// A child that encapsulates a prepared unbreakable block.
|
||||
#[derive(Debug)]
|
||||
pub struct SingleChild<'a> {
|
||||
pub align: Axes<FixedAlignment>,
|
||||
pub sticky: bool,
|
||||
pub rootable: bool,
|
||||
pub fr: Option<Fr>,
|
||||
elem: &'a Packed<BlockElem>,
|
||||
styles: StyleChain<'a>,
|
||||
locator: Locator<'a>,
|
||||
cell: CachedCell<SourceResult<Frame>>,
|
||||
}
|
||||
|
||||
impl BlockChild<'_> {
|
||||
impl SingleChild<'_> {
|
||||
/// Build the child's frame given the region's base size.
|
||||
pub fn layout(&self, engine: &mut Engine, base: Size) -> SourceResult<Frame> {
|
||||
self.cell.get_or_init(base, |base| {
|
||||
self.elem
|
||||
.layout_single(engine, self.locator.relayout(), self.styles, base)
|
||||
.map(|frame| frame.post_processed(self.styles))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A child that encapsulates a prepared breakable block.
|
||||
#[derive(Debug)]
|
||||
pub struct MultiChild<'a> {
|
||||
pub align: Axes<FixedAlignment>,
|
||||
alone: bool,
|
||||
elem: &'a Packed<BlockElem>,
|
||||
styles: StyleChain<'a>,
|
||||
locator: Locator<'a>,
|
||||
cell: CachedCell<SourceResult<Fragment>>,
|
||||
}
|
||||
|
||||
impl<'a> MultiChild<'a> {
|
||||
/// Build the child's frames given regions.
|
||||
pub fn layout(
|
||||
pub fn layout<'b>(
|
||||
&'b self,
|
||||
engine: &mut Engine,
|
||||
regions: Regions,
|
||||
) -> SourceResult<(Frame, Option<MultiSpill<'a, 'b>>)> {
|
||||
let fragment = self.layout_impl(engine, regions)?;
|
||||
|
||||
// Extract the first frame.
|
||||
let mut frames = fragment.into_iter();
|
||||
let frame = frames.next().unwrap();
|
||||
|
||||
// If there's more, return a `spill`.
|
||||
let mut spill = None;
|
||||
if frames.next().is_some() {
|
||||
spill = Some(MultiSpill {
|
||||
multi: self,
|
||||
full: regions.full,
|
||||
first: regions.size.y,
|
||||
backlog: vec![],
|
||||
});
|
||||
}
|
||||
|
||||
Ok((frame, spill))
|
||||
}
|
||||
|
||||
/// The shared internal implementation of [`Self::layout`] and
|
||||
/// [`MultiSpill::layout`].
|
||||
fn layout_impl(
|
||||
&self,
|
||||
engine: &mut Engine,
|
||||
regions: Regions,
|
||||
) -> SourceResult<Fragment> {
|
||||
let mut fragment =
|
||||
self.cell.get_or_init(regions, |mut regions| {
|
||||
// Vertical expansion is only kept if this block is the only child.
|
||||
regions.expand.y &= self.alone;
|
||||
self.elem
|
||||
.layout(engine, self.locator.relayout(), self.styles, regions)?;
|
||||
.layout_multiple(engine, self.locator.relayout(), self.styles, regions)
|
||||
.map(|mut fragment| {
|
||||
for frame in &mut fragment {
|
||||
frame.post_process(self.styles);
|
||||
}
|
||||
fragment
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for frame in &mut fragment {
|
||||
frame.post_process(self.styles);
|
||||
/// 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();
|
||||
}
|
||||
|
||||
Ok(fragment)
|
||||
// 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> {
|
||||
let align = self.alignment.unwrap_or_else(|| Alignment::CENTER);
|
||||
let aligned = AlignElem::set_alignment(align).wrap();
|
||||
|
||||
let mut frame = layout_frame(
|
||||
engine,
|
||||
&self.elem.body,
|
||||
self.locator.relayout(),
|
||||
self.styles.chain(&aligned),
|
||||
Region::new(base, Axes::splat(false)),
|
||||
)?;
|
||||
|
||||
frame.post_process(self.styles);
|
||||
Ok(frame)
|
||||
self.cell.get_or_init(base, |base| {
|
||||
let align = self.alignment.unwrap_or_else(|| Alignment::CENTER);
|
||||
let aligned = AlignElem::set_alignment(align).wrap();
|
||||
layout_frame(
|
||||
engine,
|
||||
&self.elem.body,
|
||||
self.locator.relayout(),
|
||||
self.styles.chain(&aligned),
|
||||
Region::new(base, Axes::splat(false)),
|
||||
)
|
||||
.map(|frame| frame.post_processed(self.styles))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Wraps a parameterized computation and caches its latest output.
|
||||
///
|
||||
/// - When the computation is performed multiple times consecutively with the
|
||||
/// same argument, reuses the cache.
|
||||
/// - When the argument changes, the new output is cached.
|
||||
#[derive(Clone)]
|
||||
struct CachedCell<T>(RefCell<Option<(u128, T)>>);
|
||||
|
||||
impl<T> CachedCell<T> {
|
||||
/// Create an empty cached cell.
|
||||
fn new() -> Self {
|
||||
Self(RefCell::new(None))
|
||||
}
|
||||
|
||||
/// Perform the computation `f` with caching.
|
||||
fn get_or_init<F, I>(&self, input: I, f: F) -> T
|
||||
where
|
||||
I: Hash,
|
||||
T: Clone,
|
||||
F: FnOnce(I) -> T,
|
||||
{
|
||||
let input_hash = crate::utils::hash128(&input);
|
||||
|
||||
let mut slot = self.0.borrow_mut();
|
||||
if let Some((hash, output)) = &*slot {
|
||||
if *hash == input_hash {
|
||||
return output.clone();
|
||||
}
|
||||
}
|
||||
|
||||
let output = f(input);
|
||||
*slot = Some((input_hash, output.clone()));
|
||||
output
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Default for CachedCell<T> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Debug for CachedCell<T> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.pad("CachedCell(..)")
|
||||
}
|
||||
}
|
||||
|
843
crates/typst/src/layout/flow/compose.rs
Normal 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, ®ions, 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()));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
512
crates/typst/src/layout/flow/distribute.rs
Normal 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);
|
||||
}
|
||||
}
|
@ -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(
|
||||
|
@ -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());
|
||||
|
@ -1,3 +1,5 @@
|
||||
//! Layout of content into a [`Document`].
|
||||
|
||||
mod collect;
|
||||
mod finalize;
|
||||
mod run;
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
///
|
||||
|
@ -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
|
||||
|
@ -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),
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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()),
|
||||
|
@ -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,
|
||||
|
@ -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>>,
|
||||
|
@ -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.
|
||||
|
@ -390,59 +390,52 @@ Add columns to your document to fit more on a page while maintaining legible
|
||||
line lengths. Columns are vertical blocks of text which are separated by some
|
||||
whitespace. This space is called the gutter.
|
||||
|
||||
If all of your content needs to be laid out in columns, you can just specify the
|
||||
desired number of columns in the [`{page}`]($page.columns) set rule:
|
||||
To lay out your content in columns, just specify the desired number of columns
|
||||
in a [`{page}`]($page.columns) set rule. To adjust the amount of space between
|
||||
the columns, add a set rule on the [`columns` function]($columns), specifying
|
||||
the `gutter` parameter.
|
||||
|
||||
```example
|
||||
>>> #set page(height: 120pt)
|
||||
#set page(columns: 2)
|
||||
#set columns(gutter: 12pt)
|
||||
|
||||
#lorem(30)
|
||||
```
|
||||
|
||||
If you need to adjust the gutter between the columns, refer to the method used
|
||||
in the next section.
|
||||
|
||||
### Use columns anywhere in your document { #columns-anywhere }
|
||||
Very commonly, scientific papers have a single-column title and abstract, while
|
||||
the main body is set in two-columns. To achieve this effect, Typst includes a
|
||||
standalone [`{columns}` function]($columns) that can be used to insert columns
|
||||
anywhere on a page.
|
||||
|
||||
Conceptually, the `columns` function must wrap the content of the columns:
|
||||
the main body is set in two-columns. To achieve this effect, Typst's [`place`
|
||||
function]($place) can temporarily escape the two-column layout by specifying
|
||||
`{float: true}` and `{scope: "page"}`:
|
||||
|
||||
```example:single
|
||||
>>> #set page(height: 180pt)
|
||||
= Impacts of Odobenidae
|
||||
|
||||
#set page(columns: 2)
|
||||
#set par(justify: true)
|
||||
>>> #h(11pt)
|
||||
#columns(2)[
|
||||
== About seals in the wild
|
||||
#lorem(80)
|
||||
]
|
||||
```
|
||||
|
||||
However, we can use the ["everything show rule"]($styling/#show-rules) to reduce
|
||||
nesting and write more legible Typst markup:
|
||||
|
||||
```example:single
|
||||
>>> #set page(height: 180pt)
|
||||
= Impacts of Odobenidae
|
||||
|
||||
#set par(justify: true)
|
||||
>>> #h(11pt)
|
||||
#show: columns.with(2)
|
||||
#place(
|
||||
top + center,
|
||||
float: true,
|
||||
scope: "page",
|
||||
text(1.4em, weight: "bold")[
|
||||
Impacts of Odobenidae
|
||||
],
|
||||
)
|
||||
|
||||
== About seals in the wild
|
||||
#lorem(80)
|
||||
```
|
||||
|
||||
The show rule will wrap everything that comes after it in its function. The
|
||||
[`with` method]($function.with) allows us to pass arguments, in this case, the
|
||||
column count, to a function without calling it.
|
||||
_Floating placement_ refers to elements being pushed to the top or bottom of the
|
||||
column or page, with the remaining content flowing in between. It is also
|
||||
frequently used for [figures]($figure.placement).
|
||||
|
||||
Another use of the `columns` function is to create columns inside of a container
|
||||
like a rectangle or to customize gutter size:
|
||||
### Use columns anywhere in your document { #columns-anywhere }
|
||||
To create columns within a nested layout, e.g. within a rectangle, you can use
|
||||
the [`columns` function]($columns) directly. However, it should really only be
|
||||
used within nested layouts. At the page-level, the page set rule is preferrable
|
||||
because it has better interactions with things like page-level floats,
|
||||
footnotes, and line numbers.
|
||||
|
||||
```example
|
||||
#rect(
|
||||
|
BIN
tests/ref/block-fr-height-auto-width.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
tests/ref/block-fr-height-first-child.png
Normal file
After Width: | Height: | Size: 149 B |
BIN
tests/ref/block-fr-height-multiple.png
Normal file
After Width: | Height: | Size: 152 B |
BIN
tests/ref/block-fr-height.png
Normal file
After Width: | Height: | Size: 167 B |
BIN
tests/ref/block-sticky-alone.png
Normal file
After Width: | Height: | Size: 166 B |
BIN
tests/ref/block-sticky-colbreak.png
Normal file
After Width: | Height: | Size: 283 B |
BIN
tests/ref/block-sticky-many.png
Normal file
After Width: | Height: | Size: 570 B |
BIN
tests/ref/block-sticky.png
Normal file
After Width: | Height: | Size: 469 B |
Before Width: | Height: | Size: 464 B After Width: | Height: | Size: 464 B |
BIN
tests/ref/colbreak-weak.png
Normal file
After Width: | Height: | Size: 197 B |
BIN
tests/ref/figure-placement.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
tests/ref/footnote-block-at-end.png
Normal file
After Width: | Height: | Size: 620 B |
BIN
tests/ref/footnote-break-across-pages-block.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
tests/ref/footnote-break-across-pages-float.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
tests/ref/footnote-break-across-pages-nested.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
BIN
tests/ref/footnote-float-priority.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
tests/ref/footnote-in-list.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
tests/ref/footnote-in-place.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
BIN
tests/ref/footnote-multiple-in-one-line.png
Normal file
After Width: | Height: | Size: 693 B |
Before Width: | Height: | Size: 743 B |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.5 KiB |
BIN
tests/ref/issue-2213-align-fr.png
Normal file
After Width: | Height: | Size: 291 B |
Before Width: | Height: | Size: 602 B After Width: | Height: | Size: 504 B |
Before Width: | Height: | Size: 704 B After Width: | Height: | Size: 678 B |
BIN
tests/ref/issue-3866-block-migration.png
Normal file
After Width: | Height: | Size: 777 B |
Before Width: | Height: | Size: 571 B After Width: | Height: | Size: 512 B |
Before Width: | Height: | Size: 704 B |
BIN
tests/ref/place-float-block-backlog.png
Normal file
After Width: | Height: | Size: 826 B |
BIN
tests/ref/place-float-clearance-empty.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
tests/ref/place-float-column-align-auto.png
Normal file
After Width: | Height: | Size: 932 B |
Before Width: | Height: | Size: 638 B After Width: | Height: | Size: 638 B |
BIN
tests/ref/place-float-counter.png
Normal file
After Width: | Height: | Size: 674 B |
BIN
tests/ref/place-float-delta.png
Normal file
After Width: | Height: | Size: 317 B |
BIN
tests/ref/place-float-flow-size-alone.png
Normal file
After Width: | Height: | Size: 125 B |
BIN
tests/ref/place-float-flow-size.png
Normal file
After Width: | Height: | Size: 347 B |
BIN
tests/ref/place-float-fr.png
Normal file
After Width: | Height: | Size: 507 B |
Before Width: | Height: | Size: 843 B |
BIN
tests/ref/place-float-rel-sizing.png
Normal file
After Width: | Height: | Size: 335 B |
BIN
tests/ref/place-float-threecolumn-block-backlog.png
Normal file
After Width: | Height: | Size: 707 B |
BIN
tests/ref/place-float-threecolumn.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
tests/ref/place-float-twocolumn-align-auto.png
Normal file
After Width: | Height: | Size: 719 B |
BIN
tests/ref/place-float-twocolumn-fits-not.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
tests/ref/place-float-twocolumn-fits.png
Normal file
After Width: | Height: | Size: 1001 B |
BIN
tests/ref/place-float-twocolumn-queued.png
Normal file
After Width: | Height: | Size: 862 B |
BIN
tests/ref/place-float-twocolumn.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 8.8 KiB After Width: | Height: | Size: 9.0 KiB |
0
tests/skip.txt
Normal 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;
|
||||
|
@ -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;
|
||||
|
||||
|
@ -25,10 +25,10 @@
|
||||
#outline()
|
||||
|
||||
= Introduction
|
||||
#v(1cm)
|
||||
#lines(1)
|
||||
|
||||
= Background
|
||||
#v(2cm)
|
||||
#lines(2)
|
||||
|
||||
= Approach
|
||||
|
||||
|
@ -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]
|
||||
|
@ -122,3 +122,10 @@ Hallo
|
||||
= B
|
||||
Text
|
||||
]
|
||||
|
||||
--- colbreak-weak ---
|
||||
#set page(columns: 2)
|
||||
#colbreak(weak: true)
|
||||
A
|
||||
#colbreak(weak: true)
|
||||
B
|
||||
|
@ -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)]
|
||||
|
@ -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)
|
||||
]
|
||||
|
@ -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]
|
@ -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)
|
@ -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.
|
@ -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)
|
||||
|
@ -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")
|
||||
|
@ -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 => {
|
||||
|