Skip to main content

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::diag::{At, SourceDiagnostic, SourceResult, bail};
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::pdf::ArtifactKind;
29use typst_library::routines::{Arenas, FragmentKind, Pair, RealizationKind};
30use typst_library::text::TextElem;
31use typst_library::{Library, World};
32use typst_utils::{LazyHash, NonZeroExt, Numeric, Protected};
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.world,
65        engine.library,
66        engine.introspector.into_raw(),
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.world,
93        engine.library,
94        engine.introspector.into_raw(),
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    world: Tracked<dyn World + '_>,
112    library: &LazyHash<Library>,
113    introspector: Tracked<dyn 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 introspector = Protected::from_raw(introspector);
132    let link = LocatorLink::new(locator);
133    let mut locator = Locator::link(&link).split();
134    let mut engine = Engine {
135        library,
136        world,
137        introspector,
138        traced,
139        sink,
140        route: Route::extend(route),
141    };
142
143    engine.route.check_layout_depth().at(content.span())?;
144
145    let mut kind = FragmentKind::Block;
146    let arenas = Arenas::default();
147    let children = (engine.library.routines.realize)(
148        RealizationKind::Fragment { kind: &mut kind },
149        &mut engine,
150        &mut locator,
151        &arenas,
152        content,
153        styles,
154    )?;
155
156    layout_flow(
157        &mut engine,
158        &children,
159        &mut locator,
160        styles,
161        regions,
162        columns,
163        column_gutter,
164        kind.into(),
165    )
166}
167
168/// The mode a flow can be laid out in.
169#[derive(Debug, Copy, Clone, Eq, PartialEq)]
170pub enum FlowMode {
171    /// A root flow with block-level elements. Like `FlowMode::Block`, but can
172    /// additionally host footnotes and line numbers.
173    Root,
174    /// A flow whose children are block-level elements.
175    Block,
176    /// A flow whose children are inline-level elements.
177    Inline,
178}
179
180impl From<FragmentKind> for FlowMode {
181    fn from(value: FragmentKind) -> Self {
182        match value {
183            FragmentKind::Inline => Self::Inline,
184            FragmentKind::Block => Self::Block,
185        }
186    }
187}
188
189/// Lays out realized content into regions, potentially with columns.
190#[allow(clippy::too_many_arguments)]
191pub fn layout_flow<'a>(
192    engine: &mut Engine,
193    children: &[Pair<'a>],
194    locator: &mut SplitLocator<'a>,
195    shared: StyleChain<'a>,
196    mut regions: Regions,
197    columns: NonZeroUsize,
198    column_gutter: Rel<Abs>,
199    mode: FlowMode,
200) -> SourceResult<Fragment> {
201    // Prepare configuration that is shared across the whole flow.
202    let config = configuration(shared, regions, columns, column_gutter, mode);
203
204    // Collect the elements into pre-processed children. These are much easier
205    // to handle than the raw elements.
206    let bump = Bump::new();
207    let children = collect(
208        engine,
209        &bump,
210        children,
211        locator.next(&()),
212        Size::new(config.columns.width, regions.full),
213        regions.expand.x,
214        mode,
215    )?;
216
217    let mut work = Work::new(&children);
218    let mut finished = vec![];
219
220    // This loop runs once per region produced by the flow layout.
221    loop {
222        let frame = compose(engine, &mut work, &config, locator.next(&()), regions)?;
223        finished.push(frame);
224
225        // Terminate the loop when everything is processed, though draining the
226        // backlog if necessary.
227        if work.done() && (!regions.expand.y || regions.backlog.is_empty()) {
228            break;
229        }
230
231        regions.next();
232    }
233
234    Ok(Fragment::frames(finished))
235}
236
237/// Determine the flow's configuration.
238fn configuration<'x>(
239    shared: StyleChain<'x>,
240    regions: Regions,
241    columns: NonZeroUsize,
242    column_gutter: Rel<Abs>,
243    mode: FlowMode,
244) -> Config<'x> {
245    Config {
246        mode,
247        shared,
248        columns: {
249            let mut count = columns.get();
250            if !regions.size.x.is_finite() {
251                count = 1;
252            }
253
254            let gutter = column_gutter.relative_to(regions.base().x);
255            let width = (regions.size.x - gutter * (count - 1) as f64) / count as f64;
256            let dir = shared.resolve(TextElem::dir);
257            ColumnConfig { count, width, gutter, dir }
258        },
259        footnote: FootnoteConfig {
260            separator: shared
261                .get_cloned(FootnoteEntry::separator)
262                .artifact(ArtifactKind::Other),
263            clearance: shared.resolve(FootnoteEntry::clearance),
264            gap: shared.resolve(FootnoteEntry::gap),
265            expand: regions.expand.x,
266        },
267        line_numbers: (mode == FlowMode::Root).then(|| LineNumberConfig {
268            scope: shared.get(ParLine::numbering_scope),
269            default_clearance: {
270                let width = if shared.get(PageElem::flipped) {
271                    shared.resolve(PageElem::height)
272                } else {
273                    shared.resolve(PageElem::width)
274                };
275
276                // Clamp below is safe (min <= max): if the font size is
277                // negative, we set min = max = 0; otherwise,
278                // `0.75 * size <= 2.5 * size` for zero and positive sizes.
279                (0.026 * width.unwrap_or_default()).clamp(
280                    Em::new(0.75).resolve(shared).max(Abs::zero()),
281                    Em::new(2.5).resolve(shared).max(Abs::zero()),
282                )
283            },
284        }),
285    }
286}
287
288/// The work that is left to do by flow layout.
289///
290/// The lifetimes 'a and 'b are used across flow layout:
291/// - 'a is that of the content coming out of realization
292/// - 'b is that of the collected/prepared children
293#[derive(Clone)]
294struct Work<'a, 'b> {
295    /// Children that we haven't processed yet. This slice shrinks over time.
296    children: &'b [Child<'a>],
297    /// Leftovers from a breakable block.
298    spill: Option<MultiSpill<'a, 'b>>,
299    /// Queued floats that didn't fit in previous regions.
300    floats: EcoVec<&'b PlacedChild<'a>>,
301    /// Queued footnotes that didn't fit in previous regions.
302    footnotes: EcoVec<Packed<FootnoteElem>>,
303    /// Spilled frames of a footnote that didn't fully fit. Similar to `spill`.
304    footnote_spill: Option<std::vec::IntoIter<Frame>>,
305    /// Queued tags that will be attached to the next frame.
306    tags: EcoVec<&'a Tag>,
307    /// Identifies floats and footnotes that can be skipped if visited because
308    /// they were already handled and incorporated as column or page level
309    /// insertions.
310    skips: Rc<FxHashSet<Location>>,
311}
312
313impl<'a, 'b> Work<'a, 'b> {
314    /// Create the initial work state from a list of children.
315    fn new(children: &'b [Child<'a>]) -> Self {
316        Self {
317            children,
318            spill: None,
319            floats: EcoVec::new(),
320            footnotes: EcoVec::new(),
321            footnote_spill: None,
322            tags: EcoVec::new(),
323            skips: Rc::new(FxHashSet::default()),
324        }
325    }
326
327    /// Get the first unprocessed child, from the start of the slice.
328    fn head(&self) -> Option<&'b Child<'a>> {
329        self.children.first()
330    }
331
332    /// Mark the `head()` child as processed, advancing the slice by one.
333    fn advance(&mut self) {
334        self.children = &self.children[1..];
335    }
336
337    /// Whether all work is done. This means we can terminate flow layout.
338    fn done(&self) -> bool {
339        self.children.is_empty()
340            && self.spill.is_none()
341            && self.floats.is_empty()
342            && self.footnote_spill.is_none()
343            && self.footnotes.is_empty()
344    }
345
346    /// Add skipped floats and footnotes from the insertion areas to the skip
347    /// set.
348    fn extend_skips(&mut self, skips: &[Location]) {
349        if !skips.is_empty() {
350            Rc::make_mut(&mut self.skips).extend(skips.iter().copied());
351        }
352    }
353}
354
355/// Shared configuration for the whole flow.
356struct Config<'x> {
357    /// Whether this is the root flow, which can host footnotes and line
358    /// numbers.
359    mode: FlowMode,
360    /// The styles shared by the whole flow. This is used for footnotes and line
361    /// numbers.
362    shared: StyleChain<'x>,
363    /// Settings for columns.
364    columns: ColumnConfig,
365    /// Settings for footnotes.
366    footnote: FootnoteConfig,
367    /// Settings for line numbers.
368    line_numbers: Option<LineNumberConfig>,
369}
370
371/// Configuration of footnotes.
372struct FootnoteConfig {
373    /// The separator between flow content and footnotes. Typically a line.
374    separator: Content,
375    /// The amount of space left above the separator.
376    clearance: Abs,
377    /// The gap between footnote entries.
378    gap: Abs,
379    /// Whether horizontal expansion is enabled for footnotes.
380    expand: bool,
381}
382
383/// Configuration of columns.
384struct ColumnConfig {
385    /// The number of columns.
386    count: usize,
387    /// The width of each column.
388    width: Abs,
389    /// The amount of space between columns.
390    gutter: Abs,
391    /// The horizontal direction in which columns progress. Defined by
392    /// `text.dir`.
393    dir: Dir,
394}
395
396/// Configuration of line numbers.
397struct LineNumberConfig {
398    /// Where line numbers are reset.
399    scope: LineNumberingScope,
400    /// The default clearance for `auto`.
401    ///
402    /// This value should be relative to the page's width, such that the
403    /// clearance between line numbers and text is small when the page is,
404    /// itself, small. However, that could cause the clearance to be too small
405    /// or too large when considering the current text size; in particular, a
406    /// larger text size would require more clearance to be able to tell line
407    /// numbers apart from text, whereas a smaller text size requires less
408    /// clearance so they aren't way too far apart. Therefore, the default
409    /// value is a percentage of the page width clamped between `0.75em` and
410    /// `2.5em`.
411    default_clearance: Abs,
412}
413
414/// The result type for flow layout.
415///
416/// The `Err(_)` variant incorporate control flow events for finishing and
417/// relayouting regions.
418type FlowResult<T> = Result<T, Stop>;
419
420/// A control flow event during flow layout.
421enum Stop {
422    /// Indicates that the current subregion should be finished. Can be caused
423    /// by a lack of space (`false`) or an explicit column break (`true`).
424    Finish(bool),
425    /// Indicates that the given scope should be relayouted.
426    Relayout(PlacementScope),
427    /// A fatal error.
428    Error(EcoVec<SourceDiagnostic>),
429}
430
431impl From<EcoVec<SourceDiagnostic>> for Stop {
432    fn from(error: EcoVec<SourceDiagnostic>) -> Self {
433        Stop::Error(error)
434    }
435}