typst_layout/flow/
mod.rs

1//! Layout of content into a [`Frame`] or [`Fragment`].
2
3mod block;
4mod collect;
5mod compose;
6mod distribute;
7
8pub(crate) use self::block::unbreakable_pod;
9
10use std::num::NonZeroUsize;
11use std::rc::Rc;
12
13use bumpalo::Bump;
14use comemo::{Track, Tracked, TrackedMut};
15use ecow::EcoVec;
16use rustc_hash::FxHashSet;
17use typst_library::World;
18use typst_library::diag::{At, SourceDiagnostic, SourceResult, bail};
19use typst_library::engine::{Engine, Route, Sink, Traced};
20use typst_library::foundations::{Content, Packed, Resolve, StyleChain};
21use typst_library::introspection::{
22    Introspector, Location, Locator, LocatorLink, SplitLocator, Tag,
23};
24use typst_library::layout::{
25    Abs, ColumnsElem, Dir, Em, Fragment, Frame, PageElem, PlacementScope, Region,
26    Regions, Rel, Size,
27};
28use typst_library::model::{FootnoteElem, FootnoteEntry, LineNumberingScope, ParLine};
29use typst_library::pdf::ArtifactKind;
30use typst_library::routines::{Arenas, FragmentKind, Pair, RealizationKind, Routines};
31use typst_library::text::TextElem;
32use typst_utils::{NonZeroExt, Numeric};
33
34use self::block::{layout_multi_block, layout_single_block};
35use self::collect::{
36    Child, LineChild, MultiChild, MultiSpill, PlacedChild, SingleChild, collect,
37};
38use self::compose::{Composer, compose};
39use self::distribute::distribute;
40
41/// Lays out content into a single region, producing a single frame.
42pub fn layout_frame(
43    engine: &mut Engine,
44    content: &Content,
45    locator: Locator,
46    styles: StyleChain,
47    region: Region,
48) -> SourceResult<Frame> {
49    layout_fragment(engine, content, locator, styles, region.into())
50        .map(Fragment::into_frame)
51}
52
53/// Lays out content into multiple regions.
54///
55/// When laying out into just one region, prefer [`layout_frame`].
56pub fn layout_fragment(
57    engine: &mut Engine,
58    content: &Content,
59    locator: Locator,
60    styles: StyleChain,
61    regions: Regions,
62) -> SourceResult<Fragment> {
63    layout_fragment_impl(
64        engine.routines,
65        engine.world,
66        engine.introspector,
67        engine.traced,
68        TrackedMut::reborrow_mut(&mut engine.sink),
69        engine.route.track(),
70        content,
71        locator.track(),
72        styles,
73        regions,
74        NonZeroUsize::ONE,
75        Rel::zero(),
76    )
77}
78
79/// Layout the columns.
80///
81/// This is different from just laying out into column-sized regions as the
82/// columns can interact due to parent-scoped placed elements.
83#[typst_macros::time(span = elem.span())]
84pub fn layout_columns(
85    elem: &Packed<ColumnsElem>,
86    engine: &mut Engine,
87    locator: Locator,
88    styles: StyleChain,
89    regions: Regions,
90) -> SourceResult<Fragment> {
91    layout_fragment_impl(
92        engine.routines,
93        engine.world,
94        engine.introspector,
95        engine.traced,
96        TrackedMut::reborrow_mut(&mut engine.sink),
97        engine.route.track(),
98        &elem.body,
99        locator.track(),
100        styles,
101        regions,
102        elem.count.get(styles),
103        elem.gutter.resolve(styles),
104    )
105}
106
107/// The cached, internal implementation of [`layout_fragment`].
108#[comemo::memoize]
109#[allow(clippy::too_many_arguments)]
110fn layout_fragment_impl(
111    routines: &Routines,
112    world: Tracked<dyn World + '_>,
113    introspector: Tracked<Introspector>,
114    traced: Tracked<Traced>,
115    sink: TrackedMut<Sink>,
116    route: Tracked<Route>,
117    content: &Content,
118    locator: Tracked<Locator>,
119    styles: StyleChain,
120    regions: Regions,
121    columns: NonZeroUsize,
122    column_gutter: Rel<Abs>,
123) -> SourceResult<Fragment> {
124    if !regions.size.x.is_finite() && regions.expand.x {
125        bail!(content.span(), "cannot expand into infinite width");
126    }
127    if !regions.size.y.is_finite() && regions.expand.y {
128        bail!(content.span(), "cannot expand into infinite height");
129    }
130
131    let link = LocatorLink::new(locator);
132    let mut locator = Locator::link(&link).split();
133    let mut engine = Engine {
134        routines,
135        world,
136        introspector,
137        traced,
138        sink,
139        route: Route::extend(route),
140    };
141
142    engine.route.check_layout_depth().at(content.span())?;
143
144    let mut kind = FragmentKind::Block;
145    let arenas = Arenas::default();
146    let children = (engine.routines.realize)(
147        RealizationKind::LayoutFragment { kind: &mut kind },
148        &mut engine,
149        &mut locator,
150        &arenas,
151        content,
152        styles,
153    )?;
154
155    layout_flow(
156        &mut engine,
157        &children,
158        &mut locator,
159        styles,
160        regions,
161        columns,
162        column_gutter,
163        kind.into(),
164    )
165}
166
167/// The mode a flow can be laid out in.
168#[derive(Debug, Copy, Clone, Eq, PartialEq)]
169pub enum FlowMode {
170    /// A root flow with block-level elements. Like `FlowMode::Block`, but can
171    /// additionally host footnotes and line numbers.
172    Root,
173    /// A flow whose children are block-level elements.
174    Block,
175    /// A flow whose children are inline-level elements.
176    Inline,
177}
178
179impl From<FragmentKind> for FlowMode {
180    fn from(value: FragmentKind) -> Self {
181        match value {
182            FragmentKind::Inline => Self::Inline,
183            FragmentKind::Block => Self::Block,
184        }
185    }
186}
187
188/// Lays out realized content into regions, potentially with columns.
189#[allow(clippy::too_many_arguments)]
190pub fn layout_flow<'a>(
191    engine: &mut Engine,
192    children: &[Pair<'a>],
193    locator: &mut SplitLocator<'a>,
194    shared: StyleChain<'a>,
195    mut regions: Regions,
196    columns: NonZeroUsize,
197    column_gutter: Rel<Abs>,
198    mode: FlowMode,
199) -> SourceResult<Fragment> {
200    // Prepare configuration that is shared across the whole flow.
201    let config = configuration(shared, regions, columns, column_gutter, mode);
202
203    // Collect the elements into pre-processed children. These are much easier
204    // to handle than the raw elements.
205    let bump = Bump::new();
206    let children = collect(
207        engine,
208        &bump,
209        children,
210        locator.next(&()),
211        Size::new(config.columns.width, regions.full),
212        regions.expand.x,
213        mode,
214    )?;
215
216    let mut work = Work::new(&children);
217    let mut finished = vec![];
218
219    // This loop runs once per region produced by the flow layout.
220    loop {
221        let frame = compose(engine, &mut work, &config, locator.next(&()), regions)?;
222        finished.push(frame);
223
224        // Terminate the loop when everything is processed, though draining the
225        // backlog if necessary.
226        if work.done() && (!regions.expand.y || regions.backlog.is_empty()) {
227            break;
228        }
229
230        regions.next();
231    }
232
233    Ok(Fragment::frames(finished))
234}
235
236/// Determine the flow's configuration.
237fn configuration<'x>(
238    shared: StyleChain<'x>,
239    regions: Regions,
240    columns: NonZeroUsize,
241    column_gutter: Rel<Abs>,
242    mode: FlowMode,
243) -> Config<'x> {
244    Config {
245        mode,
246        shared,
247        columns: {
248            let mut count = columns.get();
249            if !regions.size.x.is_finite() {
250                count = 1;
251            }
252
253            let gutter = column_gutter.relative_to(regions.base().x);
254            let width = (regions.size.x - gutter * (count - 1) as f64) / count as f64;
255            let dir = shared.resolve(TextElem::dir);
256            ColumnConfig { count, width, gutter, dir }
257        },
258        footnote: FootnoteConfig {
259            separator: shared
260                .get_cloned(FootnoteEntry::separator)
261                .artifact(ArtifactKind::Other),
262            clearance: shared.resolve(FootnoteEntry::clearance),
263            gap: shared.resolve(FootnoteEntry::gap),
264            expand: regions.expand.x,
265        },
266        line_numbers: (mode == FlowMode::Root).then(|| LineNumberConfig {
267            scope: shared.get(ParLine::numbering_scope),
268            default_clearance: {
269                let width = if shared.get(PageElem::flipped) {
270                    shared.resolve(PageElem::height)
271                } else {
272                    shared.resolve(PageElem::width)
273                };
274
275                // Clamp below is safe (min <= max): if the font size is
276                // negative, we set min = max = 0; otherwise,
277                // `0.75 * size <= 2.5 * size` for zero and positive sizes.
278                (0.026 * width.unwrap_or_default()).clamp(
279                    Em::new(0.75).resolve(shared).max(Abs::zero()),
280                    Em::new(2.5).resolve(shared).max(Abs::zero()),
281                )
282            },
283        }),
284    }
285}
286
287/// The work that is left to do by flow layout.
288///
289/// The lifetimes 'a and 'b are used across flow layout:
290/// - 'a is that of the content coming out of realization
291/// - 'b is that of the collected/prepared children
292#[derive(Clone)]
293struct Work<'a, 'b> {
294    /// Children that we haven't processed yet. This slice shrinks over time.
295    children: &'b [Child<'a>],
296    /// Leftovers from a breakable block.
297    spill: Option<MultiSpill<'a, 'b>>,
298    /// Queued floats that didn't fit in previous regions.
299    floats: EcoVec<&'b PlacedChild<'a>>,
300    /// Queued footnotes that didn't fit in previous regions.
301    footnotes: EcoVec<Packed<FootnoteElem>>,
302    /// Spilled frames of a footnote that didn't fully fit. Similar to `spill`.
303    footnote_spill: Option<std::vec::IntoIter<Frame>>,
304    /// Queued tags that will be attached to the next frame.
305    tags: EcoVec<&'a Tag>,
306    /// Identifies floats and footnotes that can be skipped if visited because
307    /// they were already handled and incorporated as column or page level
308    /// insertions.
309    skips: Rc<FxHashSet<Location>>,
310}
311
312impl<'a, 'b> Work<'a, 'b> {
313    /// Create the initial work state from a list of children.
314    fn new(children: &'b [Child<'a>]) -> Self {
315        Self {
316            children,
317            spill: None,
318            floats: EcoVec::new(),
319            footnotes: EcoVec::new(),
320            footnote_spill: None,
321            tags: EcoVec::new(),
322            skips: Rc::new(FxHashSet::default()),
323        }
324    }
325
326    /// Get the first unprocessed child, from the start of the slice.
327    fn head(&self) -> Option<&'b Child<'a>> {
328        self.children.first()
329    }
330
331    /// Mark the `head()` child as processed, advancing the slice by one.
332    fn advance(&mut self) {
333        self.children = &self.children[1..];
334    }
335
336    /// Whether all work is done. This means we can terminate flow layout.
337    fn done(&self) -> bool {
338        self.children.is_empty()
339            && self.spill.is_none()
340            && self.floats.is_empty()
341            && self.footnote_spill.is_none()
342            && self.footnotes.is_empty()
343    }
344
345    /// Add skipped floats and footnotes from the insertion areas to the skip
346    /// set.
347    fn extend_skips(&mut self, skips: &[Location]) {
348        if !skips.is_empty() {
349            Rc::make_mut(&mut self.skips).extend(skips.iter().copied());
350        }
351    }
352}
353
354/// Shared configuration for the whole flow.
355struct Config<'x> {
356    /// Whether this is the root flow, which can host footnotes and line
357    /// numbers.
358    mode: FlowMode,
359    /// The styles shared by the whole flow. This is used for footnotes and line
360    /// numbers.
361    shared: StyleChain<'x>,
362    /// Settings for columns.
363    columns: ColumnConfig,
364    /// Settings for footnotes.
365    footnote: FootnoteConfig,
366    /// Settings for line numbers.
367    line_numbers: Option<LineNumberConfig>,
368}
369
370/// Configuration of footnotes.
371struct FootnoteConfig {
372    /// The separator between flow content and footnotes. Typically a line.
373    separator: Content,
374    /// The amount of space left above the separator.
375    clearance: Abs,
376    /// The gap between footnote entries.
377    gap: Abs,
378    /// Whether horizontal expansion is enabled for footnotes.
379    expand: bool,
380}
381
382/// Configuration of columns.
383struct ColumnConfig {
384    /// The number of columns.
385    count: usize,
386    /// The width of each column.
387    width: Abs,
388    /// The amount of space between columns.
389    gutter: Abs,
390    /// The horizontal direction in which columns progress. Defined by
391    /// `text.dir`.
392    dir: Dir,
393}
394
395/// Configuration of line numbers.
396struct LineNumberConfig {
397    /// Where line numbers are reset.
398    scope: LineNumberingScope,
399    /// The default clearance for `auto`.
400    ///
401    /// This value should be relative to the page's width, such that the
402    /// clearance between line numbers and text is small when the page is,
403    /// itself, small. However, that could cause the clearance to be too small
404    /// or too large when considering the current text size; in particular, a
405    /// larger text size would require more clearance to be able to tell line
406    /// numbers apart from text, whereas a smaller text size requires less
407    /// clearance so they aren't way too far apart. Therefore, the default
408    /// value is a percentage of the page width clamped between `0.75em` and
409    /// `2.5em`.
410    default_clearance: Abs,
411}
412
413/// The result type for flow layout.
414///
415/// The `Err(_)` variant incorporate control flow events for finishing and
416/// relayouting regions.
417type FlowResult<T> = Result<T, Stop>;
418
419/// A control flow event during flow layout.
420enum Stop {
421    /// Indicates that the current subregion should be finished. Can be caused
422    /// by a lack of space (`false`) or an explicit column break (`true`).
423    Finish(bool),
424    /// Indicates that the given scope should be relayouted.
425    Relayout(PlacementScope),
426    /// A fatal error.
427    Error(EcoVec<SourceDiagnostic>),
428}
429
430impl From<EcoVec<SourceDiagnostic>> for Stop {
431    fn from(error: EcoVec<SourceDiagnostic>) -> Self {
432        Stop::Error(error)
433    }
434}