typst_layout/
lists.rs

1use comemo::Track;
2use smallvec::smallvec;
3use typst_library::diag::SourceResult;
4use typst_library::engine::Engine;
5use typst_library::foundations::{Content, Context, Depth, Packed, StyleChain};
6use typst_library::introspection::Locator;
7use typst_library::layout::grid::resolve::{Cell, CellGrid};
8use typst_library::layout::{Axes, Fragment, HAlignment, Regions, Sizing, VAlignment};
9use typst_library::model::{EnumElem, ListElem, Numbering, ParElem, ParbreakElem};
10use typst_library::text::TextElem;
11
12use crate::grid::GridLayouter;
13
14/// Layout the list.
15#[typst_macros::time(span = elem.span())]
16pub fn layout_list(
17    elem: &Packed<ListElem>,
18    engine: &mut Engine,
19    locator: Locator,
20    styles: StyleChain,
21    regions: Regions,
22) -> SourceResult<Fragment> {
23    let indent = elem.indent(styles);
24    let body_indent = elem.body_indent(styles);
25    let tight = elem.tight(styles);
26    let gutter = elem.spacing(styles).unwrap_or_else(|| {
27        if tight {
28            ParElem::leading_in(styles).into()
29        } else {
30            ParElem::spacing_in(styles).into()
31        }
32    });
33
34    let Depth(depth) = ListElem::depth_in(styles);
35    let marker = elem
36        .marker(styles)
37        .resolve(engine, styles, depth)?
38        // avoid '#set align' interference with the list
39        .aligned(HAlignment::Start + VAlignment::Top);
40
41    let mut cells = vec![];
42    let mut locator = locator.split();
43
44    for item in &elem.children {
45        // Text in wide lists shall always turn into paragraphs.
46        let mut body = item.body.clone();
47        if !tight {
48            body += ParbreakElem::shared();
49        }
50
51        cells.push(Cell::new(Content::empty(), locator.next(&())));
52        cells.push(Cell::new(marker.clone(), locator.next(&marker.span())));
53        cells.push(Cell::new(Content::empty(), locator.next(&())));
54        cells.push(Cell::new(
55            body.styled(ListElem::set_depth(Depth(1))),
56            locator.next(&item.body.span()),
57        ));
58    }
59
60    let grid = CellGrid::new(
61        Axes::with_x(&[
62            Sizing::Rel(indent.into()),
63            Sizing::Auto,
64            Sizing::Rel(body_indent.into()),
65            Sizing::Auto,
66        ]),
67        Axes::with_y(&[gutter.into()]),
68        cells,
69    );
70    let layouter = GridLayouter::new(&grid, regions, styles, elem.span());
71
72    layouter.layout(engine)
73}
74
75/// Layout the enumeration.
76#[typst_macros::time(span = elem.span())]
77pub fn layout_enum(
78    elem: &Packed<EnumElem>,
79    engine: &mut Engine,
80    locator: Locator,
81    styles: StyleChain,
82    regions: Regions,
83) -> SourceResult<Fragment> {
84    let numbering = elem.numbering(styles);
85    let reversed = elem.reversed(styles);
86    let indent = elem.indent(styles);
87    let body_indent = elem.body_indent(styles);
88    let tight = elem.tight(styles);
89    let gutter = elem.spacing(styles).unwrap_or_else(|| {
90        if tight {
91            ParElem::leading_in(styles).into()
92        } else {
93            ParElem::spacing_in(styles).into()
94        }
95    });
96
97    let mut cells = vec![];
98    let mut locator = locator.split();
99    let mut number =
100        elem.start(styles)
101            .unwrap_or_else(|| if reversed { elem.children.len() } else { 1 });
102    let mut parents = EnumElem::parents_in(styles);
103
104    let full = elem.full(styles);
105
106    // Horizontally align based on the given respective parameter.
107    // Vertically align to the top to avoid inheriting `horizon` or `bottom`
108    // alignment from the context and having the number be displaced in
109    // relation to the item it refers to.
110    let number_align = elem.number_align(styles);
111
112    for item in &elem.children {
113        number = item.number(styles).unwrap_or(number);
114
115        let context = Context::new(None, Some(styles));
116        let resolved = if full {
117            parents.push(number);
118            let content = numbering.apply(engine, context.track(), &parents)?.display();
119            parents.pop();
120            content
121        } else {
122            match numbering {
123                Numbering::Pattern(pattern) => {
124                    TextElem::packed(pattern.apply_kth(parents.len(), number))
125                }
126                other => other.apply(engine, context.track(), &[number])?.display(),
127            }
128        };
129
130        // Disable overhang as a workaround to end-aligned dots glitching
131        // and decreasing spacing between numbers and items.
132        let resolved =
133            resolved.aligned(number_align).styled(TextElem::set_overhang(false));
134
135        // Text in wide enums shall always turn into paragraphs.
136        let mut body = item.body.clone();
137        if !tight {
138            body += ParbreakElem::shared();
139        }
140
141        cells.push(Cell::new(Content::empty(), locator.next(&())));
142        cells.push(Cell::new(resolved, locator.next(&())));
143        cells.push(Cell::new(Content::empty(), locator.next(&())));
144        cells.push(Cell::new(
145            body.styled(EnumElem::set_parents(smallvec![number])),
146            locator.next(&item.body.span()),
147        ));
148        number =
149            if reversed { number.saturating_sub(1) } else { number.saturating_add(1) };
150    }
151
152    let grid = CellGrid::new(
153        Axes::with_x(&[
154            Sizing::Rel(indent.into()),
155            Sizing::Auto,
156            Sizing::Rel(body_indent.into()),
157            Sizing::Auto,
158        ]),
159        Axes::with_y(&[gutter.into()]),
160        cells,
161    );
162    let layouter = GridLayouter::new(&grid, regions, styles, elem.span());
163
164    layouter.layout(engine)
165}