summaryrefslogtreecommitdiff
path: root/src/eval/context.rs
blob: 1e09aaaf709fb696385758ec7825e1d2a2826c71 (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
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
use std::any::Any;
use std::rc::Rc;

use fontdock::FontStyle;

use super::*;
use crate::diag::Diag;
use crate::diag::{Deco, Feedback, Pass};
use crate::env::SharedEnv;
use crate::geom::{ChildAlign, Dir, Gen, LayoutDirs, Length, Linear, Sides, Size};
use crate::layout::{
    Expansion, Node, NodePad, NodePages, NodePar, NodeSpacing, NodeStack, NodeText, Tree,
};

/// The context for evaluation.
#[derive(Debug)]
pub struct EvalContext {
    /// The environment from which resources are gathered.
    pub env: SharedEnv,
    /// The active evaluation state.
    pub state: State,
    /// The accumulated feedback.
    feedback: Feedback,
    /// The finished page runs.
    runs: Vec<NodePages>,
    /// The stack of logical groups (paragraphs and such).
    ///
    /// Each entry contains metadata about the group and nodes that are at the
    /// same level as the group, which will return to `inner` once the group is
    /// finished.
    groups: Vec<(Box<dyn Any>, Vec<Node>)>,
    /// The nodes in the current innermost group
    /// (whose metadata is in `groups.last()`).
    inner: Vec<Node>,
}

impl EvalContext {
    /// Create a new evaluation context with a base state.
    pub fn new(env: SharedEnv, state: State) -> Self {
        Self {
            env,
            state,
            groups: vec![],
            inner: vec![],
            runs: vec![],
            feedback: Feedback::new(),
        }
    }

    /// Finish evaluation and return the created document.
    pub fn finish(self) -> Pass<Tree> {
        assert!(self.groups.is_empty(), "unfinished group");
        Pass::new(Tree { runs: self.runs }, self.feedback)
    }

    /// Add a diagnostic to the feedback.
    pub fn diag(&mut self, diag: Spanned<Diag>) {
        self.feedback.diags.push(diag);
    }

    /// Add a decoration to the feedback.
    pub fn deco(&mut self, deco: Spanned<Deco>) {
        self.feedback.decos.push(deco);
    }

    /// Push a layout node to the active group.
    ///
    /// Spacing nodes will be handled according to their [`Softness`].
    pub fn push(&mut self, node: impl Into<Node>) {
        let node = node.into();

        if let Node::Spacing(this) = node {
            if this.softness == Softness::Soft && self.inner.is_empty() {
                return;
            }

            if let Some(&Node::Spacing(other)) = self.inner.last() {
                if this.softness > other.softness {
                    self.inner.pop();
                } else if this.softness == Softness::Soft {
                    return;
                }
            }
        }

        self.inner.push(node);
    }

    /// Start a page group based on the active page state.
    ///
    /// The `softness` is a hint on whether empty pages should be kept in the
    /// output.
    ///
    /// This also starts an inner paragraph.
    pub fn start_page_group(&mut self, softness: Softness) {
        self.start_group(PageGroup {
            size: self.state.page.size,
            expand: self.state.page.expand,
            padding: self.state.page.margins(),
            dirs: self.state.dirs,
            align: self.state.align,
            softness,
        });
        self.start_par_group();
    }

    /// End a page group, returning its [`Softness`].
    ///
    /// Whether the page is kept when it's empty is decided by `keep_empty`
    /// based on its softness. If kept, the page is pushed to the finished page
    /// runs.
    ///
    /// This also ends an inner paragraph.
    pub fn end_page_group<F>(&mut self, keep_empty: F) -> Softness
    where
        F: FnOnce(Softness) -> bool,
    {
        self.end_par_group();
        let (group, children) = self.end_group::<PageGroup>();
        if !children.is_empty() || keep_empty(group.softness) {
            self.runs.push(NodePages {
                size: group.size,
                child: NodePad {
                    padding: group.padding,
                    child: NodeStack {
                        dirs: group.dirs,
                        align: group.align,
                        expand: group.expand,
                        children,
                    }
                    .into(),
                }
                .into(),
            })
        }
        group.softness
    }

    /// Start a content group.
    ///
    /// This also starts an inner paragraph.
    pub fn start_content_group(&mut self) {
        self.start_group(ContentGroup);
        self.start_par_group();
    }

    /// End a content group and return the resulting nodes.
    ///
    /// This also ends an inner paragraph.
    pub fn end_content_group(&mut self) -> Vec<Node> {
        self.end_par_group();
        self.end_group::<ContentGroup>().1
    }

    /// Start a paragraph group based on the active text state.
    pub fn start_par_group(&mut self) {
        let em = self.state.font.font_size();
        self.start_group(ParGroup {
            dirs: self.state.dirs,
            align: self.state.align,
            line_spacing: self.state.par.line_spacing.resolve(em),
        });
    }

    /// End a paragraph group and push it to its parent group if it's not empty.
    pub fn end_par_group(&mut self) {
        let (group, children) = self.end_group::<ParGroup>();
        if !children.is_empty() {
            self.push(NodePar {
                dirs: group.dirs,
                align: group.align,
                // FIXME: This is a hack and should be superseded by something
                //        better.
                cross_expansion: if self.groups.len() <= 1 {
                    Expansion::Fill
                } else {
                    Expansion::Fit
                },
                line_spacing: group.line_spacing,
                children,
            });
        }
    }

    /// Start a layouting group.
    ///
    /// All further calls to [`push`](Self::push) will collect nodes for this group.
    /// The given metadata will be returned alongside the collected nodes
    /// in a matching call to [`end_group`](Self::end_group).
    fn start_group<T: 'static>(&mut self, meta: T) {
        self.groups.push((Box::new(meta), std::mem::take(&mut self.inner)));
    }

    /// End a layouting group started with [`start_group`](Self::start_group).
    ///
    /// This returns the stored metadata and the collected nodes.
    #[track_caller]
    fn end_group<T: 'static>(&mut self) -> (T, Vec<Node>) {
        if let Some(&Node::Spacing(spacing)) = self.inner.last() {
            if spacing.softness == Softness::Soft {
                self.inner.pop();
            }
        }

        let (any, outer) = self.groups.pop().expect("no pushed group");
        let group = *any.downcast::<T>().expect("bad group type");
        (group, std::mem::replace(&mut self.inner, outer))
    }

    /// Set the directions if they would apply to different axes, producing an
    /// appropriate error otherwise.
    pub fn set_dirs(&mut self, new: Gen<Option<Spanned<Dir>>>) {
        let dirs = Gen::new(
            new.main.map(|s| s.v).unwrap_or(self.state.dirs.main),
            new.cross.map(|s| s.v).unwrap_or(self.state.dirs.cross),
        );

        if dirs.main.axis() != dirs.cross.axis() {
            self.state.dirs = dirs;
        } else {
            for dir in new.main.iter().chain(new.cross.iter()) {
                self.diag(error!(dir.span, "aligned axis"));
            }
        }
    }

    /// Apply a forced line break.
    pub fn apply_linebreak(&mut self) {
        self.end_par_group();
        self.start_par_group();
    }

    /// Apply a forced paragraph break.
    pub fn apply_parbreak(&mut self) {
        self.end_par_group();
        let em = self.state.font.font_size();
        self.push(NodeSpacing {
            amount: self.state.par.par_spacing.resolve(em),
            softness: Softness::Soft,
        });
        self.start_par_group();
    }

    /// Construct a text node from the given string based on the active text
    /// state.
    pub fn make_text_node(&self, text: String) -> NodeText {
        let mut variant = self.state.font.variant;

        if self.state.font.strong {
            variant.weight = variant.weight.thicken(300);
        }

        if self.state.font.emph {
            variant.style = match variant.style {
                FontStyle::Normal => FontStyle::Italic,
                FontStyle::Italic => FontStyle::Normal,
                FontStyle::Oblique => FontStyle::Normal,
            }
        }

        NodeText {
            text,
            align: self.state.align,
            dir: self.state.dirs.cross,
            font_size: self.state.font.font_size(),
            families: Rc::clone(&self.state.font.families),
            variant,
        }
    }
}

/// Defines how an item interacts with surrounding items.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub enum Softness {
    /// A soft item can be skipped in some circumstances.
    Soft,
    /// A hard item is always retained.
    Hard,
}

/// A group for a page run.
#[derive(Debug)]
struct PageGroup {
    size: Size,
    expand: Spec<Expansion>,
    padding: Sides<Linear>,
    dirs: LayoutDirs,
    align: ChildAlign,
    softness: Softness,
}

/// A group for generic content.
#[derive(Debug)]
struct ContentGroup;

/// A group for a paragraph.
#[derive(Debug)]
struct ParGroup {
    dirs: LayoutDirs,
    align: ChildAlign,
    line_spacing: Length,
}