Skip to main content

laser_pdf/elements/
page.rs

1use crate::*;
2
3pub struct Page<P: Element, D: Fn(&mut DecorationElements, usize, usize)> {
4    pub primary: P,
5    pub border_left: f32,
6    pub border_right: f32,
7    pub border_top: f32,
8    pub border_bottom: f32,
9    pub decoration_elements: D,
10}
11
12impl<P: Element, D: Fn(&mut DecorationElements, usize, usize)> Element for Page<P, D> {
13    fn first_location_usage(&self, ctx: FirstLocationUsageCtx) -> FirstLocationUsage {
14        if ctx.first_height < ctx.full_height {
15            FirstLocationUsage::WillSkip
16        } else {
17            FirstLocationUsage::WillUse
18        }
19    }
20
21    fn measure(&self, ctx: MeasureCtx) -> ElementSize {
22        if let Some(breakable) = ctx.breakable {
23            let mut extra_location_min_height = None;
24            let mut break_count = 0;
25
26            let primary_height = self.height(breakable.full_height);
27
28            self.primary.measure(MeasureCtx {
29                text_pieces_cache: ctx.text_pieces_cache,
30                width: WidthConstraint {
31                    max: self.width(ctx.width),
32                    expand: true,
33                },
34                first_height: primary_height,
35                breakable: Some(BreakableMeasure {
36                    full_height: primary_height,
37                    break_count: &mut break_count,
38                    extra_location_min_height: &mut extra_location_min_height,
39                }),
40            });
41
42            if ctx.first_height < breakable.full_height {
43                break_count += 1;
44            }
45
46            *breakable.break_count = break_count;
47
48            ElementSize {
49                width: Some(ctx.width.max),
50                height: Some(breakable.full_height),
51            }
52        } else {
53            ElementSize {
54                width: Some(ctx.width.max),
55                height: Some(ctx.first_height),
56            }
57        }
58    }
59
60    fn draw(&self, ctx: DrawCtx) -> ElementSize {
61        let primary_width = WidthConstraint {
62            max: self.width(ctx.width),
63            expand: true,
64        };
65
66        let mut breakable = ctx.breakable;
67
68        let height = breakable
69            .as_ref()
70            .map(|b| b.full_height)
71            .unwrap_or(ctx.first_height);
72
73        let primary_height = self.height(height);
74
75        let location;
76        let location_offset;
77
78        match breakable {
79            Some(ref mut breakable) if ctx.first_height < breakable.full_height => {
80                location = (breakable.do_break)(ctx.pdf, 0, None);
81                location_offset = 1;
82            }
83            _ => {
84                location = ctx.location;
85                location_offset = 0;
86            }
87        }
88
89        let mut break_count = 0;
90
91        // We put the primary content one layer up so the decoration elements can be used for things
92        // like watermarks.
93        let primary_location = location.next_layer(ctx.pdf);
94
95        self.primary.draw(DrawCtx {
96            pdf: ctx.pdf,
97            text_pieces_cache: ctx.text_pieces_cache,
98            location: Location {
99                pos: (
100                    location.pos.0 + self.border_left,
101                    location.pos.1 - self.border_top,
102                ),
103                ..primary_location
104            },
105            width: primary_width,
106            first_height: primary_height,
107            preferred_height: None,
108            breakable: breakable
109                .as_mut()
110                .map(|breakable| {
111                    |pdf: &mut Pdf, location_idx: u32, _| {
112                        break_count = break_count.max(location_idx + 1);
113                        let mut location = (breakable.do_break)(
114                            pdf,
115                            location_idx + location_offset,
116                            Some(breakable.full_height),
117                        );
118
119                        location = location.next_layer(pdf);
120                        location.pos.0 += self.border_left;
121                        location.pos.1 -= self.border_top;
122
123                        location
124                    }
125                })
126                .as_mut()
127                .map(|get_location| BreakableDraw {
128                    full_height: primary_height,
129                    preferred_height_break_count: 0,
130                    do_break: get_location,
131                }),
132        });
133
134        if let Some(breakable) = breakable {
135            for i in 0..=break_count {
136                let location = if i == 0 {
137                    location.clone()
138                } else {
139                    (breakable.do_break)(
140                        ctx.pdf,
141                        i + location_offset - 1,
142                        Some(breakable.full_height),
143                    )
144                };
145
146                (self.decoration_elements)(
147                    &mut DecorationElements {
148                        pdf: ctx.pdf,
149                        text_pieces_cache: ctx.text_pieces_cache,
150                        location,
151                        width: ctx.width.max,
152                        height,
153                    },
154                    i as usize,
155                    (break_count + 1) as usize,
156                );
157            }
158        } else {
159            (self.decoration_elements)(
160                &mut DecorationElements {
161                    pdf: ctx.pdf,
162                    text_pieces_cache: ctx.text_pieces_cache,
163                    location,
164                    width: ctx.width.max,
165                    height,
166                },
167                0,
168                1,
169            );
170        }
171
172        ElementSize {
173            width: Some(ctx.width.max),
174            height: Some(height),
175        }
176    }
177}
178
179impl<P: Element, D: Fn(&mut DecorationElements, usize, usize)> Page<P, D> {
180    fn width(&self, width: WidthConstraint) -> f32 {
181        width.max - self.border_left - self.border_right
182    }
183
184    fn height(&self, full_height: f32) -> f32 {
185        full_height - self.border_top - self.border_bottom
186    }
187}
188
189pub struct DecorationElements<'a> {
190    pdf: &'a mut Pdf,
191    text_pieces_cache: &'a TextPiecesCache,
192    location: Location,
193    width: f32,
194    height: f32,
195}
196
197#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
198pub enum X {
199    Left(f32),
200    Right(f32),
201}
202
203#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
204pub enum Y {
205    Top(f32),
206    Bottom(f32),
207}
208
209impl<'a> DecorationElements<'a> {
210    pub fn add(&mut self, element: &impl Element, pos: (X, Y), width: Option<f32>) {
211        element.draw(DrawCtx {
212            pdf: self.pdf,
213            text_pieces_cache: self.text_pieces_cache,
214            location: Location {
215                pos: (
216                    match pos.0 {
217                        X::Left(left) => self.location.pos.0 + left,
218                        X::Right(right) => self.location.pos.0 + self.width - right,
219                    },
220                    match pos.1 {
221                        Y::Top(top) => self.location.pos.1 - top,
222                        Y::Bottom(bottom) => self.location.pos.1 - self.height + bottom,
223                    },
224                ),
225                ..self.location
226            },
227            width: WidthConstraint {
228                max: width.unwrap_or_else(|| match pos.0 {
229                    X::Left(left) => self.width - left,
230                    X::Right(right) => right,
231                }),
232                expand: width.is_some(),
233            },
234            first_height: match pos.1 {
235                Y::Top(top) => self.height - top,
236                Y::Bottom(bottom) => bottom,
237            },
238            preferred_height: None,
239            breakable: None,
240        });
241    }
242}
243
244#[cfg(test)]
245mod tests {
246    use insta::assert_debug_snapshot;
247
248    use super::*;
249    use crate::{
250        elements::ref_element::RefElement,
251        test_utils::{record_passes::RecordPasses, *},
252    };
253    use X::*;
254    use Y::*;
255
256    #[test]
257    fn test_unbreakable() {
258        let output = test_element(
259            TestElementParams {
260                width: WidthConstraint {
261                    max: 10.,
262                    expand: false,
263                },
264                first_height: 20.,
265                breakable: None,
266                pos: (10., 30.0),
267                ..Default::default()
268            },
269            |assert, callback| {
270                let primary = RecordPasses::new(FakeText {
271                    lines: 1,
272                    line_height: 5.,
273                    width: 3.,
274                });
275
276                let top_left = RecordPasses::new(FakeText {
277                    lines: 1,
278                    line_height: 5.,
279                    width: 6.,
280                });
281
282                let bottom_right = RecordPasses::new(FakeText {
283                    lines: 1,
284                    line_height: 4.,
285                    width: 3.,
286                });
287
288                let element = Page {
289                    primary: RefElement(&primary),
290                    border_left: 2.,
291                    border_right: 3.,
292                    border_top: 4.,
293                    border_bottom: 5.,
294                    decoration_elements: |content: &mut DecorationElements, _, _| {
295                        content.add(&top_left, (Left(1.), Top(2.)), None);
296                        content.add(&bottom_right, (Right(2.), Bottom(5.)), Some(4.));
297                    },
298                };
299
300                let ret = callback.call(element);
301
302                if assert {
303                    assert_debug_snapshot!((
304                        primary.into_passes(),
305                        top_left.into_passes(),
306                        bottom_right.into_passes()
307                    ));
308                }
309
310                ret
311            },
312        );
313
314        assert_debug_snapshot!(output);
315    }
316
317    #[test]
318    fn test_breakable() {
319        let output = test_element(
320            TestElementParams {
321                width: WidthConstraint {
322                    max: 10.,
323                    expand: false,
324                },
325                first_height: 19.,
326                breakable: Some(TestElementParamsBreakable {
327                    preferred_height_break_count: 5,
328                    full_height: 20.,
329                }),
330                pos: (10., 30.0),
331                ..Default::default()
332            },
333            |assert, callback| {
334                let primary = RecordPasses::new(FakeText {
335                    lines: 3,
336                    line_height: 5.,
337                    width: 3.,
338                });
339
340                let top_right = RecordPasses::new(FakeText {
341                    lines: 1,
342                    line_height: 5.,
343                    width: 6.,
344                });
345
346                let bottom_left = RecordPasses::new(FakeText {
347                    lines: 1,
348                    line_height: 4.,
349                    width: 3.,
350                });
351
352                let element = Page {
353                    primary: RefElement(&primary),
354                    border_left: 2.,
355                    border_right: 3.,
356                    border_top: 4.,
357                    border_bottom: 5.,
358                    decoration_elements: |content: &mut DecorationElements, _, _| {
359                        content.add(&top_right, (Right(2.5), Top(2.)), None);
360                        content.add(&bottom_left, (Left(2.), Bottom(5.)), Some(4.));
361                    },
362                };
363
364                let ret = callback.call(element);
365
366                if assert {
367                    assert_debug_snapshot!((
368                        primary.into_passes(),
369                        top_right.into_passes(),
370                        bottom_left.into_passes()
371                    ));
372                }
373
374                ret
375            },
376        );
377
378        assert_debug_snapshot!(output);
379    }
380}