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