summaryrefslogtreecommitdiff
path: root/library/src/layout/list.rs
blob: e39ec3f50fdf134faf62287e148f324d851f1d07 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
use crate::layout::{BlockElem, ParElem, Sizing, Spacing};
use crate::prelude::*;
use crate::text::TextElem;

use super::GridLayouter;

/// A bullet list.
///
/// Displays a sequence of items vertically, with each item introduced by a
/// marker.
///
/// ## Example { #example }
/// ```example
/// - *Content*
///   - Text
///   - Math
///   - Layout
///   - Visualize
///   - Meta
///   - Symbols
///
/// - *Compute*
///   #list(
///     [Foundations],
///     [Calculate],
///     [Construct],
///     [Data Loading],
///   )
/// ```
///
/// ## Syntax { #syntax }
/// This functions also has dedicated syntax: Start a line with a hyphen,
/// followed by a space to create a list item. A list item can contain multiple
/// paragraphs and other block-level content. All content that is indented
/// more than an item's hyphen becomes part of that item.
///
/// Display: Bullet List
/// Category: layout
#[element(Layout)]
#[scope(
    scope.define("item", ListItem::func());
    scope
)]
pub struct ListElem {
    /// If this is `{false}`, the items are spaced apart with [list
    /// spacing]($func/list.spacing). If it is `{true}`, they use normal
    /// [leading]($func/par.leading) instead. This makes the list more compact,
    /// which can look better if the items are short.
    ///
    /// In markup mode, the value of this parameter is determined based on
    /// whether items are separated with a blank line. If items directly follow
    /// each other, this is set to `{true}`; if items are separated by a blank
    /// line, this is set to `{false}`.
    ///
    /// ```example
    /// - If a list has a lot of text, and
    ///   maybe other inline content, it
    ///   should not be tight anymore.
    ///
    /// - To make a list wide, simply insert
    ///   a blank line between the items.
    /// ```
    #[default(true)]
    pub tight: bool,

    /// The marker which introduces each item.
    ///
    /// Instead of plain content, you can also pass an array with multiple
    /// markers that should be used for nested lists. If the list nesting depth
    /// exceeds the number of markers, the last one is repeated. For total
    /// control, you may pass a function that maps the list's nesting depth
    /// (starting from `{0}`) to a desired marker.
    ///
    /// ```example
    /// #set list(marker: [--])
    /// - A more classic list
    /// - With en-dashes
    ///
    /// #set list(marker: ([•], [--]))
    /// - Top-level
    ///   - Nested
    ///   - Items
    /// - Items
    /// ```
    #[default(ListMarker::Content(vec![TextElem::packed('•')]))]
    pub marker: ListMarker,

    /// The indent of each item.
    #[resolve]
    pub indent: Length,

    /// The spacing between the marker and the body of each item.
    #[resolve]
    #[default(Em::new(0.5).into())]
    pub body_indent: Length,

    /// The spacing between the items of a wide (non-tight) list.
    ///
    /// If set to `{auto}`, uses the spacing [below blocks]($func/block.below).
    pub spacing: Smart<Spacing>,

    /// The bullet list's children.
    ///
    /// When using the list syntax, adjacent items are automatically collected
    /// into lists, even through constructs like for loops.
    ///
    /// ```example
    /// #for letter in "ABC" [
    ///   - Letter #letter
    /// ]
    /// ```
    #[variadic]
    pub children: Vec<ListItem>,

    /// The nesting depth.
    #[internal]
    #[fold]
    depth: Depth,
}

impl Layout for ListElem {
    #[tracing::instrument(name = "ListElem::layout", skip_all)]
    fn layout(
        &self,
        vt: &mut Vt,
        styles: StyleChain,
        regions: Regions,
    ) -> SourceResult<Fragment> {
        let indent = self.indent(styles);
        let body_indent = self.body_indent(styles);
        let gutter = if self.tight(styles) {
            ParElem::leading_in(styles).into()
        } else {
            self.spacing(styles)
                .unwrap_or_else(|| BlockElem::below_in(styles).amount())
        };

        let depth = self.depth(styles);
        let marker = self
            .marker(styles)
            .resolve(vt, depth)?
            // avoid '#set align' interference with the list
            .aligned(Align::LEFT_TOP.into());

        let mut cells = vec![];
        for item in self.children() {
            cells.push(Content::empty());
            cells.push(marker.clone());
            cells.push(Content::empty());
            cells.push(item.body().styled(Self::set_depth(Depth)));
        }

        let layouter = GridLayouter::new(
            Axes::with_x(&[
                Sizing::Rel(indent.into()),
                Sizing::Auto,
                Sizing::Rel(body_indent.into()),
                Sizing::Auto,
            ]),
            Axes::with_y(&[gutter.into()]),
            &cells,
            regions,
            styles,
        );

        Ok(layouter.layout(vt)?.fragment)
    }
}

/// A bullet list item.
///
/// Display: Bullet List Item
/// Category: layout
#[element]
pub struct ListItem {
    /// The item's body.
    #[required]
    pub body: Content,
}

cast! {
    ListItem,
    v: Content => v.to::<Self>().cloned().unwrap_or_else(|| Self::new(v.clone())),
}

/// A list's marker.
#[derive(Debug, Clone, Hash)]
pub enum ListMarker {
    Content(Vec<Content>),
    Func(Func),
}

impl ListMarker {
    /// Resolve the marker for the given depth.
    fn resolve(&self, vt: &mut Vt, depth: usize) -> SourceResult<Content> {
        Ok(match self {
            Self::Content(list) => {
                list.get(depth).or(list.last()).cloned().unwrap_or_default()
            }
            Self::Func(func) => func.call_vt(vt, [depth])?.display(),
        })
    }
}

cast! {
    ListMarker,
    self => match self {
        Self::Content(vec) => if vec.len() == 1 {
            vec.into_iter().next().unwrap().into_value()
        } else {
            vec.into_value()
        },
        Self::Func(func) => func.into_value(),
    },
    v: Content => Self::Content(vec![v]),
    array: Array => {
        if array.is_empty() {
            bail!("array must contain at least one marker");
        }
        Self::Content(array.into_iter().map(Value::display).collect())
    },
    v: Func => Self::Func(v),
}

struct Depth;

cast! {
    Depth,
    self => Value::None,
    _: Value => Self,
}

impl Fold for Depth {
    type Output = usize;

    fn fold(self, outer: Self::Output) -> Self::Output {
        outer + 1
    }
}