2025-01-24 12:11:26 +00:00

421 lines
13 KiB
Rust

//! Layout of content into a [`Frame`] or [`Fragment`].
mod block;
mod collect;
mod compose;
mod distribute;
pub(crate) use self::block::unbreakable_pod;
use std::collections::HashSet;
use std::num::NonZeroUsize;
use std::rc::Rc;
use bumpalo::Bump;
use comemo::{Track, Tracked, TrackedMut};
use ecow::EcoVec;
use typst_library::diag::{bail, At, SourceDiagnostic, SourceResult};
use typst_library::engine::{Engine, Route, Sink, Traced};
use typst_library::foundations::{Content, Packed, Resolve, StyleChain};
use typst_library::introspection::{
Introspector, Location, Locator, LocatorLink, SplitLocator, Tag,
};
use typst_library::layout::{
Abs, ColumnsElem, Dir, Em, Fragment, Frame, PageElem, PlacementScope, Region,
Regions, Rel, Size,
};
use typst_library::model::{FootnoteElem, FootnoteEntry, LineNumberingScope, ParLine};
use typst_library::routines::{Arenas, FragmentKind, Pair, RealizationKind, Routines};
use typst_library::text::TextElem;
use typst_library::World;
use typst_utils::{NonZeroExt, Numeric};
use self::block::{layout_multi_block, layout_single_block};
use self::collect::{
collect, Child, LineChild, MultiChild, MultiSpill, PlacedChild, SingleChild,
};
use self::compose::{compose, Composer};
use self::distribute::distribute;
/// Lays out content into a single region, producing a single frame.
pub fn layout_frame(
engine: &mut Engine,
content: &Content,
locator: Locator,
styles: StyleChain,
region: Region,
) -> SourceResult<Frame> {
layout_fragment(engine, content, locator, styles, region.into())
.map(Fragment::into_frame)
}
/// Lays out content into multiple regions.
///
/// When laying out into just one region, prefer [`layout_frame`].
pub fn layout_fragment(
engine: &mut Engine,
content: &Content,
locator: Locator,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
layout_fragment_impl(
engine.routines,
engine.world,
engine.introspector,
engine.traced,
TrackedMut::reborrow_mut(&mut engine.sink),
engine.route.track(),
content,
locator.track(),
styles,
regions,
NonZeroUsize::ONE,
Rel::zero(),
)
}
/// Layout the columns.
///
/// This is different from just laying out into column-sized regions as the
/// columns can interact due to parent-scoped placed elements.
#[typst_macros::time(span = elem.span())]
pub fn layout_columns(
elem: &Packed<ColumnsElem>,
engine: &mut Engine,
locator: Locator,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
layout_fragment_impl(
engine.routines,
engine.world,
engine.introspector,
engine.traced,
TrackedMut::reborrow_mut(&mut engine.sink),
engine.route.track(),
&elem.body,
locator.track(),
styles,
regions,
elem.count(styles),
elem.gutter(styles),
)
}
/// The cached, internal implementation of [`layout_fragment`].
#[comemo::memoize]
#[allow(clippy::too_many_arguments)]
fn layout_fragment_impl(
routines: &Routines,
world: Tracked<dyn World + '_>,
introspector: Tracked<Introspector>,
traced: Tracked<Traced>,
sink: TrackedMut<Sink>,
route: Tracked<Route>,
content: &Content,
locator: Tracked<Locator>,
styles: StyleChain,
regions: Regions,
columns: NonZeroUsize,
column_gutter: Rel<Abs>,
) -> SourceResult<Fragment> {
if !regions.size.x.is_finite() && regions.expand.x {
bail!(content.span(), "cannot expand into infinite width");
}
if !regions.size.y.is_finite() && regions.expand.y {
bail!(content.span(), "cannot expand into infinite height");
}
let link = LocatorLink::new(locator);
let mut locator = Locator::link(&link).split();
let mut engine = Engine {
routines,
world,
introspector,
traced,
sink,
route: Route::extend(route),
};
engine.route.check_layout_depth().at(content.span())?;
let mut kind = FragmentKind::Block;
let arenas = Arenas::default();
let children = (engine.routines.realize)(
RealizationKind::LayoutFragment(&mut kind),
&mut engine,
&mut locator,
&arenas,
content,
styles,
)?;
layout_flow(
&mut engine,
&children,
&mut locator,
styles,
regions,
columns,
column_gutter,
kind.into(),
)
}
/// The mode a flow can be laid out in.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum FlowMode {
/// A root flow with block-level elements. Like `FlowMode::Block`, but can
/// additionally host footnotes and line numbers.
Root,
/// A flow whose children are block-level elements.
Block,
/// A flow whose children are inline-level elements.
Inline,
}
impl From<FragmentKind> for FlowMode {
fn from(value: FragmentKind) -> Self {
match value {
FragmentKind::Inline => Self::Inline,
FragmentKind::Block => Self::Block,
}
}
}
/// Lays out realized content into regions, potentially with columns.
#[allow(clippy::too_many_arguments)]
pub fn layout_flow<'a>(
engine: &mut Engine,
children: &[Pair<'a>],
locator: &mut SplitLocator<'a>,
shared: StyleChain<'a>,
mut regions: Regions,
columns: NonZeroUsize,
column_gutter: Rel<Abs>,
mode: FlowMode,
) -> SourceResult<Fragment> {
// Prepare configuration that is shared across the whole flow.
let config = Config {
mode,
shared,
columns: {
let mut count = columns.get();
if !regions.size.x.is_finite() {
count = 1;
}
let gutter = column_gutter.relative_to(regions.base().x);
let width = (regions.size.x - gutter * (count - 1) as f64) / count as f64;
let dir = TextElem::dir_in(shared);
ColumnConfig { count, width, gutter, dir }
},
footnote: FootnoteConfig {
separator: FootnoteEntry::separator_in(shared),
clearance: FootnoteEntry::clearance_in(shared),
gap: FootnoteEntry::gap_in(shared),
expand: regions.expand.x,
},
line_numbers: (mode == FlowMode::Root).then(|| LineNumberConfig {
scope: ParLine::numbering_scope_in(shared),
default_clearance: {
let width = if PageElem::flipped_in(shared) {
PageElem::height_in(shared)
} else {
PageElem::width_in(shared)
};
// Clamp below is safe (min <= max): if the font size is
// negative, we set min = max = 0; otherwise,
// `0.75 * size <= 2.5 * size` for zero and positive sizes.
(0.026 * width.unwrap_or_default()).clamp(
Em::new(0.75).resolve(shared).max(Abs::zero()),
Em::new(2.5).resolve(shared).max(Abs::zero()),
)
},
}),
};
// Collect the elements into pre-processed children. These are much easier
// to handle than the raw elements.
let bump = Bump::new();
let children = collect(
engine,
&bump,
children,
locator.next(&()),
Size::new(config.columns.width, regions.full),
regions.expand.x,
mode,
)?;
let mut work = Work::new(&children);
let mut finished = vec![];
// This loop runs once per region produced by the flow layout.
loop {
let frame = compose(engine, &mut work, &config, locator.next(&()), regions)?;
finished.push(frame);
// Terminate the loop when everything is processed, though draining the
// backlog if necessary.
if work.done() && (!regions.expand.y || regions.backlog.is_empty()) {
break;
}
regions.next();
}
Ok(Fragment::frames(finished))
}
/// The work that is left to do by flow layout.
///
/// The lifetimes 'a and 'b are used across flow layout:
/// - 'a is that of the content coming out of realization
/// - 'b is that of the collected/prepared children
#[derive(Clone)]
struct Work<'a, 'b> {
/// Children that we haven't processed yet. This slice shrinks over time.
children: &'b [Child<'a>],
/// Leftovers from a breakable block.
spill: Option<MultiSpill<'a, 'b>>,
/// Queued floats that didn't fit in previous regions.
floats: EcoVec<&'b PlacedChild<'a>>,
/// Queued footnotes that didn't fit in previous regions.
footnotes: EcoVec<Packed<FootnoteElem>>,
/// Spilled frames of a footnote that didn't fully fit. Similar to `spill`.
footnote_spill: Option<std::vec::IntoIter<Frame>>,
/// Queued tags that will be attached to the next frame.
tags: EcoVec<&'a Tag>,
/// Identifies floats and footnotes that can be skipped if visited because
/// they were already handled and incorporated as column or page level
/// insertions.
skips: Rc<HashSet<Location>>,
}
impl<'a, 'b> Work<'a, 'b> {
/// Create the initial work state from a list of children.
fn new(children: &'b [Child<'a>]) -> Self {
Self {
children,
spill: None,
floats: EcoVec::new(),
footnotes: EcoVec::new(),
footnote_spill: None,
tags: EcoVec::new(),
skips: Rc::new(HashSet::new()),
}
}
/// Get the first unprocessed child, from the start of the slice.
fn head(&self) -> Option<&'b Child<'a>> {
self.children.first()
}
/// Mark the `head()` child as processed, advancing the slice by one.
fn advance(&mut self) {
self.children = &self.children[1..];
}
/// Whether all work is done. This means we can terminate flow layout.
fn done(&self) -> bool {
self.children.is_empty()
&& self.spill.is_none()
&& self.floats.is_empty()
&& self.footnote_spill.is_none()
&& self.footnotes.is_empty()
}
/// Add skipped floats and footnotes from the insertion areas to the skip
/// set.
fn extend_skips(&mut self, skips: &[Location]) {
if !skips.is_empty() {
Rc::make_mut(&mut self.skips).extend(skips.iter().copied());
}
}
}
/// Shared configuration for the whole flow.
struct Config<'x> {
/// Whether this is the root flow, which can host footnotes and line
/// numbers.
mode: FlowMode,
/// The styles shared by the whole flow. This is used for footnotes and line
/// numbers.
shared: StyleChain<'x>,
/// Settings for columns.
columns: ColumnConfig,
/// Settings for footnotes.
footnote: FootnoteConfig,
/// Settings for line numbers.
line_numbers: Option<LineNumberConfig>,
}
/// Configuration of footnotes.
struct FootnoteConfig {
/// The separator between flow content and footnotes. Typically a line.
separator: Content,
/// The amount of space left above the separator.
clearance: Abs,
/// The gap between footnote entries.
gap: Abs,
/// Whether horizontal expansion is enabled for footnotes.
expand: bool,
}
/// Configuration of columns.
struct ColumnConfig {
/// The number of columns.
count: usize,
/// The width of each column.
width: Abs,
/// The amount of space between columns.
gutter: Abs,
/// The horizontal direction in which columns progress. Defined by
/// `text.dir`.
dir: Dir,
}
/// Configuration of line numbers.
struct LineNumberConfig {
/// Where line numbers are reset.
scope: LineNumberingScope,
/// The default clearance for `auto`.
///
/// This value should be relative to the page's width, such that the
/// clearance between line numbers and text is small when the page is,
/// itself, small. However, that could cause the clearance to be too small
/// or too large when considering the current text size; in particular, a
/// larger text size would require more clearance to be able to tell line
/// numbers apart from text, whereas a smaller text size requires less
/// clearance so they aren't way too far apart. Therefore, the default
/// value is a percentage of the page width clamped between `0.75em` and
/// `2.5em`.
default_clearance: Abs,
}
/// The result type for flow layout.
///
/// The `Err(_)` variant incorporate control flow events for finishing and
/// relayouting regions.
type FlowResult<T> = Result<T, Stop>;
/// A control flow event during flow layout.
enum Stop {
/// Indicates that the current subregion should be finished. Can be caused
/// by a lack of space (`false`) or an explicit column break (`true`).
Finish(bool),
/// Indicates that the given scope should be relayouted.
Relayout(PlacementScope),
/// A fatal error.
Error(EcoVec<SourceDiagnostic>),
}
impl From<EcoVec<SourceDiagnostic>> for Stop {
fn from(error: EcoVec<SourceDiagnostic>) -> Self {
Stop::Error(error)
}
}